[
  {
    "path": ".cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"language\": \"en,en-us\",\n  \"words\": [\n    \"gress\",\n    \"doctoc\",\n    \"minmax\",\n    \"openmct\",\n    \"datasources\",\n    \"Sinewave\",\n    \"deregistration\",\n    \"unregisters\",\n    \"codecov\",\n    \"carryforward\",\n    \"Chacon\",\n    \"Straub\",\n    \"OWASP\",\n    \"Testathon\",\n    \"Testathons\",\n    \"testathon\",\n    \"npmjs\",\n    \"treeitem\",\n    \"timespan\",\n    \"Timespan\",\n    \"spinbutton\",\n    \"popout\",\n    \"textbox\",\n    \"tablist\",\n    \"Telem\",\n    \"codecoverage\",\n    \"browserless\",\n    \"networkidle\",\n    \"nums\",\n    \"mgmt\",\n    \"faultname\",\n    \"gantt\",\n    \"sharded\",\n    \"MMOC\",\n    \"codegen\",\n    \"viewports\",\n    \"updatesnapshots\",\n    \"browsercontexts\",\n    \"miminum\",\n    \"testcase\",\n    \"testsuite\",\n    \"domcontentloaded\",\n    \"Tracefile\",\n    \"lcov\",\n    \"linecov\",\n    \"Browserless\",\n    \"webserver\",\n    \"yamcs\",\n    \"quickstart\",\n    \"subobject\",\n    \"autosize\",\n    \"Horz\",\n    \"vehicula\",\n    \"Praesent\",\n    \"pharetra\",\n    \"Duis\",\n    \"eget\",\n    \"arcu\",\n    \"elementum\",\n    \"mauris\",\n    \"Donec\",\n    \"nunc\",\n    \"quis\",\n    \"Proin\",\n    \"elit\",\n    \"Nunc\",\n    \"Aenean\",\n    \"mollis\",\n    \"hendrerit\",\n    \"Vestibulum\",\n    \"placerat\",\n    \"velit\",\n    \"augue\",\n    \"Quisque\",\n    \"mattis\",\n    \"lectus\",\n    \"rutrum\",\n    \"Fusce\",\n    \"tincidunt\",\n    \"nibh\",\n    \"blandit\",\n    \"urna\",\n    \"Nullam\",\n    \"congue\",\n    \"enim\",\n    \"Morbi\",\n    \"bibendum\",\n    \"Vivamus\",\n    \"imperdiet\",\n    \"Pellentesque\",\n    \"cursus\",\n    \"Aliquam\",\n    \"orci\",\n    \"Suspendisse\",\n    \"amet\",\n    \"justo\",\n    \"Etiam\",\n    \"vestibulum\",\n    \"ullamcorper\",\n    \"Cras\",\n    \"aliquet\",\n    \"Mauris\",\n    \"Nulla\",\n    \"scelerisque\",\n    \"viverra\",\n    \"metus\",\n    \"condimentum\",\n    \"varius\",\n    \"nulla\",\n    \"sapien\",\n    \"Curabitur\",\n    \"tristique\",\n    \"Nonsectetur\",\n    \"convallis\",\n    \"accumsan\",\n    \"lacus\",\n    \"posuere\",\n    \"turpis\",\n    \"egestas\",\n    \"feugiat\",\n    \"tortor\",\n    \"faucibus\",\n    \"euismod\",\n    \"pathing\",\n    \"testcases\",\n    \"Noneditable\",\n    \"listitem\",\n    \"Gantt\",\n    \"timelist\",\n    \"timestrip\",\n    \"networkevents\",\n    \"fetchpriority\",\n    \"persistable\",\n    \"Persistable\",\n    \"persistability\",\n    \"Persistability\",\n    \"testdata\",\n    \"Testdata\",\n    \"metdata\",\n    \"timeconductor\",\n    \"contenteditable\",\n    \"autoscale\",\n    \"Autoscale\",\n    \"prepan\",\n    \"sinewave\",\n    \"cyanish\",\n    \"driv\",\n    \"searchbox\",\n    \"datetime\",\n    \"timeframe\",\n    \"recents\",\n    \"recentobjects\",\n    \"gsearch\",\n    \"Disp\",\n    \"Cloc\",\n    \"noselect\",\n    \"requestfailed\",\n    \"viewlarge\",\n    \"Imageurl\",\n    \"thumbstrip\",\n    \"checkmark\",\n    \"Unshelve\",\n    \"autosized\",\n    \"chacskaylo\",\n    \"numberfield\",\n    \"OPENMCT\",\n    \"Autoflow\",\n    \"Timelist\",\n    \"faultmanagement\",\n    \"GEOSPATIAL\",\n    \"geospatial\",\n    \"plotspatial\",\n    \"annnotation\",\n    \"keystrings\",\n    \"undelete\",\n    \"sometag\",\n    \"containee\",\n    \"composability\",\n    \"mutables\",\n    \"Mutables\",\n    \"composee\",\n    \"handleoutsideclick\",\n    \"Datetime\",\n    \"Perc\",\n    \"autodismiss\",\n    \"filetree\",\n    \"deeptailor\",\n    \"keystring\",\n    \"reindex\",\n    \"unlisten\",\n    \"symbolsfont\",\n    \"ellipsize\",\n    \"TIMESYSTEM\",\n    \"Metadatas\",\n    \"unsub\",\n    \"callbacktwo\",\n    \"unsubscribetwo\",\n    \"telem\",\n    \"unemitted\",\n    \"granually\",\n    \"timesystem\",\n    \"metadatas\",\n    \"iteratees\",\n    \"metadatum\",\n    \"printj\",\n    \"sprintf\",\n    \"unlisteners\",\n    \"amts\",\n    \"reregistered\",\n    \"hudsonfoo\",\n    \"onclone\",\n    \"autoflow\",\n    \"xdescribe\",\n    \"mockmct\",\n    \"Autoflowed\",\n    \"plotly\",\n    \"relayout\",\n    \"Plotly\",\n    \"Yaxis\",\n    \"showlegend\",\n    \"textposition\",\n    \"xaxis\",\n    \"automargin\",\n    \"fixedrange\",\n    \"yaxis\",\n    \"Axistype\",\n    \"showline\",\n    \"bglayer\",\n    \"autorange\",\n    \"hoverinfo\",\n    \"dotful\",\n    \"Dotful\",\n    \"cartesianlayer\",\n    \"scatterlayer\",\n    \"textfont\",\n    \"ampm\",\n    \"cdef\",\n    \"horz\",\n    \"STYLEABLE\",\n    \"styleable\",\n    \"afff\",\n    \"shdw\",\n    \"braintree\",\n    \"vals\",\n    \"Subobject\",\n    \"Shdw\",\n    \"Movebar\",\n    \"inspectable\",\n    \"Stringformatter\",\n    \"sclk\",\n    \"Objectpath\",\n    \"Keystring\",\n    \"duplicatable\",\n    \"composees\",\n    \"Composees\",\n    \"Composee\",\n    \"callthrough\",\n    \"objectpath\",\n    \"createable\",\n    \"noneditable\",\n    \"Classname\",\n    \"classname\",\n    \"selectedfaults\",\n    \"accum\",\n    \"newpersisted\",\n    \"Metadatum\",\n    \"MCWS\",\n    \"YAMCS\",\n    \"frameid\",\n    \"containerid\",\n    \"mmgis\",\n    \"PERC\",\n    \"curval\",\n    \"viewbox\",\n    \"mutablegauge\",\n    \"Flatbush\",\n    \"flatbush\",\n    \"Indicies\",\n    \"Marqueed\",\n    \"NSEW\",\n    \"nsew\",\n    \"vrover\",\n    \"gimbled\",\n    \"Pannable\",\n    \"unsynced\",\n    \"Unsynced\",\n    \"pannable\",\n    \"autoscroll\",\n    \"TIMESTRIP\",\n    \"TWENTYFOUR\",\n    \"FULLSIZE\",\n    \"intialize\",\n    \"Timestrip\",\n    \"spyon\",\n    \"Unlistener\",\n    \"multipane\",\n    \"DATESTRING\",\n    \"akhenry\",\n    \"Niklas\",\n    \"Hertzen\",\n    \"Kash\",\n    \"Nouroozi\",\n    \"Bostock\",\n    \"BOSTOCK\",\n    \"Arnout\",\n    \"Kazemier\",\n    \"Karolis\",\n    \"Narkevicius\",\n    \"Ashkenas\",\n    \"Madhavan\",\n    \"Iskren\",\n    \"Ivov\",\n    \"Chernev\",\n    \"Borshchov\",\n    \"painterro\",\n    \"sheetjs\",\n    \"Yuxi\",\n    \"ACITON\",\n    \"localstorage\",\n    \"Linkto\",\n    \"Painterro\",\n    \"Editability\",\n    \"filteredsnapshots\",\n    \"Fromimage\",\n    \"muliple\",\n    \"notebookstorage\",\n    \"Andpage\",\n    \"pixelize\",\n    \"Quickstart\",\n    \"indexhtml\",\n    \"youradminpassword\",\n    \"chttpd\",\n    \"sourcefiles\",\n    \"USERPASS\",\n    \"XPUT\",\n    \"adipiscing\",\n    \"eiusmod\",\n    \"tempor\",\n    \"incididunt\",\n    \"labore\",\n    \"dolore\",\n    \"aliqua\",\n    \"perspiciatis\",\n    \"iteree\",\n    \"submodels\",\n    \"symlog\",\n    \"Plottable\",\n    \"antisymlog\",\n    \"docstrings\",\n    \"webglcontextlost\",\n    \"gridlines\",\n    \"Xaxis\",\n    \"Crosshairs\",\n    \"telemetrylimit\",\n    \"xscale\",\n    \"yscale\",\n    \"untracks\",\n    \"swatched\",\n    \"NULLVALUE\",\n    \"unobserver\",\n    \"unsubscriber\",\n    \"drap\",\n    \"Averager\",\n    \"averager\",\n    \"movecolumnfromindex\",\n    \"callout\",\n    \"Konqueror\",\n    \"unmark\",\n    \"hitarea\",\n    \"Hitarea\",\n    \"Unmark\",\n    \"controlbar\",\n    \"reactified\",\n    \"perc\",\n    \"DHMS\",\n    \"timespans\",\n    \"timeframes\",\n    \"Timesystems\",\n    \"Hilite\",\n    \"datetimes\",\n    \"momentified\",\n    \"ucontents\",\n    \"TIMELIST\",\n    \"Timeframe\",\n    \"Guirk\",\n    \"resizeable\",\n    \"iframing\",\n    \"Btns\",\n    \"Ctrls\",\n    \"Chakra\",\n    \"Petch\",\n    \"propor\",\n    \"phoneandtablet\",\n    \"desktopandtablet\",\n    \"Imgs\",\n    \"UNICODES\",\n    \"datatable\",\n    \"csvg\",\n    \"cpath\",\n    \"cellipse\",\n    \"xlink\",\n    \"cstyle\",\n    \"bfill\",\n    \"ctitle\",\n    \"eicon\",\n    \"interactability\",\n    \"AFFORDANCES\",\n    \"affordance\",\n    \"scrollcontainer\",\n    \"Icomoon\",\n    \"icomoon\",\n    \"configurability\",\n    \"btns\",\n    \"AUTOFLOW\",\n    \"DATETIME\",\n    \"infobubble\",\n    \"thumbsbubble\",\n    \"codehilite\",\n    \"vscroll\",\n    \"bgsize\",\n    \"togglebutton\",\n    \"Hacskaylo\",\n    \"noie\",\n    \"fullscreen\",\n    \"horiz\",\n    \"menubutton\",\n    \"SNAPSHOTTING\",\n    \"snapshotting\",\n    \"PAINTERRO\",\n    \"ptro\",\n    \"PLOTLY\",\n    \"gridlayer\",\n    \"xtick\",\n    \"ytick\",\n    \"subobjects\",\n    \"Ucontents\",\n    \"Userand\",\n    \"Userbefore\",\n    \"brdr\",\n    \"ALPH\",\n    \"Recents\",\n    \"Qbert\",\n    \"Infobubble\",\n    \"haslink\",\n    \"VPID\",\n    \"vpid\",\n    \"updatedtest\",\n    \"KHTML\",\n    \"Chromezilla\",\n    \"Safarifox\",\n    \"deregistering\",\n    \"hundredtized\",\n    \"dhms\",\n    \"unthrottled\",\n    \"Codecov\",\n    \"dont\",\n    \"mediump\",\n    \"sinonjs\",\n    \"generatedata\",\n    \"grandsearch\",\n    \"websockets\",\n    \"swgs\",\n    \"memlab\",\n    \"devmode\",\n    \"blockquote\",\n    \"blockquotes\",\n    \"Blockquote\",\n    \"Blockquotes\",\n    \"oger\",\n    \"lcovonly\",\n    \"gcov\",\n    \"WCAG\",\n    \"stackedplot\",\n    \"Andale\",\n    \"unnormalized\",\n    \"checksnapshots\",\n    \"specced\",\n    \"composables\",\n    \"countup\",\n    \"darkmatter\",\n    \"Undeletes\",\n    \"SSSZ\",\n    \"pageerror\",\n    \"annotatable\",\n    \"requestfinished\",\n    \"LOCF\",\n    \"Unack\"\n  ],\n  \"dictionaries\": [\"npm\", \"softwareTerms\", \"node\", \"html\", \"css\", \"bash\", \"en_US\", \"en-gb\", \"misc\"],\n  \"ignorePaths\": [\n    \"package.json\",\n    \"dist/**\",\n    \"package-lock.json\",\n    \"node_modules\",\n    \"coverage\",\n    \"*.log\",\n    \"html-test-results\",\n    \"test-results\"\n  ]\n}\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "const LEGACY_FILES = ['example/**'];\n/** @type {import('eslint').Linter.Config} */\nconst config = {\n  env: {\n    browser: true,\n    es2024: true,\n    jasmine: true,\n    amd: true,\n    node: true\n  },\n  globals: {\n    _: 'readonly',\n    __webpack_public_path__: 'writeable',\n    __OPENMCT_VERSION__: 'readonly',\n    __OPENMCT_BUILD_DATE__: 'readonly',\n    __OPENMCT_REVISION__: 'readonly',\n    __OPENMCT_BUILD_BRANCH__: 'readonly',\n    __OPENMCT_ROOT_RELATIVE__: 'readonly'\n  },\n  plugins: ['prettier', 'unicorn', 'simple-import-sort'],\n  extends: [\n    'eslint:recommended',\n    'plugin:compat/recommended',\n    'plugin:vue/vue3-recommended',\n    'plugin:you-dont-need-lodash-underscore/compatible',\n    'plugin:prettier/recommended',\n    'plugin:no-unsanitized/DOM'\n  ],\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    parser: '@babel/eslint-parser',\n    requireConfigFile: false,\n    allowImportExportEverywhere: true,\n    ecmaVersion: 'latest',\n    ecmaFeatures: {\n      impliedStrict: true\n    },\n    sourceType: 'module'\n  },\n  rules: {\n    'simple-import-sort/imports': 'warn',\n    'simple-import-sort/exports': 'warn',\n    'vue/no-deprecated-dollar-listeners-api': 'warn',\n    'vue/no-deprecated-events-api': 'warn',\n    'vue/no-v-for-template-key': 'off',\n    'vue/no-v-for-template-key-on-child': 'error',\n    'vue/component-name-in-template-casing': ['error', 'PascalCase'],\n    'prettier/prettier': 'error',\n    'you-dont-need-lodash-underscore/omit': 'off',\n    'you-dont-need-lodash-underscore/throttle': 'off',\n    'you-dont-need-lodash-underscore/flatten': 'off',\n    'you-dont-need-lodash-underscore/get': 'off',\n    'no-bitwise': 'error',\n    curly: 'error',\n    eqeqeq: 'error',\n    'guard-for-in': 'error',\n    'no-extend-native': 'error',\n    'no-inner-declarations': 'off',\n    'no-use-before-define': ['error', 'nofunc'],\n    'no-caller': 'error',\n    'no-irregular-whitespace': 'error',\n    'no-new': 'error',\n    'no-shadow': 'error',\n    'no-undef': 'error',\n    'no-unused-vars': [\n      'error',\n      {\n        vars: 'all',\n        args: 'none'\n      }\n    ],\n    'no-console': 'off',\n    'new-cap': [\n      'error',\n      {\n        capIsNew: false,\n        properties: false\n      }\n    ],\n    'dot-notation': 'error',\n\n    // https://eslint.org/docs/rules/no-case-declarations\n    'no-case-declarations': 'error',\n    // https://eslint.org/docs/rules/max-classes-per-file\n    'max-classes-per-file': ['error', 1],\n    // https://eslint.org/docs/rules/no-eq-null\n    'no-eq-null': 'error',\n    // https://eslint.org/docs/rules/no-eval\n    'no-eval': 'error',\n    // https://eslint.org/docs/rules/no-implicit-globals\n    'no-implicit-globals': 'error',\n    // https://eslint.org/docs/rules/no-implied-eval\n    'no-implied-eval': 'error',\n    // https://eslint.org/docs/rules/no-lone-blocks\n    'no-lone-blocks': 'error',\n    // https://eslint.org/docs/rules/no-loop-func\n    'no-loop-func': 'error',\n    // https://eslint.org/docs/rules/no-new-func\n    'no-new-func': 'error',\n    // https://eslint.org/docs/rules/no-new-wrappers\n    'no-new-wrappers': 'error',\n    // https://eslint.org/docs/rules/no-octal-escape\n    'no-octal-escape': 'error',\n    // https://eslint.org/docs/rules/no-proto\n    'no-proto': 'error',\n    // https://eslint.org/docs/rules/no-return-await\n    'no-return-await': 'error',\n    // https://eslint.org/docs/rules/no-script-url\n    'no-script-url': 'error',\n    // https://eslint.org/docs/rules/no-self-compare\n    'no-self-compare': 'error',\n    // https://eslint.org/docs/rules/no-sequences\n    'no-sequences': 'error',\n    // https://eslint.org/docs/rules/no-unmodified-loop-condition\n    'no-unmodified-loop-condition': 'error',\n    // https://eslint.org/docs/rules/no-useless-call\n    'no-useless-call': 'error',\n    // https://eslint.org/docs/rules/no-nested-ternary\n    'no-nested-ternary': 'error',\n    // https://eslint.org/docs/rules/no-useless-computed-key\n    'no-useless-computed-key': 'error',\n    // https://eslint.org/docs/rules/no-var\n    'no-var': 'error',\n    // https://eslint.org/docs/rules/one-var\n    'one-var': ['error', 'never'],\n    // https://eslint.org/docs/rules/default-case-last\n    'default-case-last': 'error',\n    // https://eslint.org/docs/rules/default-param-last\n    'default-param-last': 'error',\n    // https://eslint.org/docs/rules/grouped-accessor-pairs\n    'grouped-accessor-pairs': 'error',\n    // https://eslint.org/docs/rules/no-constructor-return\n    'no-constructor-return': 'error',\n    // https://eslint.org/docs/rules/array-callback-return\n    'array-callback-return': 'error',\n    // https://eslint.org/docs/rules/no-invalid-this\n    'no-invalid-this': 'error', // Believe this one actually surfaces some bugs\n    // https://eslint.org/docs/rules/func-style\n    'func-style': ['error', 'declaration'],\n    // https://eslint.org/docs/rules/no-unused-expressions\n    'no-unused-expressions': 'error',\n    // https://eslint.org/docs/rules/no-useless-concat\n    'no-useless-concat': 'error',\n    // https://eslint.org/docs/rules/radix\n    radix: 'error',\n    // https://eslint.org/docs/rules/require-await\n    'require-await': 'error',\n    // https://eslint.org/docs/rules/no-alert\n    'no-alert': 'error',\n    // https://eslint.org/docs/rules/no-useless-constructor\n    'no-useless-constructor': 'error',\n    // https://eslint.org/docs/rules/no-duplicate-imports\n    'no-duplicate-imports': 'error',\n\n    // https://eslint.org/docs/rules/no-implicit-coercion\n    'no-implicit-coercion': 'error',\n    //https://eslint.org/docs/rules/no-unneeded-ternary\n    'no-unneeded-ternary': 'error',\n    'unicorn/filename-case': [\n      'error',\n      {\n        cases: {\n          pascalCase: true\n        },\n        ignore: ['^.*\\\\.(js|cjs|mjs)$']\n      }\n    ],\n    'vue/first-attribute-linebreak': 'error',\n    'vue/multiline-html-element-content-newline': 'off',\n    'vue/singleline-html-element-content-newline': 'off',\n    'vue/no-mutating-props': 'off' // TODO: Remove this rule and fix resulting errors\n  },\n  overrides: [\n    {\n      files: LEGACY_FILES,\n      rules: {\n        'no-unused-vars': [\n          'error',\n          {\n            vars: 'all',\n            args: 'none',\n            varsIgnorePattern: 'controller'\n          }\n        ],\n        'no-nested-ternary': 'off',\n        'no-var': 'off',\n        'one-var': 'off'\n      }\n    }\n  ]\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# git-blame ignored revisions\n# To configure, run:\n#   git config blame.ignoreRevsFile .git-blame-ignore-revs\n# Requires Git > 2.23\n# See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt\n\n# vue-eslint update 2019\n14a0f84c1bcd56886d7c9e4e6afa8f7d292734e5\n# eslint changes 2022\nd80b6923541704ab925abf0047cbbc58735c27e2\n# Copyright year update 2022\n4a9744e916d24122a81092f6b7950054048ba860\n# Copyright year update 2023\n8040b275fcf2ba71b42cd72d4daa64bb25c19c2d\n# Apply `prettier` formatting\ncaa7bc6faebc204f67aedae3e35fb0d0d3ce27a7\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: File a Bug !\ntitle: ''\nlabels: type:bug\nassignees: ''\n\n---\n\n<!--- Focus on user impact in the title. Use the Summary Field to -->\n<!--- describe the problem technically. -->\n\n#### Summary\n<!--- A description of the issue encountered. When possible, a description -->\n<!--- of the impact of the issue. What use case does it impede?-->\n\n#### Expected vs Current Behavior\n<!--- Tell us what should have happened -->\n\n#### Steps to Reproduce\n<!--- Provide a link to a live example, or an unambiguous set of steps to -->\n<!--- reproduce this bug. Include code to reproduce, if relevant -->\n1.\n2.\n3.\n4.\n\n#### Environment\n<!--- If encountered on local machine, execute the following:\n<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->\n* Open MCT Version: <!--- date of build, version, or SHA -->\n* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->\n* OS:\n* Browser:\n\n#### Impact Check List\n<!--- Please select from the following options -->\n- [ ] Data loss or misrepresented data?\n- [ ] Regression? Did this used to work or has it always been broken?\n- [ ] Is there a workaround available?\n- [ ] Does this impact a critical component?\n- [ ] Is this just a visual bug with no functional impact?\n- [ ] Does this block the execution of e2e tests?\n- [ ] Does this have an impact on Performance?\n\n#### Additional Information\n<!--- Include any screenshots, gifs, or logs which will expedite triage -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Discussions\n    url: https://github.com/nasa/openmct/discussions\n    about: Have a question about the project?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement-request.md",
    "content": "---\nname: Enhancement request\nabout: Suggest an enhancement or new improvement for this project\ntitle: ''\nlabels: type:enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/maintenance-type.md",
    "content": "---\nname: Maintenance\nabout: Add, update or remove documentation, tests, or dependencies.\ntitle: ''\nlabels: type:maintenance\nassignees: ''\n\n---\n\n#### Summary\n<!--- Generally describe the purpose of the change. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Note: Please open the PR in draft form until you are ready for active review. -->\nCloses <!--- Insert Issue Number(s) this PR addresses. Start by typing # will open a dropdown of recent issues. Note: this does not work on PRs which target release branches -->\n\n### Describe your changes:\n<!--- Describe your changes and add any comments about your approach either here or inline if code comments aren't added -->\n\n### All Submissions:\n\n* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?\n* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?\n* [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?\n\n### Author Checklist\n\n* [ ] Changes address original issue?\n* [ ] Tests included and/or updated with changes?\n* [ ] Has this been smoke tested?\n* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.\n* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.\n* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?\n\n### Reviewer Checklist\n\n* [ ] Changes appear to address issue?\n* [ ] Reviewer has tested changes by following the provided instructions?\n* [ ] Changes appear not to be breaking changes?\n* [ ] Appropriate automated tests included?\n* [ ] Code style and in-line documentation are appropriate?\n"
  },
  {
    "path": ".github/codeql/codeql-config.yml",
    "content": "name: 'Custom CodeQL config'\n\npaths-ignore:\n  # Ignore e2e tests and framework\n  - e2e\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n    open-pull-requests-limit: 10\n    rebase-strategy: 'disabled'\n    labels:\n      - 'pr:daveit'\n      - 'pr:e2e'\n      - 'type:maintenance'\n      - 'dependencies'\n      - 'pr:platform'\n    ignore:\n      #We have to source the playwright container which is not detected by Dependabot\n      - dependency-name: '@playwright/test'\n      - dependency-name: 'playwright-core'\n      #Lots of noise in these type patch releases.\n      - dependency-name: '@babel/eslint-parser'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: 'eslint-plugin-vue'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: 'babel-loader'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: 'sinon'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: 'moment-timezone'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: '@types/lodash'\n        update-types: ['version-update:semver-patch']\n      - dependency-name: 'marked'\n        update-types: ['version-update:semver-patch']\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    schedule:\n      interval: 'daily'\n    rebase-strategy: 'disabled'\n    labels:\n      - 'pr:daveit'\n      - 'type:maintenance'\n      - 'dependencies'\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  categories:\n    - title: 💥 Notable Changes\n      labels:\n        - notable_change\n    - title: 🏕 Features\n      labels:\n        - type:feature\n    - title: 🎉 Enhancements\n      labels:\n        - type:enhancement\n      exclude:\n        labels:\n          - type:feature\n    - title: 🔧 Maintenance\n      labels:\n        - type:maintenance\n    - title: ⚡ Performance\n      labels:\n        - performance\n    - title: 👒 Dependencies\n      labels:\n        - dependencies\n    - title: 🐛 Bug Fixes\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: 'CodeQL'\n\non:\n  push:\n    branches: [master, 'release/*']\n  pull_request:\n    branches: [master, 'release/*']\n    paths-ignore:\n      - '**/*Spec.js'\n      - '**/*.md'\n      - '**/*.txt'\n      - '**/*.yml'\n      - '**/*.yaml'\n      - '**/*.spec.js'\n      - '**/*.config.js'\n  schedule:\n    - cron: '28 21 * * 3'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v3\n        with:\n          config-file: ./.github/codeql/codeql-config.yml\n          languages: javascript\n          queries: security-and-quality\n\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v3\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/e2e-couchdb.yml",
    "content": "name: 'e2e-couchdb'\non:\n  push:\n  pull_request:\n    types:\n      - opened\njobs:\n  e2e-couchdb:\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/hydrogen'\n\n      - name: Cache NPM dependencies\n        uses: actions/cache@v3\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - run: npm ci --no-audit --progress=false\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        continue-on-error: true\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - run: npx playwright@1.57.0 install\n\n      - name: Start CouchDB Docker Container and Init with Setup Scripts\n        run: |\n          export $(cat src/plugins/persistence/couch/.env.ci | xargs)\n          docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach\n          sleep 3\n          bash src/plugins/persistence/couch/setup-couchdb.sh\n          bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh\n\n      - name: Run CouchDB Tests\n        env:\n          COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}\n        run: npm run test:e2e:couchdb\n\n      - name: Generate Code Coverage Report\n        run: npm run cov:e2e:report\n\n      - name: Publish Results to Codecov.io\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./coverage/e2e/lcov.info\n          flags: e2e-full\n          fail_ci_if_error: true\n          verbose: true\n\n      - name: Archive test results\n        if: success() || failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: e2e-couchdb-test-results\n          path: test-results\n          overwrite: true\n\n      - name: Archive html test results\n        if: success() || failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: e2e-couchdb-html-test-results\n          path: html-test-results\n          overwrite: true\n\n      - name: Remove pr:e2e:couchdb label (if present)\n        if: always()\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { owner, repo, number } = context.issue;\n            const labelToRemove = 'pr:e2e:couchdb';\n            try {\n              await github.rest.issues.removeLabel({\n                owner,\n                repo,\n                issue_number: number,\n                name: labelToRemove\n              });\n            } catch (error) {\n              core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);\n            }\n"
  },
  {
    "path": ".github/workflows/e2e-full.yml",
    "content": "name: 'e2e-full'\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch:\n  pull_request:\n    types:\n      - labeled\n      - opened\n  schedule:\n    - cron: '0 0 * * *'\njobs:\n  e2e-full:\n    if: contains(github.event.pull_request.labels.*.name, 'pr:e2e') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n          - windows-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/hydrogen'\n\n      - name: Cache NPM dependencies\n        uses: actions/cache@v3\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - run: npx playwright@1.57.0 install\n      - run: npx playwright install chrome-beta\n      - run: npm ci --no-audit --progress=false\n      - run: npm run test:e2e:full -- --max-failures=40\n      - run: npm run cov:e2e:report || true\n      - shell: bash\n        env:\n          SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}\n        run: |\n          npm run cov:e2e:full:publish\n      - name: Archive test results\n        if: success() || failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: e2e-pr-test-results\n          path: test-results\n          overwrite: true\n\n      - name: Remove pr:e2e label (if present)\n        if: always()\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { owner, repo, number } = context.issue;\n            const labelToRemove = 'pr:e2e';\n            try {\n              await github.rest.issues.removeLabel({\n                owner,\n                repo,\n                issue_number: number,\n                name: labelToRemove\n              });\n            } catch (error) {\n              core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);\n            }\n"
  },
  {
    "path": ".github/workflows/e2e-perf.yml",
    "content": "name: 'e2e-perf'\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch:\n  pull_request:\n    types:\n      - labeled\n      - opened\n  schedule:\n    - cron: '0 0 * * *'\njobs:\n  e2e-full:\n    if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/hydrogen'\n\n      - name: Cache NPM dependencies\n        uses: actions/cache@v3\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - run: npx playwright@1.57.0 install\n      - run: npm ci --no-audit --progress=false\n      - run: npm run test:perf:localhost\n      - run: npm run test:perf:contract\n      - run: npm run test:perf:memory\n      - name: Archive test results\n        if: success() || failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: e2e-perf-test-results\n          path: test-results\n          overwrite: true\n\n      - name: Remove pr:e2e:perf label (if present)\n        if: always()\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { owner, repo, number } = context.issue;\n            const labelToRemove = 'pr:e2e:perf';\n            try {\n              await github.rest.issues.removeLabel({\n                owner,\n                repo,\n                issue_number: number,\n                name: labelToRemove\n              });\n            } catch (error) {\n              core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);\n            }\n"
  },
  {
    "path": ".github/workflows/npm-prerelease.yml",
    "content": "# This workflow will run tests using node and then publish a package to npmjs when a prerelease is created\n# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages\n\nname: npm_prerelease\n\non:\n  release:\n    types: [prereleased]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/hydrogen\n      - run: npm ci\n      - run: |\n          echo \"//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN\" >> ~/.npmrc\n          npm whoami\n          npm publish --access=public --tag unstable openmct\n      # - run: npm test\n\n  publish-npm-prerelease:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/hydrogen\n          registry-url: https://registry.npmjs.org/\n      - run: npm ci\n      - run: npm publish --access=public --tag unstable\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/pr-platform.yml",
    "content": "name: 'pr-platform'\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch:\n  pull_request:\n    types:\n      - labeled\n      - opened\n  schedule:\n    - cron: '0 0 * * *'\njobs:\n  pr-platform:\n    if: contains(github.event.pull_request.labels.*.name, 'pr:platform') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - ubuntu-latest\n          - macos-latest\n          - windows-latest\n        node_version:\n          - lts/iron\n          - lts/hydrogen\n        architecture:\n          - x64\n\n    name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node_version }}\n          architecture: ${{ matrix.architecture }}\n\n      - name: Cache NPM dependencies\n        uses: actions/cache@v3\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ matrix.node_version }}-\n\n      - run: npm ci --no-audit --progress=false\n\n      - run: npm test\n\n      - run: npm run lint -- --quiet\n\n      - name: Remove pr:platform label (if present)\n        if: always()\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { owner, repo, number } = context.issue;\n            const labelToRemove = 'pr:platform';\n            try {\n              await github.rest.issues.removeLabel({\n                owner,\n                repo,\n                issue_number: number,\n                name: labelToRemove\n              });\n            } catch (error) {\n              core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);\n            }\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: PR\n\non:\n  push:\n  pull_request:\n    types:\n      - opened\n      - labeled\n\nenv:\n  NODE_ENV: development\n  NODE_VERSION: 22\n  PERCY_POSTINSTALL_BROWSER: \"true\"\n  PERCY_LOGLEVEL: \"debug\"\n  PERCY_PARALLEL_TOTAL: 2\n  PERCY_TOKEN: \"${{ secrets.PERCY_TOKEN }}\"\n\njobs:\n    generate_cache_key:\n        runs-on: ubuntu-latest\n        container: mcr.microsoft.com/playwright:v1.57.0-jammy \n        steps:\n          - uses: actions/checkout@v4\n          - id: generate_cache_key\n            run: |\n                lock_hash=\"$(sha256sum package-lock.json | awk '{print $1}')\"\n                echo \"cache_key=node${NODE_VERSION}-deps-${lock_hash}\" >> \"$GITHUB_OUTPUT\"\n        outputs:\n            cache_key: ${{ steps.generate_cache_key.outputs.cache_key }}\n    build_and_cache_dependencies_if_needed:\n        needs: generate_cache_key\n        runs-on: ubuntu-latest\n        container: mcr.microsoft.com/playwright:v1.57.0-jammy \n        steps:\n          - uses: actions/checkout@v4\n          - uses: actions/setup-node@v4\n            with:\n                node-version: ${{ env.NODE_VERSION }}\n                cache: npm\n          - uses: actions/cache/restore@v4\n            id: node_deps_restore_cache\n            with:\n              path: |\n                node_modules\n                e2e/node_modules\n                dist\n              key: ${{ needs.generate_cache_key.outputs.cache_key }}\n          - run: npm ci\n            if: steps.node_deps_restore_cache.outputs.cache-hit != 'true' || contains(github.event.pull_request.labels.*.name, 'build:clean')\n          - name: Remove build:clean label (if present)\n            if: contains(github.event.pull_request.labels.*.name, 'build:clean')\n            uses: actions/github-script@v6\n            with:\n              script: |\n                const { owner, repo, number } = context.issue;\n                const labelToRemove = 'build:clean';\n                try {\n                  await github.rest.issues.removeLabel({\n                    owner,\n                    repo,\n                    issue_number: number,\n                    name: labelToRemove\n                  });\n                } catch (error) {\n                  core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);\n                }\n          - uses: actions/cache/save@v4\n            id: node_deps_save_cache\n            if: steps.node_deps_restore_cache.outputs.cache-hit != 'true' || contains(github.event.pull_request.labels.*.name, 'build:clean')\n            with:\n              path: |\n                node_modules\n                e2e/node_modules\n                dist\n              key: ${{ needs.generate_cache_key.outputs.cache_key }}\n        outputs:\n            cache_key: ${{ needs.generate_cache_key.outputs.cache_key }}\n    lint:\n        needs: \n            - build_and_cache_dependencies_if_needed\n        runs-on: ubuntu-latest\n        container: mcr.microsoft.com/playwright:v1.57.0-jammy \n        steps:\n          - uses: actions/checkout@v4\n          - uses: actions/setup-node@v4\n            with:\n                node-version: ${{ env.NODE_VERSION }}\n                cache: npm\n          - uses: actions/cache/restore@v4\n            with:\n              path: |\n                node_modules\n                e2e/node_modules\n                dist\n              key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n          - run: npm run lint\n    unit-test:\n        needs: \n            - build_and_cache_dependencies_if_needed\n        runs-on: ubuntu-latest\n        steps:\n          - uses: actions/checkout@v4\n          - uses: actions/setup-node@v4\n            with:\n                node-version: ${{ env.NODE_VERSION }}\n                cache: npm\n          - uses: actions/cache/restore@v4\n            with:\n              path: |\n                node_modules\n                e2e/node_modules\n                dist\n              key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n          - run: |\n              mkdir -p dist/reports/tests/\n              npm ci\n              npm run test\n          - uses: codecov/codecov-action@v4\n            if: always()\n            with:\n              token: ${{ secrets.CODECOV_TOKEN }}\n              files: ./coverage/unit/lcov.info\n              flags: unit\n              name: unit-test-${{ github.run_id }}\n              fail_ci_if_error: false\n          - uses: actions/upload-artifact@v4\n            if: always()\n            with:\n              name: unit-test-results\n              path: dist/reports/tests/\n              if-no-files-found: warn\n          - uses: actions/upload-artifact@v4\n            if: always()\n            with:\n              name: unit-test-coverage\n              path: coverage\n              if-no-files-found: warn\n          - name: Collect version artifacts\n            if: always()\n            run: |\n              mkdir -p /tmp/artifacts\n              printenv NODE_ENV > /tmp/artifacts/NODE_ENV.txt || true\n              npm -v > /tmp/artifacts/npm-version.txt\n              node -v > /tmp/artifacts/node-version.txt\n              ls -latR > /tmp/artifacts/dir.txt\n          - uses: actions/upload-artifact@v4\n            if: always()\n            with:\n              name: unit-test-artifacts\n              path: /tmp/artifacts\n              if-no-files-found: warn\n    e2e-test:\n      name: e2e-ci (shard ${{ matrix.shard }}/4)\n      runs-on: ubuntu-latest\n      container: mcr.microsoft.com/playwright:v1.57.0-jammy \n      strategy:\n        fail-fast: false\n        matrix:\n          shard: [1, 2, 3, 4]\n      needs: \n        - build_and_cache_dependencies_if_needed\n      steps:\n        - uses: actions/checkout@v4\n        - uses: actions/setup-node@v4\n          with:\n            node-version: ${{ env.NODE_VERSION }}\n            cache: npm\n        - uses: actions/cache/restore@v4\n          with:\n            path: |\n              node_modules\n              e2e/node_modules\n              dist\n            key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n        - run: |\n            mkdir -p test-results\n            npm run test:e2e:ci -- --shard=${{ matrix.shard }}/4\n        - run: npm run cov:e2e:report || true\n        - uses: codecov/codecov-action@v4\n          with:\n            token: ${{ secrets.CODECOV_TOKEN }}\n            files: ./coverage/e2e/lcov.info\n            flags: e2e-ci\n            name: e2e-ci-${{ github.run_id }}-shard-${{ matrix.shard }}\n            fail_ci_if_error: false\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: e2e-ci-test-results-shard-${{ matrix.shard }}\n            path: test-results\n            if-no-files-found: warn\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: e2e-ci-coverage-shard-${{ matrix.shard }}\n            path: coverage\n            if-no-files-found: warn\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: e2e-ci-html-results-shard-${{ matrix.shard }}\n            path: html-test-results\n            if-no-files-found: warn\n        - name: Collect version artifacts\n          if: always()\n          run: |\n            mkdir -p /tmp/artifacts\n            printenv NODE_ENV > /tmp/artifacts/NODE_ENV.txt || true\n            npm -v > /tmp/artifacts/npm-version.txt\n            node -v > /tmp/artifacts/node-version.txt\n            ls -latR > /tmp/artifacts/dir.txt\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: e2e-ci-artifacts-shard-${{ matrix.shard }}\n            path: /tmp/artifacts\n            if-no-files-found: warn\n    visual-a11y:\n      name: visual-a11y-ci\n      runs-on: ubuntu-latest\n      container: mcr.microsoft.com/playwright:v1.57.0-jammy \n      needs: \n          - build_and_cache_dependencies_if_needed\n      strategy:\n        fail-fast: false\n        matrix:\n          shard: [1, 2]\n      steps:\n        - uses: actions/checkout@v4\n        - uses: actions/setup-node@v4\n          with:\n            node-version: ${{ env.NODE_VERSION }}\n            cache: npm\n        - uses: actions/cache/restore@v4\n          with:\n            path: |\n              node_modules\n              e2e/node_modules\n              dist\n            key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n        - run: npm run test:e2e:visual:ci -- --shard=${{ matrix.shard }}/2\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: visual-a11y-ci-test-results-shard-${{ matrix.shard }}\n            path: test-results\n            if-no-files-found: warn\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: visual-a11y-ci-html-results-shard-${{ matrix.shard }}\n            path: html-test-results\n            if-no-files-found: warn\n        - name: Collect version artifacts\n          if: always()\n          run: |\n            mkdir -p /tmp/artifacts\n            printenv NODE_ENV > /tmp/artifacts/NODE_ENV.txt || true\n            npm -v > /tmp/artifacts/npm-version.txt\n            node -v > /tmp/artifacts/node-version.txt\n            ls -latR > /tmp/artifacts/dir.txt\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: visual-a11y-ci-artifacts-shard-${{ matrix.shard }}\n            path: /tmp/artifacts\n            if-no-files-found: warn\n    perf-test:\n      runs-on: ubuntu-latest\n      container: mcr.microsoft.com/playwright:v1.57.0-jammy \n      needs: \n          - build_and_cache_dependencies_if_needed\n      if: ${{ always() }}\n      steps:\n        - uses: actions/checkout@v4\n        - uses: actions/setup-node@v4\n          with:\n            node-version: ${{ env.NODE_VERSION }}\n            cache: npm\n        - uses: actions/cache/restore@v4\n          with:\n            path: |\n              node_modules\n              e2e/node_modules\n              dist\n            key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n        - run: npm run test:perf:localhost\n        - run: npm run test:perf:contract\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: perf-test-results\n            path: test-results\n            if-no-files-found: warn\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: perf-html-results\n            path: html-test-results\n            if-no-files-found: warn\n        - name: Collect version artifacts\n          if: always()\n          run: |\n            mkdir -p /tmp/artifacts\n            printenv NODE_ENV > /tmp/artifacts/NODE_ENV.txt || true\n            npm -v > /tmp/artifacts/npm-version.txt\n            node -v > /tmp/artifacts/node-version.txt\n            ls -latR > /tmp/artifacts/dir.txt\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: perf-test-artifacts\n            path: /tmp/artifacts\n            if-no-files-found: warn\n    mem-test:\n      runs-on: ubuntu-latest\n      container: mcr.microsoft.com/playwright:v1.57.0-jammy \n      needs: \n          - build_and_cache_dependencies_if_needed\n      if: ${{ always() }}\n      steps:\n        - uses: actions/checkout@v4\n        - uses: actions/setup-node@v4\n          with:\n            node-version: ${{ env.NODE_VERSION }}\n            cache: npm\n        - uses: actions/cache/restore@v4\n          with:\n            path: |\n              node_modules\n              e2e/node_modules\n              dist\n            key: ${{ needs.build_and_cache_dependencies_if_needed.outputs.cache_key }}\n        - run: npm run test:perf:memory\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: mem-test-results\n            path: test-results\n            if-no-files-found: warn\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: mem-html-results\n            path: html-test-results\n            if-no-files-found: warn\n        - name: Collect version artifacts\n          if: always()\n          run: |\n            mkdir -p /tmp/artifacts\n            printenv NODE_ENV > /tmp/artifacts/NODE_ENV.txt || true\n            npm -v > /tmp/artifacts/npm-version.txt\n            node -v > /tmp/artifacts/node-version.txt\n            ls -latR > /tmp/artifacts/dir.txt\n        - uses: actions/upload-artifact@v4\n          if: always()\n          with:\n            name: mem-test-artifacts\n            path: /tmp/artifacts\n            if-no-files-found: warn\n"
  },
  {
    "path": ".github/workflows/prcop-config.json",
    "content": "{\n  \"linters\": [\n    {\n      \"name\": \"descriptionRegexp\",\n      \"config\": {\n        \"regexp\": \"[x|X]] Testing instructions\",\n        \"errorMessage\": \":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions\"\n      }\n    },\n    {\n      \"name\": \"descriptionMinWords\",\n      \"config\": {\n        \"minWordsCount\": 160,\n        \"errorMessage\": \":police_officer: Please, be sure to use existing PR template.\"\n      }\n    }\n  ],\n  \"disableWord\": \"pr:daveit\"\n}\n"
  },
  {
    "path": ".github/workflows/prcop.yml",
    "content": "name: PRCop\n\non:\n  pull_request:\n    types:\n      - labeled\n      - unlabeled\n      - milestoned\n      - demilestoned\n      - opened\n      - reopened\n      - synchronize\n      - edited\n  pull_request_review_comment:\n    types:\n      - created\nenv:\n  LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }}\njobs:\n  prcop:\n    runs-on: ubuntu-latest\n    name: Template Check\n    steps:\n      - name: Linting Pull Request\n        uses: makaroni4/prcop@v1.0.35\n        with:\n          config-file: '.github/workflows/prcop-config.json'\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  check-type-label:\n    name: Check type Label\n    runs-on: ubuntu-latest\n    steps:\n      - if: contains( env.LABELS, 'type:' ) == false\n        run: exit 1\n  check-milestone:\n    name: Check Milestone\n    runs-on: ubuntu-latest\n    steps:\n      - if: github.event.pull_request.milestone == null && contains( env.LABELS, 'no milestone' ) == false\n        run: exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "*.scssc\n*.zip\n*.gzip\n*.tgz\n*.DS_Store\n*.swp\n\n# Compiled CSS, unless directly added\n*.sass-cache\n*COMPILE.css\n*.css\n*.css.map\n\n# Intellij project configuration files\n*.idea\n*.iml\n\n# VSCode\n.vscode/settings.json\n\n# Build output\ntarget\ndist\n\n# Mac OS X Finder\n.DS_Store\n\n# Node, Bower dependencies\nnode_modules\nbower_components\n\n# npm-debug log\nnpm-debug.log\n\n# karma reports\nreport.*.json\n\n# e2e test artifacts\ntest-results\nhtml-test-results\n\n# couchdb scripting artifacts\nsrc/plugins/persistence/couch/.env.local\nindex.html.bak\n\n# codecov artifacts\n.nyc_output\ncoverage\ncodecov\n\n# Don't commit MacOS screenshots\n*-darwin.png\n/.run/All Tests.run.xml\n"
  },
  {
    "path": ".npmignore",
    "content": "# Ignore everything first (will not ignore special files like LICENSE.md,\n# README.md, and package.json)...\n/**/*\n\n# ...but include these folders...\n!/dist/**/*\n!/src/**/*\n\n# We might be able to remove this if it is not imported by any project directly.\n# https://github.com/nasa/openmct/issues/4992\n!/example/**/*\n\n# ...except for these files in the above folders.\n/src/**/*Spec.js\n/src/**/test/\n# TODO move test utils into test/ folders\n/src/utils/testing.js\n\n# Also include these special top-level files.\n!copyright-notice.js\n!copyright-notice.html\n!index.html\n!openmct.js\n!SECURITY.md\n\n# Dont include the example html\ndist/index.html"
  },
  {
    "path": ".npmrc",
    "content": "loglevel=warn\n\n#Prevent folks from ignoring an important error when building from source\nengine-strict=true\n"
  },
  {
    "path": ".nvmrc",
    "content": "lts/*"
  },
  {
    "path": ".prettierignore",
    "content": "# Docs\n*.md\n\n# Build output\ntarget\ndist\n\n# Mac OS X Finder\n.DS_Store\n\n# Node dependencies\nnode_modules\n\n# npm-debug log\nnpm-debug.log\n\n# karma reports\nreport.*.json\n\n# e2e test artifacts\ntest-results\nhtml-test-results\n\n# codecov artifacts\n.nyc_output\ncoverage\ncodecov\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"none\",\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"endOfLine\": \"auto\"\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.\n  // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp\n\n  // List of extensions which should be recommended for users of this workspace.\n  \"recommendations\": [\n    \"Vue.volar\",\n    \"dbaeumer.vscode-eslint\",\n    \"rvest.vs-code-prettier-eslint\"\n  ],\n  // List of extensions recommended by VS Code that should not be recommended for users of this workspace.\n  \"unwantedRecommendations\": [\"octref.vetur\"]\n}\n"
  },
  {
    "path": ".webpack/webpack.common.mjs",
    "content": "/*\nThis is the OpenMCT common webpack file. It is imported by the other three webpack configurations:\n - webpack.prod.mjs - the production configuration for OpenMCT (default)\n - webpack.dev.mjs - the development configuration for OpenMCT\n - webpack.coverage.mjs - imports webpack.dev.js and adds code coverage\nThere are separate npm scripts to use these configurations, though simply running `npm install`\nwill use the default production configuration.\n*/\nimport { execSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimport MiniCssExtractPlugin from 'mini-css-extract-plugin';\nimport { VueLoaderPlugin } from 'vue-loader';\nimport webpack from 'webpack';\nimport { merge } from 'webpack-merge';\nlet gitRevision = 'error-retrieving-revision';\nlet gitBranch = 'error-retrieving-branch';\n\nconst { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));\n\ntry {\n  //Caching of GitHub actions causes git directory ownership issues.\n  execSync('git config --global --add safe.directory /__w/openmct/openmct');\n  gitRevision = execSync('git rev-parse HEAD').toString().trim();\n  gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();\n} catch (err) {\n  console.warn(err);\n}\n\nconst projectRootDir = fileURLToPath(new URL('../', import.meta.url));\n\n/** @type {import('webpack').Configuration} */\nconst config = {\n  context: projectRootDir,\n  devServer: {\n    client: {\n      progress: true,\n      overlay: {\n        // Disable overlay for runtime errors.\n        // See: https://github.com/webpack/webpack-dev-server/issues/4771\n        runtimeErrors: false\n      }\n    }\n  },\n  entry: {\n    openmct: './openmct.js',\n    generatorWorker: './example/generator/generatorWorker.js',\n    couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',\n    inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',\n    compsMathWorker: './src/plugins/comps/CompsMathWorker.js',\n    espressoTheme: './src/plugins/themes/espresso-theme.scss',\n    snowTheme: './src/plugins/themes/snow-theme.scss',\n    darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'\n  },\n  output: {\n    globalObject: 'this',\n    filename: '[name].js',\n    path: path.resolve(projectRootDir, 'dist'),\n    library: {\n      name: 'openmct',\n      type: 'umd',\n      export: 'default'\n    },\n    publicPath: '',\n    hashFunction: 'xxhash64',\n    clean: true\n  },\n  resolve: {\n    alias: {\n      '@': path.join(projectRootDir, 'src'),\n      legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'),\n      csv: 'comma-separated-values',\n      bourbon: 'bourbon.scss',\n      'plotly-basic': 'plotly.js-basic-dist-min',\n      'plotly-gl2d': 'plotly.js-gl2d-dist-min',\n      printj: 'printj/printj.mjs',\n      styles: path.join(projectRootDir, 'src/styles'),\n      MCT: path.join(projectRootDir, 'src/MCT'),\n      testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),\n      objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),\n      utils: path.join(projectRootDir, 'src/utils'),\n      vue: 'vue/dist/vue.esm-bundler'\n    }\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      __OPENMCT_VERSION__: `'${version}'`,\n      __OPENMCT_BUILD_DATE__: `'${new Date()}'`,\n      __OPENMCT_REVISION__: `'${gitRevision}'`,\n      __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,\n      __VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true\n      __VUE_PROD_DEVTOOLS__: false, // enable/disable devtools support in production, default: false\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // enable/disable hydration mismatch details in production, default: false\n    }),\n    new VueLoaderPlugin(),\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: 'src/images/favicons',\n          to: 'favicons'\n        },\n        {\n          from: './index.html',\n          transform: function (content) {\n            return content.toString().replace(/dist\\//g, '');\n          }\n        },\n        {\n          from: 'src/plugins/imagery/layers',\n          to: 'imagery'\n        }\n      ]\n    }),\n    new MiniCssExtractPlugin({\n      filename: '[name].css',\n      chunkFilename: '[name].css'\n    }),\n    // Add a UTF-8 BOM to CSS output to avoid random mojibake\n    new webpack.BannerPlugin({\n      test: /.*Theme\\.css$/,\n      raw: true,\n      banner: '@charset \"UTF-8\";'\n    })\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.(sc|sa|c)ss$/,\n        use: [\n          MiniCssExtractPlugin.loader,\n          {\n            loader: 'css-loader'\n          },\n          {\n            loader: 'resolve-url-loader'\n          },\n          {\n            loader: 'sass-loader',\n            options: { sourceMap: true }\n          }\n        ]\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: {\n          compilerOptions: {\n            hoistStatic: false,\n            whitespace: 'preserve'\n          }\n        }\n      },\n      {\n        test: /\\.html$/,\n        type: 'asset/source'\n      },\n      {\n        test: /\\.(jpg|jpeg|png|svg)$/,\n        type: 'asset/resource',\n        generator: {\n          filename: 'images/[name][ext]'\n        }\n      },\n      {\n        test: /\\.ico$/,\n        type: 'asset/resource',\n        generator: {\n          filename: 'icons/[name][ext]'\n        }\n      },\n      {\n        test: /\\.(woff|woff2?|eot|ttf)$/,\n        type: 'asset/resource',\n        generator: {\n          filename: 'fonts/[name][ext]'\n        }\n      }\n    ]\n  },\n  stats: 'errors-warnings',\n  performance: {\n    // We should eventually consider chunking to decrease\n    // these values\n    maxEntrypointSize: 27000000,\n    maxAssetSize: 27000000\n  }\n};\n\nexport default config;\n"
  },
  {
    "path": ".webpack/webpack.coverage.mjs",
    "content": "/*\nThis file extends the webpack.dev.mjs config to add babel istanbul coverage.\nOpenMCT Continuous Integration servers use this configuration to add code coverage\ninformation to pull requests.\n*/\n\nimport config from './webpack.dev.mjs';\n\nconfig.devtool = 'inline-source-map';\nconfig.devServer.hot = false;\n\nconfig.module.rules.push({\n  test: /\\.js$/,\n  exclude: /(Spec\\.js$)|(node_modules)/,\n  use: {\n    loader: 'babel-loader',\n    options: {\n      retainLines: true,\n      plugins: [\n        [\n          'babel-plugin-istanbul',\n          {\n            extension: ['.js', '.vue']\n          }\n        ]\n      ]\n    }\n  }\n});\n\nexport default config;\n"
  },
  {
    "path": ".webpack/webpack.dev.mjs",
    "content": "/*\nThis configuration should be used for development purposes. It contains full source map, a\ndevServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.\nIf OpenMCT is to be used for a production server, use webpack.prod.mjs instead.\n*/\nimport { fileURLToPath } from 'node:url';\n\nimport path from 'path';\nimport webpack from 'webpack';\nimport { merge } from 'webpack-merge';\n\nimport common from './webpack.common.mjs';\n\nexport default merge(common, {\n  mode: 'development',\n  watchOptions: {\n    // Since we use require.context, webpack is watching the entire directory.\n    // We need to exclude any files we don't want webpack to watch.\n    // See: https://webpack.js.org/configuration/watch/#watchoptions-exclude\n    ignored: [\n      '**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,\n      '**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files\n      '**/*.{sh,md,png,ttf,woff,svg}', // Non source files\n      '**/.*' // dotfiles and dotfolders\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      __OPENMCT_ROOT_RELATIVE__: '\"dist/\"'\n    })\n  ],\n  devtool: 'eval-source-map',\n  devServer: {\n    devMiddleware: {\n      writeToDisk: (filePathString) => {\n        const filePath = path.parse(filePathString);\n        const shouldWrite = !filePath.base.includes('hot-update');\n\n        return shouldWrite;\n      }\n    },\n    watchFiles: ['src/**/*.css', 'example/**/*.css'],\n    static: [{\n      directory: fileURLToPath(new URL('../dist', import.meta.url)),\n      publicPath: '/dist',\n      watch: false\n    }, {\n      directory: fileURLToPath(new URL('../e2e/test-data', import.meta.url)),\n      publicPath: '/test-data',\n      watch: false\n    }]\n  }\n});\n"
  },
  {
    "path": ".webpack/webpack.prod.mjs",
    "content": "/*\nThis configuration should be used for production installs.\nIt is the default webpack configuration.\n*/\n\nimport webpack from 'webpack';\nimport { merge } from 'webpack-merge';\n\nimport common from './webpack.common.mjs';\n\nexport default merge(common, {\n  mode: 'production',\n  plugins: [\n    new webpack.DefinePlugin({\n      __OPENMCT_ROOT_RELATIVE__: '\"\"'\n    })\n  ],\n  devtool: 'source-map'\n});\n"
  },
  {
    "path": "API.md",
    "content": "<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n**Table of Contents**\n\n- [Developing Applications With Open MCT](#developing-applications-with-open-mct)\n  - [Scope and purpose of this document](#scope-and-purpose-of-this-document)\n  - [Building From Source](#building-from-source)\n  - [Starting an Open MCT application](#starting-an-open-mct-application)\n  - [Types](#types)\n    - [Using Types](#using-types)\n    - [Limitations](#limitations)\n  - [Plugins](#plugins)\n    - [Defining and Installing a New Plugin](#defining-and-installing-a-new-plugin)\n  - [Domain Objects and Identifiers](#domain-objects-and-identifiers)\n    - [Object Attributes](#object-attributes)\n    - [Domain Object Types](#domain-object-types)\n  - [Root Objects](#root-objects)\n  - [Object Providers](#object-providers)\n  - [Composition Providers](#composition-providers)\n    - [Adding Composition Providers](#adding-composition-providers)\n    - [Default Composition Provider](#default-composition-provider)\n  - [Telemetry API](#telemetry-api)\n    - [Integrating Telemetry Sources](#integrating-telemetry-sources)\n      - [Telemetry Metadata](#telemetry-metadata)\n        - [Values](#values)\n          - [Value Hints](#value-hints)\n        - [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry)\n      - [Telemetry Providers](#telemetry-providers)\n      - [Telemetry Requests and Responses](#telemetry-requests-and-responses)\n      - [Request Strategies **draft**](#request-strategies-draft)\n        - [`latest` request strategy](#latest-request-strategy)\n        - [`minmax` request strategy](#minmax-request-strategy)\n      - [Telemetry Formats](#telemetry-formats)\n        - [Built-in Formats](#built-in-formats)\n          - [**Number Format (default):**](#number-format-default)\n          - [**String Format**](#string-format)\n          - [**Enum Format**](#enum-format)\n        - [Registering Formats](#registering-formats)\n      - [Telemetry Data](#telemetry-data)\n        - [Telemetry Datums](#telemetry-datums)\n      - [Limit Evaluators **draft**](#limit-evaluators-draft)\n    - [Telemetry Consumer APIs **draft**](#telemetry-consumer-apis-draft)\n  - [Time API](#time-api)\n    - [Time Systems and Bounds](#time-systems-and-bounds)\n      - [Defining and Registering Time Systems](#defining-and-registering-time-systems)\n      - [Getting and Setting the Active Time System](#getting-and-setting-the-active-time-system)\n      - [Time Bounds](#time-bounds)\n    - [Clocks](#clocks)\n      - [Defining and registering clocks](#defining-and-registering-clocks)\n      - [Getting and setting active clock](#getting-and-setting-active-clock)\n      - [⚠️ \\[DEPRECATED\\] Stopping an active clock](#️-deprecated-stopping-an-active-clock)\n      - [Clock Offsets](#clock-offsets)\n    - [Time Modes](#time-modes)\n      - [Time Mode Helper Methods](#time-mode-helper-methods)\n    - [Time Events](#time-events)\n      - [List of Time Events](#list-of-time-events)\n    - [The Time Conductor](#the-time-conductor)\n      - [Time Conductor Configuration](#time-conductor-configuration)\n      - [Example conductor configuration](#example-conductor-configuration)\n  - [Indicators](#indicators)\n    - [The URL Status Indicator](#the-url-status-indicator)\n    - [Creating a Simple Indicator](#creating-a-simple-indicator)\n    - [Custom Indicators](#custom-indicators)\n  - [Priority API](#priority-api)\n    - [Priority Types](#priority-types)\n  - [User API](#user-api)\n    - [Example](#example)\n  - [Visibility-Based Rendering in View Providers](#visibility-based-rendering-in-view-providers)\n    - [Overview](#overview)\n    - [Implementing Visibility-Based Rendering](#implementing-visibility-based-rendering)\n    - [Example](#example-1)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n# Developing Applications With Open MCT\n\n## Scope and purpose of this document\n\nThis document is intended to serve as a reference for developing an application\nbased on Open MCT. It will provide details of the API functions necessary to\nextend the Open MCT platform meet common use cases such as integrating with a telemetry source.\n\nThe best place to start is with the [Open MCT Tutorials](https://github.com/nasa/openmct-tutorial).\nThese will walk you through the process of getting up and running with Open\nMCT, as well as addressing some common developer use cases.\n\n## Building From Source\n\nThe latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct).\nIf you have `git`, and `node` installed, you can build Open MCT with the commands\n\n```bash\ngit clone https://github.com/nasa/openmct.git\ncd openmct\nnpm install\n```\n\nThese commands will fetch the Open MCT source from our GitHub repository, and\nbuild a minified version that can be included in your application. The output\nof the build process is placed in a `dist` folder under the openmct source\ndirectory, which can be copied out to another location as needed. The contents\nof this folder will include a minified javascript file named `openmct.js` as\nwell as assets such as html, css, and images necessary for the UI.\n\n## Starting an Open MCT application\n\n> [!WARNING]\n> Open MCT provides a development server via `webpack-dev-server` (`npm start`). **This should be used for development purposes only and should never be deployed to a production environment**.\n\nTo start a minimally functional Open MCT application, it is necessary to\ninclude the Open MCT distributable, enable some basic plugins, and bootstrap\nthe application. The tutorials walk through the process of getting Open MCT up\nand running from scratch, but provided below is a minimal HTML template that\nincludes Open MCT, installs some basic plugins, and bootstraps the application.\nIt assumes that Open MCT is installed under an `openmct` subdirectory, as\ndescribed in [Building From Source](#building-from-source).\n\nThis approach includes openmct using a simple script tag, resulting in a global\nvariable named `openmct`. This `openmct` object is used subsequently to make\nAPI calls.\n\nOpen MCT is packaged as a UMD (Universal Module Definition) module, so common\nscript loaders are also supported.\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <title>Open MCT</title>\n  <script src=\"dist/openmct.js\"></script>\n  <script>\n    openmct.install(openmct.plugins.LocalStorage());\n    openmct.install(openmct.plugins.MyItems());\n    openmct.install(openmct.plugins.UTCTimeSystem());\n    openmct.time.setTimeSystem('utc');\n    openmct.install(openmct.plugins.Espresso());\n\n    openmct.start();\n  </script>\n</head>\n<body>\n</body>\n</html>\n\n```\n\nCalling `openmct.start()` will start Open MCT and mount it into the \nspecified element once the DOM is ready. An element or a selector \nstring may be provided for this purposes. A selector string is \nsupported to obviate the need for boilerplate code to wait for the \nbody to load. If no argument is provided, Open MCT will create a \ndiv element as a child of the body, and bootstrap into it.\n\nThe Open MCT library included above requires certain assets such as html\ntemplates, images, and css. If you installed Open MCT from GitHub as described\nin the section on [Building from Source](#building-from-source) then these\nassets will have been downloaded along with the Open MCT javascript library.\n\nThere are some plugins bundled with the application that provide UI,\npersistence, and other default configuration which are necessary to be able to\ndo anything with the application initially. Any of these plugins can, in\nprinciple, be replaced with a custom plugin. The included plugins are\ndocumented in the [Included Plugins](#plugins) section.  \n\n## Types\n\nThe Open MCT library includes its own TypeScript declaration files which can be\nused to provide code hints and typechecking in your own Open MCT application.\n\nOpen MCT's type declarations are generated via `tsc` from JSDoc-style comment\nblocks. For more information on this, [check out TypeScript's documentation](https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html).\n\n### Using Types\n\nIn order to use Open MCT's provided types in your own application, create a\n`jsconfig.js` at the root of your project with this minimal configuration:\n\n```json\n{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n      \"target\": \"es6\",\n      \"checkJs\": true,\n      \"moduleResolution\": \"node\",\n      \"paths\": {\n        \"openmct\": [\"node_modules/openmct/dist/openmct.d.ts\"]\n      }\n  }\n}\n```\n\nThen, simply import and use `openmct` in your application:\n\n```js\nimport openmct from \"openmct\";\n```\n\n### Limitations\n\nThe effort to add types for Open MCT's public API is ongoing, and the provided\ntype declarations may be incomplete.\n\nIf you would like to contribute types to Open MCT, please check out\n[TypeScript's documentation](https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html) on generating type declarations from JSDoc-style comment blocks.\nThen read through our [contributing guide](https://github.com/nasa/openmct/blob/f7cf3f72c2efd46da7ce5719c5e52c8806d166f0/CONTRIBUTING.md) and open a PR!\n\n## Plugins\n\n### Defining and Installing a New Plugin\n\n```javascript\nopenmct.install(function install(openmctAPI) {\n    // Do things here\n    // ...\n});\n```\n\nNew plugins are installed in Open MCT by calling `openmct.install`, and\nproviding a plugin installation function. This function will be invoked on\napplication startup with one parameter - the openmct API object. A common\napproach used in the Open MCT codebase is to define a plugin as a function that\nreturns this installation function. This allows configuration to be specified\nwhen the plugin is included.\n\neg.\n\n```javascript\nopenmct.install(openmct.plugins.Elasticsearch(\"http://localhost:8002/openmct\"));\n```\n\nThis approach can be seen in all of the [plugins provided with Open MCT](https://github.com/nasa/openmct/blob/master/src/plugins/plugins.js).\n\n## Domain Objects and Identifiers\n\n_Domain Objects_ are the basic entities that represent domain knowledge in Open\nMCT.  The temperature sensor on a solar panel, an overlay plot comparing the\nresults of all temperature sensors, the command dictionary for a spacecraft,\nthe individual commands in that dictionary, the \"My Items\" folder: All of these\nthings are domain objects.\n\nA _Domain Object_ is simply a javascript object with some standard attributes.  \nAn example of a _Domain Object_ is the \"My Items\" object which is a folder in\nwhich a user can persist any objects that they create. The My Items object\nlooks like this:\n\n```javascript\n{\n    identifier: {\n        namespace: \"\"\n        key: \"mine\"\n    }\n    name:\"My Items\",\n    type:\"folder\",\n    location:\"ROOT\",\n    composition: []\n}\n```\n\n### Object Attributes\n\nThe main attributes to note are the `identifier`, and `type` attributes.\n\n- `identifier`: A composite key that provides a universally unique identifier\n  for this object. The `namespace` and `key` are used to identify the object.\n  The `key` must be unique within the namespace.\n- `type`: All objects in Open MCT have a type. Types allow you to form an\n  ontology of knowledge and provide an abstraction for grouping, visualizing,\n  and interpreting data. Details on how to define a new object type are\n  provided below.\n\nOpen MCT uses a number of builtin types. Typically you are going to want to\ndefine your own when extending Open MCT.\n\n### Domain Object Types\n\nCustom types may be registered via the `addType` function on the Open MCT Type\nregistry.\n\neg.\n\n```javascript\nopenmct.types.addType('example.my-type', {\n    name: \"My Type\",\n    description: \"This is a type that I added!\",\n    creatable: true\n});\n```\n\nThe `addType` function accepts two arguments:\n\n- A `string` key identifying the type. This key is used when specifying a type\nfor an object.  We recommend prefixing your types with a namespace to avoid\nconflicts with other plugins.\n- An object type specification. An object type definition supports the following\nattributes\n  - `name`: a `string` naming this object type\n  - `description`: a `string` specifying a longer-form description of this type\n  - `initialize`: a `function` which initializes the model for new domain objects\n    of this type. This can be used for setting default values on an object when\n    it is instantiated.\n  - `creatable`: A `boolean` indicating whether users should be allowed to create\n    this type (default: `false`). This will determine whether the type appears\n    in the `Create` menu.\n  - `cssClass`: A `string` specifying a CSS class to apply to each representation\n    of this object. This is used for specifying an icon to appear next to each\n    object of this type.\n\nThe [Open MCT Tutorials](https://github.com/nasa/openmct-tutorial) provide a\nstep-by-step examples of writing code for Open MCT that includes a [section on\ndefining a new object type](https://github.com/nasa/openmct-tutorial#step-3---providing-objects).\n\n## Root Objects\n\nIn many cases, you'd like a certain object (or a certain hierarchy of objects)\nto be accessible from the top level of the application (the tree on the left-hand\nside of Open MCT.) For example, it is typical to expose a telemetry dictionary\nas a hierarchy of telemetry-providing domain objects in this fashion.\n\nTo do so, use the `addRoot` method of the object API.\n\neg.\n\n```javascript\nopenmct.objects.addRoot({\n    namespace: \"example.namespace\",\n    key: \"my-key\"\n},\nopenmct.priority.HIGH);\n```\n\nThe `addRoot` function takes a two arguments, the first can be an [object identifier](#domain-objects-and-identifiers) for a root level object, or an array of identifiers for root\nlevel objects, or a function that returns a promise for an identifier or an array of root level objects, the second is a [priority](#priority-api) or numeric value.\n\nWhen using the `getAll` method of the object API, they will be returned in order of priority.\n\neg.\n\n```javascript\nopenmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree\nopenmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree\n```\n\nRoot objects are loaded just like any other objects, i.e. via an object provider.\n\n## Object Providers\n\nAn Object Provider is used to build _Domain Objects_, typically retrieved from\nsome source such as a persistence store or telemetry dictionary. In order to\nintegrate telemetry from a new source an object provider will need to be created\nthat can build objects representing telemetry points exposed by the telemetry\nsource. The API call to define a new object provider is fairly straightforward.\nHere's a very simple example:\n\n```javascript\nopenmct.objects.addProvider('example.namespace', {\n    get: function (identifier) {\n        return Promise.resolve({\n            identifier: identifier,\n            name: 'Example Object',\n            type: 'example-object-type'\n        });\n    }\n});\n```\n\nThe `addProvider` function takes two arguments:\n\n- `namespace`: A `string` representing the namespace that this object provider\nwill provide objects for.\n- `provider`: An `object` with a single function, `get`. This function accepts an\n[Identifier](#domain-objects-and-identifiers) for the object to be provided.\nIt is expected that the `get` function will return a\n[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\nthat resolves with the object being requested.\n\nIn future, object providers will support other methods to enable other operations with persistence stores, such as creating, updating, and deleting objects.\n\n## Composition Providers\n\nThe _composition_ of a domain object is the list of objects it contains, as\nshown (for example) in the tree for browsing. Open MCT provides a\n[default solution](#default-composition-provider) for composition, but there\nmay be cases where you want to provide the composition of a certain object\n(or type of object) dynamically.\n\n### Adding Composition Providers\n\nYou may want to populate a hierarchy under a custom root-level object based on\nthe contents of a telemetry dictionary. To do this, you can add a new\nComposition Provider:\n\n```javascript\nopenmct.composition.addProvider({\n    appliesTo: function (domainObject) {\n        return domainObject.type === 'example.my-type';\n    },\n    load: function (domainObject) {\n        return Promise.resolve(myDomainObjects);\n    }\n});\n```\n\nThe `addProvider` function accepts a Composition Provider object as its sole\nargument. A Composition Provider is a javascript object exposing two functions:\n\n- `appliesTo`: A `function` that accepts a `domainObject` argument, and returns\na `boolean` value indicating whether this composition provider applies to the\ngiven object.\n- `load`: A `function` that accepts a `domainObject` as an argument, and returns\na `Promise` that resolves with an array of [Identifier](#domain-objects-and-identifiers).\nThese identifiers will be used to fetch Domain Objects from an [Object Provider](#object-provider)\n\n### Default Composition Provider\n\nThe default composition provider applies to any domain object with a\n`composition` property. The value of `composition` should be an array of\nidentifiers, e.g.:\n\n```javascript\nvar domainObject = {\n    name: \"My Object\",\n    type: 'folder',\n    composition: [\n        {\n            id: '412229c3-922c-444b-8624-736d85516247',\n            namespace: 'foo'\n        },\n        {\n            key: 'd6e0ce02-5b85-4e55-8006-a8a505b64c75',\n            namespace: 'foo'\n        }\n    ]\n};\n```\n\n## Telemetry API\n\nThe Open MCT telemetry API provides two main sets of interfaces \n1. For integrating telemetry data into Open MCT, and \n2. For developing Open MCT visualization plugins utilizing the telemetry API.  \n\nThe APIs for integrating telemetry metadata into Open MCT are stable and documentation is included below. However, the APIs for visualization plugins are still a work in progress and docs may change at any time.\n\n### Integrating Telemetry Sources\n\nThere are two main tasks for integrating telemetry sources \n* Describing telemetry objects with relevant metadata. You'll use an [Object Provider](#object-providers) to provide objects with the necessary [Telemetry Metadata](#telemetry-metadata). Alternatively, you can register a telemetry metadata provider to provide the necessary telemetry metadata.\n* Providing telemetry data for those objects.  You'll register a [Telemetry Provider](#telemetry-providers) to retrieve telemetry data for those objects.\n\nFor a step-by-step guide to building a telemetry adapter, please see the\n[Open MCT Tutorials](https://github.com/nasa/openmct-tutorial).\n\n#### Telemetry Metadata\n\nA telemetry object is a domain object with a `telemetry` property.  To take an example from the tutorial, here is the telemetry object for the \"fuel\" measurement of the spacecraft:\n\n```json\n{\n    \"identifier\": {\n        \"namespace\": \"example.taxonomy\",\n        \"key\": \"prop.fuel\"\n    },\n    \"name\": \"Fuel\",\n    \"type\": \"example.telemetry\",\n    \"telemetry\": {\n        \"values\": [\n            {\n                \"key\": \"value\",\n                \"name\": \"Value\",\n                \"unit\": \"kilograms\",\n                \"format\": \"float\",\n                \"min\": 0,\n                \"max\": 100,\n                \"hints\": {\n                    \"range\": 1\n                }\n            },\n            {\n                \"key\": \"utc\",\n                \"source\": \"timestamp\",\n                \"name\": \"Timestamp\",\n                \"format\": \"utc\",\n                \"hints\": {\n                    \"domain\": 1\n                }\n            }\n        ]\n    }\n}\n```\n\nThe most important part of the telemetry metadata is the `values` property. This describes the attributes of telemetry datums (objects) that a telemetry provider returns.  These descriptions must be provided for telemetry views to work properly.\n\n##### Values\n\n`telemetry.values` is an array of value description objects, which have the following fields:\n\nattribute      | type    | flags    | notes\n---            |---------|----------| ---\n`key`          | string  | required | unique identifier for this field.  \n`hints`        | object  | required | Hints allow views to intelligently select relevant attributes for display, and are required for most views to function.  See section on \"Value Hints\" below.\n`name`         | string  | optional | a human readable label for this field.  If omitted, defaults to `key`.\n`source`       | string  | optional | identifies the property of a datum where this value is stored.  If omitted, defaults to `key`.\n`format`       | string  | optional | a specific format identifier, mapping to a formatter.  If omitted, uses a default formatter.  For enumerations, use `enum`.  For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format. For arrays use `number[]` or `string[]` See arrays below in the this table.  \n`unit`        | string  | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`\n`min`          | number  | optional | the minimum possible value of this measurement.  Will be used by plots, gauges, etc to automatically set a min value.\n`max`          | number  | optional | the maximum possible value of this measurement.  Will be used by plots, gauges, etc to automatically set a max value.\n`enumerations` | array   | optional | for objects where `format` is `\"enum\"`, this array tracks all possible enumerations of the value.  Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration.  ex: `{\"value\": 0, \"string\": \"OFF\"}`.  If you use an enumerations array, `min` and `max` will be set automatically for you.\n`arrays` | string  | optional | for objects where `format` is `\"number[]\" or \"string[]\"`. Will be used by plots, gauges, etc to automatically interpret values as arrays.\n\n###### Value Hints\n\nEach telemetry value description has an object defining hints.  Keys in this object represent the hint itself, and the value represents the weight of that hint.  A lower weight means the hint has a higher priority.  For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice.  Likewise, a table will use hints to determine the default order of columns.\n\nKnown hints:\n\n- `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.\n- `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.\n- `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.\n- `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file.\n\n##### The Time Conductor and Telemetry\n\nOpen MCT provides a number of ways to pivot through data and link data via time.  The Time Conductor helps synchronize multiple views around the same time.\n\nIn order for the time conductor to work, there will always be an active \"time system\".  All telemetry metadata _must_ have a telemetry value with a `key` that matches the `key` of the active time system.  You can use the `source` attribute on the value metadata to remap this to a different field in the telemetry datum-- especially useful if you are working with disparate datasources that have different field mappings.\n\n#### Telemetry Providers\n\nTelemetry providers are responsible for providing historical and real-time telemetry data for telemetry objects.  Each telemetry provider determines which objects it can provide telemetry for, and then must implement methods to provide telemetry for those objects.\n\nA telemetry provider is a javascript object with up to four methods:\n\n- `supportsSubscribe(domainObject, callback, options)` optional.  Must be implemented to provide realtime telemetry.  Should return `true` if the provider supports subscriptions for the given domain object (and request options).\n- `subscribe(domainObject, callback, options)` required if `supportsSubscribe` is implemented.  Establish a subscription for realtime data for the given domain object.  Should invoke `callback` with a single telemetry datum every time data is received.  Must return an unsubscribe function.  Multiple views can subscribe to the same telemetry object, so it should always return a new unsubscribe function.\n- `supportsRequest(domainObject, options)` optional.  Must be implemented to provide historical telemetry.  Should return `true` if the provider supports historical requests for the given domain object.\n- `request(domainObject, options)` required if `supportsRequest` is implemented.  Must return a promise for an array of telemetry datums that fulfills the request.  The `options` argument will include a `start`, `end`, and `domain` attribute representing the query bounds.  See [Telemetry Requests and Responses](#telemetry-requests-and-responses) for more info on how to respond to requests.\n- `supportsMetadata(domainObject)` optional.  Implement and return `true` for objects that you want to provide dynamic metadata for.\n- `getMetadata(domainObject)` required if `supportsMetadata` is implemented.  Must return a valid telemetry metadata definition that includes at least one valueMetadata definition.\n- `supportsLimits(domainObject)` optional.  Implement and return `true` for domain objects that you want to provide a limit evaluator for.\n- `getLimitEvaluator(domainObject)` required if `supportsLimits` is implemented.  Must return a valid LimitEvaluator for a given domain object.\n\nTelemetry providers are registered by calling `openmct.telemetry.addProvider(provider)`, e.g.\n\n```javascript\nopenmct.telemetry.addProvider({\n    supportsRequest: function (domainObject, options) { /*...*/ },\n    request: function (domainObject, options) { /*...*/ },\n})\n```\n\nNote: it is not required to implement all of the methods on every provider.  Depending on the complexity of your implementation, it may be helpful to instantiate and register your realtime, historical, and metadata providers separately.\n\n#### Telemetry Requests and Responses\n\nTelemetry requests support time bounded queries. A call to a _Telemetry Provider_'s `request` function will include an `options` argument. These are simply javascript objects with attributes for the request parameters. An example of a telemetry request object with a start and end time is included below:\n\n```javascript\n{\n    start: 1487981997240,\n    end: 1487982897240,\n    domain: 'utc'\n}\n```\n\nIn this case, the `domain` is the currently selected time-system, and the start and end dates are valid dates in that time system.\n\nA telemetry provider's `request` method should return a promise for an array of telemetry datums.  These datums must be sorted by `domain` in ascending order.\n\nThe telemetry provider's `request` method will also return an object `signal` with an `aborted` property with a value `true` if the request has been aborted by user navigation. This can be used to trigger actions when a request has been aborted.\n\n#### Request Strategies **draft**\n\nTo improve performance views may request a certain strategy for data reduction.  These are intended to improve visualization performance by reducing the amount of data needed to be sent to the client.  These strategies will be indicated by additional parameters in the request options.  You may choose to handle them or ignore them.  \n\nNote: these strategies are currently being tested in core plugins and may change based on developer feedback.\n\n##### `latest` request strategy\n\nThis request is a \"depth based\" strategy.  When a view is only capable of\ndisplaying a single value (or perhaps the last ten values), then it can\nuse the `latest` request strategy with a `size` parameter that specifies\nthe number of results it desires.  The `size` parameter is a hint; views\nmust not assume the response will have the exact number of results requested.\n\nexample:\n\n```javascript\n{\n    start: 1487981997240,\n    end: 1487982897240,\n    domain: 'utc',\n    strategy: 'latest',\n    size: 1\n}\n```\n\nThis strategy says \"I want the latest data point in this time range\".  A provider which recognizes this request should return only one value-- the latest-- in the requested time range.  Depending on your back-end implementation, performing these queries in bulk can be a large performance increase.  These are generally issued by views that are only capable of displaying a single value and only need to show the latest value.\n\n##### `minmax` request strategy\n\nexample:\n\n```javascript\n{\n    start: 1487981997240,\n    end: 1487982897240,\n    domain: 'utc',\n    strategy: 'minmax',\n    size: 720\n}\n```\n\nMinMax queries are issued by plots, and may be issued by other types as well.  The aim is to reduce the amount of data returned but still faithfully represent the full extent of the data.  In order to do this, the view calculates the maximum data resolution it can display (i.e. the number of horizontal pixels in a plot) and sends that as the `size`.  The response should include at least one minimum and one maximum value per point of resolution.\n\n#### Telemetry Formats\n\nTelemetry format objects define how to interpret and display telemetry data.\nThey have a simple structure, provided here as a TypeScript interface:\n\n```ts\ninterface Formatter {\n    key: string; // A string that uniquely identifies this formatter.\n\n    format: (\n        value: any, // The raw telemetry value in its native type.\n        minValue?: number, // An optional argument specifying the minimum displayed value.\n        maxValue?: number, // An optional argument specifying the maximum displayed value.\n        count?: number // An optional argument specifying the number of displayed values.\n    ) => string; // Returns a human-readable string representation of the provided value.\n\n    parse: (\n        value: string | any // A string representation of a telemetry value or an already-parsed value.\n    ) => any; // Returns the value in its native type. This function should be idempotent.\n\n    validate: (value: string) => boolean; // Takes a string representation of a telemetry value and returns a boolean indicating whether the provided string can be parsed.\n}\n```\n\n##### Built-in Formats\n\nOpen MCT on its own defines a handful of built-in formats:\n\n###### **Number Format (default):**\n\nApplied to data with `format: 'number'`\n\n```js\nvalueMetadata = {\n    format: 'number'\n    // ...\n};\n```\n\n```ts\ninterface NumberFormatter extends Formatter {\n    parse: (x: any) => number;\n    format: (x: number) => string;\n    validate: (value: any) => boolean;\n}\n```\n\n###### **String Format**\n\nApplied to data with `format: 'string'`\n\n```js\nvalueMetadata = {\n    format: 'string'\n    // ...\n};\n```\n\n```ts\ninterface StringFormatter extends Formatter {\n    parse: (value: any) => string;\n    format: (value: string) => string;\n    validate: (value: any) => boolean;\n}\n```\n\n###### **Enum Format**\n\nApplied to data with `format: 'enum'`\n\n```js\nvalueMetadata = {\n    format: 'enum',\n    enumerations: [\n    {\n        value: 1,\n        string: 'APPLE'\n    }, \n    {\n        value: 2,\n        string: 'PEAR',\n    },\n    {\n        value: 3,\n        string: 'ORANGE'\n    }]\n    // ...\n};\n```\n\nCreates a two-way mapping between enum string and value to be used in the `parse` and `format` methods.\nEx:\n\n- `formatter.parse('APPLE') === 1;`\n- `formatter.format(1) === 'APPLE';`\n\n```ts\ninterface EnumFormatter extends Formatter {\n    parse: (value: string) => string;\n    format: (value: number) => string;\n    validate: (value: any) => boolean;\n}\n```\n##### Time Formats\n\nTime formatters are used to format and parse datetime values. See as an example the UTC time formatter provided in src/plugins/utcTimeSystem/UTCTimeFormat.js.\n\nIf a formatDate method is provided, it will be used in conjunction with a duration formatter to provide split date and time inputs for the time conductor.\n\n```ts\ninterface TimeFormatter extends Formatter {\n    parse: (value: string) => number;\n    format: (value: number) => string;\n    formatDate?: (value: number) => string;\n    validate: (value: any) => boolean;\n}\n```\n\n##### Registering Formats\n\nFormats implement the following interface (provided here as TypeScript for simplicity):\n\nFormats are registered with the Telemetry API using the `addFormat` function. eg.\n\n```javascript\nopenmct.telemetry.addFormat({\n    key: 'number-to-string',\n    format: function (number) {\n        return number + '';\n    },\n    parse: function (text) {\n        return Number(text);\n    },\n    validate: function (text) {\n        return !isNaN(text);\n    }\n});\n```\n\n#### Telemetry Data\n\nA single telemetry point is considered a Datum, and is represented by a standard\njavascript object.  Realtime subscriptions (obtained via **subscribe**) will\ninvoke the supplied callback once for each telemetry datum received.  Telemetry\nrequests (obtained via **request**) will return a promise for an array of\ntelemetry datums.\n\n##### Telemetry Datums\n\nA telemetry datum is a simple javascript object, e.g.:\n\n```json\n{\n    \"timestamp\": 1491267051538,\n    \"value\": 77,\n    \"id\": \"prop.fuel\"\n}\n```\n\nThe key-value pairs of this object are described by the telemetry metadata of\na domain object, as discussed in the [Telemetry Metadata](#telemetry-metadata)\nsection.\n\n#### Limit Evaluators **draft**\n\nLimit evaluators allow a telemetry integrator to define which limits exist for a\ntelemetry endpoint and how limits should be applied to telemetry from a given domain object.\n\nA limit evaluator can implement the `evaluate` method which is used to define how limits\nshould be applied to telemetry and the `getLimits` method which is used to specify\nwhat the limit values are for different limit levels.\n\nLimit levels can be mapped to one of 5 colors for visualization:\n`purple`, `red`, `orange`, `yellow` and `cyan`.\n\nFor an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.\n\n### Telemetry Consumer APIs **draft**\n\nThe APIs for requesting telemetry from Open MCT -- e.g. for use in custom views -- are currently in draft state and are being revised.  If you'd like to experiment with them before they are finalized, please contact the team via the contact-us link on our website.\n\n## Time API\n\nOpen MCT provides API for managing the temporal state of the application.\nCentral to this is the concept of \"time bounds\". Views in Open MCT will\ntypically show telemetry data for some prescribed date range, and the Time API\nprovides a way to centrally manage these bounds.\n\nThe Time API exposes a number of methods for querying and setting the temporal\nstate of the application, and emits events to inform listeners when the state changes.\n\nBecause the data displayed tends to be time domain data, Open MCT must always\nhave at least one time system installed and activated. When you download Open\nMCT, it will be pre-configured to use the UTC time system, which is installed and activated,\nalong with other default plugins, in `index.html`. Installing and activating a time system\nis simple, and is covered [in the next section](#defining-and-registering-time-systems).\n\n### Time Systems and Bounds\n\n#### Defining and Registering Time Systems\n\nThe time bounds of an Open MCT application are defined as numbers, and a Time\nSystem gives meaning and context to these numbers so that they can be correctly\ninterpreted. Time Systems are JavaScript objects that provide some information\nabout the current time reference frame. An example of defining and registering\na new time system is given below:\n\n``` javascript\nopenmct.time.addTimeSystem({\n    key: 'utc',\n    name: 'UTC Time',\n    cssClass = 'icon-clock',\n    timeFormat = 'utc',\n    durationFormat = 'duration',\n    isUTCBased = true\n});\n```\n\nThe example above defines a new utc based time system. In fact, this time system\nis configured and activated by default from `index.html` in the default\ninstallation of Open MCT if you download the source from GitHub. Some details of\neach of the required properties is provided below.\n\n- `key`: A `string` that uniquely identifies this time system.\n- `name`: A `string` providing a brief human readable label. If the [Time Conductor](#the-time-conductor)\nplugin is enabled, this name will identify the time system in a dropdown menu.\n- `cssClass`: A class name `string` that will be applied to the time system when\nit appears in the UI. This will be used to represent the time system with an icon.\nThere are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss),\nor a custom class can be used here.\n- `timeFormat`: A `string` corresponding to the key of a registered\n[telemetry time format](#telemetry-formats). The format will be used for\ndisplaying discrete timestamps from telemetry streams when this time system is\nactivated. If the [UTCTimeSystem](#included-plugins) is enabled, then the `utc`\nformat can be used if this is a utc-based time system\n- `durationFormat`: A `string` corresponding to the key of a registered\n[telemetry time format](#telemetry-formats). The format will be used for\ndisplaying time ranges, for example `00:15:00` might be used to represent a time\nperiod of fifteen minutes. These are used by the Time Conductor plugin to specify\nrelative time offsets. If the [UTCTimeSystem](#included-plugins) is enabled,\nthen the `duration` format can be used if this is a utc-based time system\n- `isUTCBased`: A `boolean` that defines whether this time system represents\nnumbers in UTC terrestrial time.\n\n#### Getting and Setting the Active Time System\n\nOnce registered, a time system can be activated by calling `setTimeSystem` with\nthe timeSystem `key` or an instance of the time system.  You can also specify\nvalid [bounds](#time-bounds) for the timeSystem.\n\n```javascript\nopenmct.time.setTimeSystem('utc', bounds);\n```\n\nThe current time system can be retrieved as well by calling `getTimeSystem`.\n\n```javascript\nopenmct.time.getTimeSystem();\n```\n\nA time system can be immediately activated after registration:\n\n```javascript\nopenmct.time.addTimeSystem(utcTimeSystem);\nopenmct.time.setTimeSystem(utcTimeSystem, bounds);\n```\n\nSetting the active time system will trigger a [`'timeSystemChanged'`](#time-events)\nevent.  If you supplied bounds, a [`'boundsChanged'`](#time-events) event will be triggered afterwards with your newly supplied bounds.\n\n> ⚠️ **Deprecated**\n>\n> - The method `timeSystem()` is deprecated. Please use `getTimeSystem()` and `setTimeSystem()` as a replacement.\n\n#### Time Bounds\n\nThe TimeAPI provides a getter and setter for querying and setting time bounds. Time\nbounds are simply an object with a `start` and an end `end` attribute.\n\n- `start`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).\nThis will be used as the beginning of the time period displayed by time-responsive\ntelemetry views.\n- `end`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).\nThis will be used as the end of the time period displayed by time-responsive\ntelemetry views.\n\nNew bounds can be set system wide by calling `setBounds` with [bounds](#time-bounds).\n\n``` javascript\nconst ONE_HOUR = 60 * 60 * 1000;\nlet now = Date.now();\nopenmct.time.setBounds({start: now - ONE_HOUR, now);\n```\n\nCalling `getBounds` will return the current application-wide time bounds.\n\n``` javascript\nopenmct.time.getBounds();\n```\n\nTo respond to bounds change events, listen for the [`'boundsChanged'`](#time-events)\nevent.\n\n> ⚠️ **Deprecated**\n>\n> - The method `bounds()` is deprecated and will be removed in a future release. Please use `getBounds()` and `setBounds()` as a replacement.\n\n### Clocks\n\nThe Time API requires a clock source which will cause the bounds to be updated\nautomatically whenever the clock source \"ticks\". A clock is simply an object that\nsupports registration of listeners and periodically invokes its listeners with a\nnumber. Open MCT supports registration of new clock sources that tick on almost\nanything. A tick occurs when the clock invokes callback functions registered by its\nlisteners with a new time value.\n\nAn example of a clock source is the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)\nwhich emits the current time in UTC every 100ms. Clocks can tick on anything. For\nexample, a clock could be defined to provide the timestamp of any new data\nreceived via a telemetry subscription. This would have the effect of advancing\nthe bounds of views automatically whenever data is received. A clock could also\nbe defined to tick on some remote timing source.\n\nThe values provided by clocks are simple `number`s, which are interpreted in the\ncontext of the active [Time System](#defining-and-registering-time-systems).\n\n#### Defining and registering clocks\n\nA clock is an object that defines certain required metadata and functions:\n\n- `key`: A `string` uniquely identifying this clock. This can be used later to\nreference the clock in places such as the [Time Conductor configuration](#time-conductor-configuration)\n- `cssClass`: A `string` identifying a CSS class to apply to this clock when it's\ndisplayed in the UI. This will be used to represent the time system with an icon.\nThere are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss),\nor a custom class can be used here.\n- `name`: A `string` providing a human-readable identifier for the clock source.\nThis will be displayed in the clock selector menu in the Time Conductor UI\ncomponent, if active.\n- `description`: An **optional** `string` providing a longer description of the\nclock. The description will be visible in the clock selection menu in the Time\nConductor plugin.\n- `on`: A `function` supporting registration of a new callback that will be\ninvoked when the clock next ticks. It will be invoked with two arguments:\n  - `eventName`: A `string` specifying the event to listen on. For now, clocks\n    support one event - `tick`.\n  - `callback`: A `function` that will be invoked when this clock ticks. The\n    function must be invoked with one parameter - a `number` representing a valid\n    time in the current time system.\n- `off`: A `function` that allows deregistration of a tick listener. It accepts\nthe same arguments as `on`.\n- `currentValue`: A `function` that returns a `number` representing a point in\ntime in the active time system. It should be the last value provided by a tick,\nor some default value if no ticking has yet occurred.\n\nA new clock can be registered using the `addClock` function exposed by the Time\nAPI:\n\n```javascript\nvar someClock = {\n    key: 'someClock',\n    cssClass: 'icon-clock',\n    name: 'Some clock',\n    description: \"Presumably does something useful\",\n    on: function (event, callback) {\n        // Some function that registers listeners, and updates them on a tick\n    },\n    off: function (event, callback) {\n        // Some function that unregisters listeners.\n    },\n    currentValue: function () {\n        // A function that returns the last ticked value for the clock\n    }\n}\n\nopenmct.time.addClock(someClock);\n```\n\nAn example clock implementation is provided in the form of the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)\n\n#### Getting and setting active clock\n\nOnce registered a clock can be activated by calling the `setClock` function on the\nTime API passing in the key or instance of a registered clock. Only one clock\nmay be active at once, so activating a clock will deactivate any currently\nactive clock and start the new clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.\n\nSetting the clock triggers a [`'clockChanged'`](#time-events) event, followed by a [`'clockOffsetsChanged'`](#time-events) event, and then a [`'boundsChanged'`](#time-events) event as the offsets are applied to the clock's currentValue().\n\n```\nopenmct.time.setClock(someClock, clockOffsets);\n```\n\nUpon being activated, the time API will listen for tick events on the clock by calling `clock.on`.\n\nThe currently active clock can be retrieved by calling `getClock`.\n\n```\nopenmct.time.getClock();\n```\n\n> ⚠️ **Deprecated**\n>\n> - The method `clock()` is deprecated and will be removed in a future release. Please use `getClock()` and `setClock()` as a replacement.\n\n#### ⚠️ [DEPRECATED] Stopping an active clock\n\n_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._\n\nThe `stopClock` method can be used to stop an active clock, and to clear it. It\nwill stop the clock from ticking, and set the active clock to `undefined`.\n\n``` javascript\nopenmct.time.stopClock();\n```\n\n> ⚠️ **Deprecated**\n>\n> - The method `stopClock()` is deprecated and will be removed in a future release.\n\n#### Clock Offsets\n\nWhen in Real-time [mode](#time-modes), the time bounds of the application will be updated automatically each time the\nclock \"ticks\". The bounds are calculated based on the current value provided by\nthe active clock (via its `tick` event, or its `currentValue()` method).\n\nUnlike bounds, which represent absolute time values, clock offsets represent\nrelative time spans. Offsets are defined as an object with two properties:\n\n- `start`: A `number` that must be < 0 and which is used to calculate the start\nbounds on each clock tick. The start offset will be calculated relative to the\nvalue provided by a clock's tick callback, or its `currentValue()` function.\n- `end`: A `number` that must be >= 0 and which is used to calculate the end\nbounds on each clock tick.\n\nThe `setClockOffsets` function can be used to get or set clock offsets. For example,\nto show the last fifteen minutes in a ms-based time system:\n\n```javascript\nvar FIFTEEN_MINUTES = 15 * 60 * 1000;\n\nopenmct.time.setClockOffsets({\n    start: -FIFTEEN_MINUTES,\n    end: 0\n})\n```\n\nThe `getClockOffsets` method will return the currently set clock offsets.\n\n```javascript\nopenmct.time.getClockOffsets()\n```\n\n**Note:** Setting the clock offsets will trigger an immediate bounds change, as\nnew bounds will be calculated based on the `currentValue()` of the active clock.\nClock offsets are only relevant when in Real-time [mode](#time-modes).\n\n> ⚠️ **Deprecated**\n>\n> - The method `clockOffsets()` is deprecated and will be removed in a future release. Please use `getClockOffsets()` and `setClockOffsets()` as a replacement.\n\n### Time Modes\n\nThere are two time modes in Open MCT, \"Fixed\" and \"Real-time\". In Real-time mode the\ntime bounds of the application will be updated automatically each time the clock \"ticks\".\nThe bounds are calculated based on the current value provided by the active clock. In\nFixed mode, the time bounds are set for a specified time range. When Open MCT is first\ninitialized, it will be in Real-time mode.\n\nThe `setMode` method can be used to set the current time mode. It accepts a mode argument,\n`'realtime'` or `'fixed'` and it also accepts an optional [offsets](#clock-offsets)/[bounds](#time-bounds) argument dependent\non the current mode.\n\n``` javascript\nopenmct.time.setMode('fixed');\nopenmct.time.setMode('fixed', bounds); // with optional bounds\n```\n\nor\n\n``` javascript\nopenmct.time.setMode('realtime');\nopenmct.time.setMode('realtime', offsets); // with optional offsets\n```\n\nThe `getMode` method will return the current time mode, either `'realtime'` or `'fixed'`.\n\n``` javascript\nopenmct.time.getMode();\n```\n\n#### Time Mode Helper Methods\n\nThere are two methods available to determine the current time mode in Open MCT programmatically,\n`isRealTime` and `isFixed`. Each one will return a boolean value based on the current mode.\n\n``` javascript\nif (openmct.time.isRealTime()) {\n  // do real-time stuff\n}\n```\n\n``` javascript\nif (openmct.time.isFixed()) {\n  // do fixed-time stuff\n}\n```\n\n### Time Events\n\nThe Time API is a standard event emitter; you can register callbacks for events using the `on` method and remove callbacks for events with the `off` method.\n\nFor example:\n\n``` javascript\nopenmct.time.on('boundsChanged', function callback (newBounds, tick) {\n    // Do something with new bounds\n});\n```\n\n#### List of Time Events\n\nThe events emitted by the Time API are:\n\n- `boundsChanged`: emitted whenever the bounds change.  The callback will be invoked\n  with two arguments:\n  - `bounds`: A [bounds](#getting-and-setting-bounds) bounds object\n    representing a new time period bound by the specified start and send times.\n  - `tick`: A `boolean` indicating whether or not this bounds change is due to\n    a \"tick\" from a [clock source](#clocks). This information can be useful\n    when determining a strategy for fetching telemetry data in response to a\n    bounds change event. For example, if the bounds change was automatic, and\n    is due to a tick then it's unlikely that you would need to perform a\n    historical data query. It should be sufficient to just show any new\n    telemetry received via subscription since the last tick, and optionally to\n    discard any older data that now falls outside of the currently set bounds.\n    If `tick` is false,then the bounds change was not due to an automatic tick,\n    and a query for historical data may be necessary, depending on your data\n    caching strategy, and how significantly the start bound has changed.\n- `timeSystemChanged`: emitted whenever the active time system changes.  The callback will be invoked with a single argument:\n  - `timeSystem`: The newly active [time system](#defining-and-registering-time-systems).\n- `clockChanged`: emitted whenever the clock changes.  The callback will be invoked\n  with a single argument:\n  - `clock`: The newly active [clock](#clocks), or `undefined` if an active\n    clock has been deactivated.\n- `clockOffsetsChanged`: emitted whenever the active clock offsets change.  The\n  callback will be invoked with a single argument:\n  - `clockOffsets`: The new [clock offsets](#clock-offsets).\n- `modeChanged`: emitted whenever the time [mode](#time-modes) changed. The callback will\n  be invoked with one argument:\n  - `mode`: A string representation of the current time mode, either `'realtime'` or `'fixed'`.\n\n> ⚠️ **Deprecated Events** (These will be removed in a future release):\n>\n> - `bounds` → `boundsChanged`\n> - `timeSystem` → `timeSystemChanged`\n> - `clock` → `clockChanged`\n> - `clockOffsets` → `clockOffsetsChanged`\n\n### The Time Conductor\n\nThe Time Conductor provides a user interface for managing time bounds in Open\nMCT. It allows a user to select from configured time systems and clocks, and to set bounds and clock offsets.\n\nIf activated, the time conductor must be provided with configuration options,\ndetailed below.\n\n#### Time Conductor Configuration\n\nThe time conductor is configured by specifying the options that will be\navailable to the user from the menus in the time conductor. These will determine\nthe clocks available from the conductor, the time systems available for each\nclock, and some default bounds and clock offsets for each combination of clock\nand time system. By default, the conductor always supports a `fixed` mode where\nno clock is active. To specify configuration for fixed mode, simply leave out a\n`clock` attribute in the configuration entry object.\n\nConfiguration is provided as an `array` of menu options. Each entry of the\narray is an object with some properties specifying configuration. The configuration\noptions specified are slightly different depending on whether or not it is for\nan active clock mode.\n\n**Configuration for Fixed Time Mode (no active clock)**\n\n- `timeSystem`: A `string`, the key for the time system that this configuration\nrelates to.\n- `bounds`: A [`Time Bounds`](#time-bounds) object. These bounds will be applied\nwhen the user selects the time system specified in the previous `timeSystem`\nproperty.\n- `zoomOutLimit`: An **optional** `number` specifying the longest period of time\nthat can be represented by the conductor when zooming. If a `zoomOutLimit` is\nprovided, then a `zoomInLimit` must also be provided. If provided, the zoom\nslider will automatically become available in the Time Conductor UI.\n- `zoomInLimit`: An **optional** `number` specifying the shortest period of time\nthat can be represented by the conductor when zooming. If a `zoomInLimit` is\nprovided, then a `zoomOutLimit` must also be provided. If provided, the zoom\nslider will automatically become available in the Time Conductor UI.\n\n**Configuration for Active Clock**\n\n- `clock`: A `string`, the `key` of the clock that this configuration applies to.\n- `timeSystem`: A `string`, the key for the time system that this configuration\nrelates to. Separate configuration must be provided for each time system that you\nwish to be available to users when they select the specified clock.\n- `clockOffsets`: A [`clockOffsets`](#clock-offsets) object that will be\nautomatically applied when the combination of clock and time system specified in\nthis configuration is selected from the UI.\n\n#### Example conductor configuration\n\nAn example time conductor configuration is provided below. It sets up some\ndefault options for the [UTCTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/UTCTimeSystem.js)\nand [LocalTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/localTimeSystem/LocalTimeSystem.js),\nin both fixed mode, and for the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)\nsource. In this configuration, the local clock supports both the UTCTimeSystem\nand LocalTimeSystem. Configuration for fixed bounds mode is specified by omitting\na clock key.\n\n``` javascript\nconst ONE_YEAR = 365 * 24 * 60 * 60 * 1000;\nconst ONE_MINUTE = 60 * 1000;\n\nopenmct.install(openmct.plugins.Conductor({\n    menuOptions: [\n        // 'Fixed' bounds mode configuration for the UTCTimeSystem\n        {\n            timeSystem: 'utc',\n            bounds: {start: Date.now() - 30 * ONE_MINUTE, end: Date.now()},\n            zoomOutLimit: ONE_YEAR,\n            zoomInLimit: ONE_MINUTE\n        },\n        // Configuration for the LocalClock in the UTC time system\n        {\n            clock: 'local',\n            timeSystem: 'utc',\n            clockOffsets: {start: - 30 * ONE_MINUTE, end: 0},\n            zoomOutLimit: ONE_YEAR,\n            zoomInLimit: ONE_MINUTE\n        },\n        //Configuration for the LocaLClock in the Local time system\n        {\n            clock: 'local',\n            timeSystem: 'local',\n            clockOffsets: {start: - 15 * ONE_MINUTE, end: 0}\n        }\n    ]\n}));\n```\n\n## Indicators\n\nIndicators are small widgets that reside at the bottom of the screen and are visible from\nevery screen in Open MCT. They can be used to convey system state using an icon and text.\nTypically an indicator will display an icon (customizable with a CSS class) that will\nreveal additional information when the mouse cursor is hovered over it.\n\n### The URL Status Indicator\n\nA common use case for indicators is to convey the state of some external system such as a\npersistence backend or HTTP server. So long as this system is accessible via HTTP request,\nOpen MCT provides a general purpose indicator to show whether the server is available and\nreturning a 2xx status code. The URL Status Indicator is made available as a default plugin. See\nthe [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the\nURL Status Indicator.\n\n### Creating a Simple Indicator\n\nA simple indicator with an icon and some text can be created and added with minimal code. An indicator\nof this type exposes functions for customizing the text, icon, and style of the indicator.\n\neg.\n\n``` javascript\nvar myIndicator = openmct.indicators.simpleIndicator();\nmyIndicator.text(\"Hello World!\");\nopenmct.indicators.add(myIndicator);\n```\n\nThis will create a new indicator and add it to the bottom of the screen in Open MCT.\nBy default, the indicator will appear as an information icon. Hovering over the icon will\nreveal the text set via the call to `.text()`. The Indicator object returned by the API\ncall exposes a number of functions for customizing the content and appearance of the indicator:\n\n- `.text([text])`: Gets or sets the text shown when the user hovers over the indicator.\nAccepts an **optional** `string` argument that, if provided, will be used to set the text.\nHovering over the indicator will expand it to its full size, revealing this text alongside\nthe icon. Returns the currently set text as a `string`.\n- `.description([description])`: Gets or sets the indicator's description. Accepts an\n**optional** `string` argument that, if provided, will be used to set the text. The description\nallows for more detail to be provided in a tooltip when the user hovers over the indicator.\nReturns the currently set text as a `string`.\n- `.iconClass([className])`: Gets or sets the CSS class used to define the icon. Accepts an **optional**\n`string` parameter to be used to set the class applied to the indicator. Any of\n[the built-in glyphs](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home/glyphs?view=styleguide.glyphs)\nmay be used here, or a custom CSS class can be provided. Returns the currently defined CSS\nclass as a `string`.\n- `.statusClass([className])`: Gets or sets the CSS class used to determine status. Accepts an **optional**\n`string` parameter to be used to set a status class applied to the indicator. May be used to apply\ndifferent colors to indicate status.\n\n### Custom Indicators\n\nA completely custom indicator can be added by simply providing a DOM element to place alongside other indicators.\n\n``` javascript\n    var domNode = document.createElement('div');\n    domNode.innerText = new Date().toString();\n    setInterval(function () {\n        domNode.innerText = new Date().toString();\n    }, 1000);\n\n    openmct.indicators.add({\n        element: domNode\n    });\n```\n\n## Priority API\n\nOpen MCT provides some built-in priority values that can be used in the application for view providers, indicators, root object order, and more.\n\n### Priority Types\n\nCurrently, the Open MCT Priority API provides (type: numeric value):\n\n- HIGHEST: Infinity\n- HIGH: 1000\n- Default: 0\n- LOW: -1000\n- LOWEST: -Infinity\n\nView provider Example:\n\n``` javascript\n  class ViewProvider {\n    ...\n    priority() {\n        return openmct.priority.HIGH;\n    }\n}\n```\n\n## User API\n\nOpen MCT provides a User API which can be used to define providers for user information. The API\ncan be used to manage user information and roles.\n\n### Example\n\nOpen MCT provides an example [user](example/exampleUser/exampleUserCreator.js) and [user provider](example/exampleUser/ExampleUserProvider.js) which\ncan be used as a starting point for creating a custom user provider.\n\n## Visibility-Based Rendering in View Providers\n\nTo enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout).\n\n### Overview\n\nThe show function is responsible for the rendering of a view. An [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) is used internally to determine whether the view is visible. This observer drives the visibility-based rendering feature, accessed via the `renderWhenVisible` function provided in the `viewOptions` parameter.\n\n### Implementing Visibility-Based Rendering\n\nThe `renderWhenVisible` function is passed to the show function as part of the `viewOptions` object. This function can be used for all rendering logic that would otherwise be executed within a `requestAnimationFrame` call. When called, `renderWhenVisible` will either execute the provided function immediately (via `requestAnimationFrame`) if the view is currently visible, or defer its execution until the view becomes visible.\n\nAdditionally, `renderWhenVisible` returns a boolean value indicating whether the provided function was executed immediately (`true`) or deferred (`false`).\n\nMonitoring of visibility begins after the first call to `renderWhenVisible` is made.\n\nHere’s the signature for the show function:\n\n`show(element, isEditing, viewOptions)`\n\n- `element` (HTMLElement) - The DOM element where the view should be rendered.\n- `isEditing` (boolean) - Indicates whether the view is in editing mode.\n- `viewOptions` (Object) - An object with configuration options for the view, including:\n  - `renderWhenVisible` (Function) - This function wraps the `requestAnimationFrame` and only triggers the provided render logic when the view is visible in the viewport.\n\n### Example\n\nAn OpenMCT view provider might implement the show function as follows:\n\n```js\n// Define your view provider\nconst myViewProvider = {\n  // ... other properties and methods ...\n  show: function (element, isEditing, viewOptions) {\n    // Callback for rendering view content\n    const renderCallback = () => {\n      // Your view rendering logic goes here\n    };\n    \n    // Use the renderWhenVisible function to ensure rendering only happens when view is visible\n    const wasRenderedImmediately = viewOptions.renderWhenVisible(renderCallback);\n\n    // Optionally handle the immediate rendering return value\n    if (wasRenderedImmediately) {\n      console.debug('🪞 Rendering triggered immediately as the view is visible.');\n    } else {\n      console.debug('🛑 Rendering has been deferred until the view becomes visible.');\n    }\n  }\n};\n```\n\nNote that `renderWhenVisible` defers rendering while the view is not visible and caters to the latest execution call. This provides responsiveness for dynamic content while ensuring performance optimizations.\n\nEnsure your view logic is prepared to handle potentially multiple deferrals if using this API, as only the last call to renderWhenVisible will be queued for execution upon the view becoming visible.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Open MCT\n\nThis document describes the process of contributing to Open MCT as well as the standards that will be applied when evaluating contributions.\n\nIn order for external contributions to be merged, contributors must have on record a signed [Contributor License Agreement (CLA)](https://nasa.github.io/openmct/static/files/ind-cla-open-mct.pdf). More information on this process can be found [in this discussion](https://github.com/nasa/openmct/discussions/3821).\n\n## Summary\n\nThe short version:\n\n1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).\n2. Make sure your contribution meets code, test, and commit message standards as described below.\n3. Submit a pull request from a topic branch back to `master`. Include a check list, as described below. (Optionally, assign this to a specific member for review.)\n4. Respond to any discussion. When the reviewer decides it's ready, they will merge back `master` and fill out their own check list.\n5. If you are a first-time contributor, please see [this discussion](https://github.com/nasa/openmct/discussions/3821) for further information.\n\n## Contribution Process\n\nOpen MCT uses git for software version control, and for branching and merging. The central repository is at <https://github.com/nasa/openmct.git>.\n\n### Roles\n\nReferences to roles are made throughout this document. These are not intended to reflect titles or long-term job assignments; rather, these are used as descriptors to refer to members of the development team performing tasks in the check-in process.\n\nThese roles are:\n\n* _Author_: The individual who has made changes to files in the software repository, and wishes to check these in.\n* _Reviewer_: The individual who reviews changes to files before they are checked in.\n* _Integrator_: The individual who performs the task of merging these files. Usually the reviewer.\n\n### Branching\n\nThree basic types of branches may be included in the above repository:\n\n1. Master branch\n2. Topic branches\n3. Developer branches\n\nBranches which do not fit into the above categories may be created and used during the course of development for various reasons, such as large-scale refactoring of code or implementation of complex features which may cause instability. In these exceptional cases it is the responsibility of the developer who initiates the task which motivated this branching to communicate to the team the role of these branches and any associated procedures for the duration of their use.\n\n#### Master Branch\n\nThe role of the `master` branches is to represent the latest \"ready for test\" version of the software. Source code on the master branch has undergone peer review, and will undergo regular automated testing with notification on failure. Master branches may be unstable (particularly for recent features), but the intent is for the stability of any features on master branches to be non-decreasing. It is the shared responsibility of authors, reviewers, and integrators to ensure this.\n\n#### Topic Branches\n\nTopic branches are used by developers to perform and record work on issues.\n\nTopic branches need not necessarily be stable, even when pushed to the central repository; in fact, the practice of making incremental commits while working on an issue and pushing these to the central repository is encouraged, to avoid lost work and to share work-in-progress. (Small commits also help isolate changes, which can help in identifying which change introduced a defect, particularly when that defect went unnoticed for some time, e.g. using `git bisect`.)\n\nTopic branches should be named according to their corresponding issue identifiers, all lower case, without hyphens. (e.g. branch mct9 would refer to issue #9.)\n\nIn some cases, work on an issue may warrant the use of multiple divergent branches; for instance, when a developer wants to try more than one solution and compare them, or when a \"dead end\" is reached and an initial approach to resolving an issue needs to be abandoned. In these cases, a short suffix should be added to the additional branches; this may be simply a single character (e.g. wtd481b) or, where useful, a descriptive term for what distinguishes the branches (e.g. wtd481verbose). It is the responsibility of the author to communicate which branch is intended to be merged to both the reviewer and the integrator.\n\n#### Developer Branches\n\nDeveloper branches are any branches used for purposes outside of the scope of the above; e.g. to try things out, or maintain a \"my latest stuff\" branch that is not delayed by the review and integration process. These may be pushed to the central repository, and may follow any naming convention desired so long as the owner of the branch is identifiable, and so long as the name chosen could not be mistaken for a topic or master branch.\n\n### Merging\n\nWhen development is complete on an issue, the first step toward merging it back into the master branch is to file a Pull Request (PR). The contributions should meet code, test, and commit message standards as described below, and the pull request should include a completed author checklist, also as described below. Pull requests may be assigned to specific team members when appropriate (e.g. to draw to a specific person's attention).\n\nCode review should take place using discussion features within the pull request. When the reviewer is satisfied, they should add a comment to the pull request containing the reviewer checklist (from below) and complete the merge back to the master branch.\n\nAdditionally:\n\n* Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, [create one](https://github.com/nasa/openmct/issues/new/choose).\n* Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them.\n* A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull request’s __author__.\n* When a pull request is merged, and the corresponding issue closed, the __reviewer__ must add the tag “unverified” to the original issue. This will indicate that although the issue is closed, it has not been tested yet.\n* Every PR must have two reviewers assigned, though only one approval is necessary for merge.\n* Changes to API require approval by a senior developer.\n* When creating a PR, it is the author's responsibility to apply any priority label from the issue to the PR as well. This helps with prioritization.\n\n## Standards\n\nContributions to Open MCT are expected to meet the following standards. In addition, reviewers should use general discretion before accepting changes.\n\n### Code Standards\n\nJavaScript sources in Open MCT must satisfy the [ESLint](https://eslint.org/) rules defined in this repository. [Prettier](https://prettier.io/) is used in conjunction with ESLint to enforce code style via automated formatting. These are verified by the command line build.\n\n#### Code Guidelines\n\nThe following guidelines are provided for anyone contributing source code to the Open MCT project:\n\n1. Write clean code. Here’s a good summary - <https://github.com/ryanmcdermott/clean-code-javascript>.\n1. Include JSDoc for any exposed API (e.g. public methods, classes).\n1. Include non-JSDoc comments as-needed for explaining private variables, methods, or algorithms when they are non-obvious. Otherwise code should be self-documenting.\n1. Classes and Vue components should use camel case, first letter capitalized (e.g. SomeClassName).\n1. Methods, variables, fields, events, and function names should use camelCase, first letter lower-case (e.g. someVariableName).\n1. Source files that export functions should use camelCase, first letter lower-case (eg. testTools.js)\n1. Constants (variables or fields which are meant to be declared and initialized statically, and never changed) should use only capital letters, with underscores between words (e.g. SOME_CONSTANT). They should always be declared as `const`s\n1. File names should be the name of the exported class, plus a .js extension (e.g. SomeClassName.js).\n1. Avoid anonymous functions, except when functions are short (one or two lines) and their inclusion makes sense within the flow of the code (e.g. as arguments to a forEach call). Anonymous functions should always be arrow functions.\n1. Named functions are preferred over functions assigned to variables.\n   eg.\n\n   ```JavaScript\n   function renameObject(object, newName) {\n       Object.name = newName;\n   }\n   ```\n\n   is preferable to\n\n   ```JavaScript\n   const rename = (object, newName) => {\n       Object.name = newName;\n   }\n   ```\n\n1. Avoid deep nesting (especially of functions), except where necessary (e.g. due to closure scope).\n1. End with a single new-line character.\n1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal pattern.\n1. Within a given function's scope, do not mix declarations and imperative code, and  present these in the following order:\n   * First, variable declarations and initialization.\n   * Secondly, imperative statements.\n   * Finally, the returned value. A single return statement at the end of the function should be used, except where an early return would improve code clarity.\n1. Avoid the use of \"magic\" values.\n   eg.\n\n   ```JavaScript\n   const UNAUTHORIZED = 401;\n   if (responseCode === UNAUTHORIZED)\n   ```\n\n   is preferable to\n\n   ```JavaScript\n   if (responseCode === 401)\n   ```\n\n1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases.\n1. Unit Test specs should reside alongside the source code they test, not in a separate directory.\n1. Organize code by feature, not by type.\n   eg.\n\n   ```txt\n   - telemetryTable\n       - row\n           TableRow.js\n           TableRowCollection.js\n           TableRow.vue\n       - column\n           TableColumn.js\n           TableColumn.vue\n       plugin.js\n       pluginSpec.js\n   ```\n\n   is preferable to\n\n   ```txt\n   - telemetryTable\n       - components\n           TableRow.vue\n           TableColumn.vue\n       - collections\n           TableRowCollection.js\n       TableColumn.js\n       TableRow.js\n       plugin.js\n       pluginSpec.js\n   ```\n\nDeviations from Open MCT code style guidelines require two-party agreement, typically from the author of the change and its reviewer.\n\n### Commit Message Standards\n\nCommit messages should:\n\n* Contain a one-line subject, followed by one line of white space, followed by one or more descriptive paragraphs, each separated by one line of white space.\n* Contain a short (usually one word) reference to the feature or subsystem the commit effects, in square brackets, at the start of the subject line (e.g. `[Documentation] Draft of check-in process`).\n* Contain a reference to a relevant issue number in the body of the commit.\n  * This is important for traceability; while branch names also provide this, you cannot tell from looking at a commit what branch it was authored on.\n  * This may be omitted if the relevant issue is otherwise obvious from the commit history (that is, if using `git log` from the relevant commit directly leads to a similar issue reference) to minimize clutter.\n* Describe the change that was made, and any useful rationale therefore.\n  * Comments in code should explain what things do, commit messages describe how they came to be done that way.\n* Provide sufficient information for a reviewer to understand the changes made and their relationship to previous code.\n\nCommit messages should not:\n\n* Exceed 54 characters in length on the subject line.\n* Exceed 72 characters in length in the body of the commit,\n  * Except where necessary to maintain the structure of machine-readable or machine-generated text (e.g. error messages).\n\nSee [Contributing to a Project](http://git-scm.com/book/ch5-2.html) from Pro Git by Shawn Chacon and Ben Straub for a bit of the rationale behind these standards.\n\n## Issue Reporting\n\nIssues are tracked at <https://github.com/nasa/openmct/issues>.\n\nIssue severity is categorized as follows (in ascending order):\n\n* _Trivial_: Minimal impact on the usefulness and functionality of the software; a \"nice-to-have.\" Visual impact without functional impact,\n* _Medium_: Some impairment of use, but simple workarounds exist\n* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though. Complex workarounds exist.\n* _Blocker_: Major functionality is impaired or lost, threatening mission success. Display of telemetry data is impaired or blocked by the bug, which could lead to loss of situational awareness.\n\n## Check Lists\n\nThe following check lists should be completed and attached to pull requests when they are filed (author checklist) and when they are merged (reviewer checklist).\n\n[Within PR Template](.github/PULL_REQUEST_TEMPLATE.md)\n"
  },
  {
    "path": "LICENSE.md",
    "content": "# Open MCT License\n\nOpen MCT, Copyright (c) 2014-2024, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.\n\nOpen MCT is 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.\n\nUnless 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.\n"
  },
  {
    "path": "README.md",
    "content": "# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct) [![npm version](https://img.shields.io/npm/v/openmct.svg)](https://www.npmjs.com/package/openmct) ![CodeQL](https://github.com/nasa/openmct/workflows/CodeQL/badge.svg)\n\nOpen MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.\n\n> [!NOTE]\n> Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)\n\nOnce you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!\n\n![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)\n\n## Building and Running Open MCT Locally\n\nBuilding and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.\n(These instructions assume you are installing as a non-root user; developers have [reported issues](https://github.com/nasa/openmct/issues/1151) running these steps with root privileges.)\n\n1. Clone the source code:\n\n```sh\ngit clone https://github.com/nasa/openmct.git\n```\n\n2. (Optional) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm):\n\n```sh\nnvm install\n```\n\n3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):\n\n```sh\nnpm install\n```\n\n4. Run a local development server:\n\n```\nnpm start\n```\n\n> [!IMPORTANT]\n> Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)\n\nOpen MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).\n\n## Documentation\n\nDocumentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/).\n\n### Examples\n\nThe clearest examples for developing Open MCT plugins are in the\n[tutorials](https://github.com/nasa/openmct-tutorial) provided in\nour documentation.\n\n> [!NOTE]\n> We want Open MCT to be as easy to use, install, run, and develop for as\n> possible, and your feedback will help us get there!\n> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),\n> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),\n> or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).\n\n## Developing Applications With Open MCT\n\nFor more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application).\n\n## Compatibility\n\nThis is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and NodeJS APIs. We have a published list of support available in our package.json's `browserslist` key.\n\nThe project utilizes `nvm` to maintain consistent node and npm versions across all projects. For UNIX, MacOS, Windows WSL, and other POSIX-compliant shell environments, click [here](https://github.com/nvm-sh/nvm). For Windows, check out [nvm-windows](https://github.com/coreybutler/nvm-windows).\n\nIf you encounter an issue with a particular browser, OS, or NodeJS API, please [file an issue](https://github.com/nasa/openmct/issues/new/choose).\n\n## Plugins\n\nOpen MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group\nof software components (including source code and resources such as images and HTML templates)\nthat is intended to be added or removed as a single unit.\n\nAs well as providing an extension mechanism, most of the core Open MCT codebase is also\nwritten as plugins.\n\nFor information on writing plugins, please see [our API documentation](./API.md#plugins).\n\n## Tests\n\nOur automated test coverage comes in the form of unit, e2e, visual, performance, and security tests.\n\n### Unit Tests\n\nUnit Tests are written for [Jasmine](https://jasmine.github.io/api/edge/global)\nand run by [Karma](http://karma-runner.github.io). To run:\n\n`npm test`\n\nThe test suite is configured to load any scripts ending with `Spec.js` found\nin the `src` hierarchy. Full configuration details are found in\n`karma.conf.js`. By convention, unit test scripts should be located\nalongside the units that they test; for example, `src/foo/Bar.js` would be\ntested by `src/foo/BarSpec.js`.\n\n### e2e, Visual, and Performance Testing\n\nOur e2e (end-to-end), Visual, and Performance tests leverage the Playwright framework and are executed using Playwright's test runner, [@playwright/test](https://playwright.dev/).\n\n#### How to Run Tests\n\n- **e2e Tests**: These tests are run on every commit. To run the tests locally, use:\n\n  ```sh\n  npm run test:e2e:ci\n  ```\n\n- **Visual Tests**: For running the visual test suite, use:\n\n  ```sh\n  npm run test:e2e:visual\n  ```\n\n- **Performance Tests**: To initiate the performance tests, enter:\n\n  ```sh\n  npm run test:perf\n  ```\n\nAll tests are located within the `e2e/tests/` directory and are identified by the `*.e2e.spec.js` filename pattern. For more information about the e2e test suite, refer to the [README](./e2e/README.md).\n\n### Security Tests\n\nEach commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).\n\n### Test Reporting and Code Coverage\n\nEach test suite generates a report in CircleCI. For a complete overview of testing functionality, please see our [Circle CI Test Insights Dashboard](https://app.circleci.com/insights/github/nasa/openmct/workflows/the-nightly/overview?branch=master&reporting-window=last-30-days)\n\nOur code coverage is generated during the runtime of our unit, e2e, and visual tests. The combination of those reports is published to [codecov.io](https://app.codecov.io/gh/nasa/openmct/)\n\nFor more on the specifics of our code coverage setup, [see](TESTING.md#code-coverage)\n\n## Glossary\n\nCertain terms are used throughout Open MCT with consistent meanings\nor conventions. Any deviations from the below are issues and should be\naddressed (either by updating this glossary or changing code to reflect\ncorrect usage.) Other developer documentation, particularly in-line\ndocumentation, may presume an understanding of these terms.\n| Term          | Definition                                                                                                                                                                                                                                                                                                                                                   |\n|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| _plugin_      | A removable, reusable grouping of software elements. The application is composed of plugins.                                                                                                                                                                                                                                                                  |\n| _composition_ | In the context of a domain object, this term refers to the set of other domain objects that compose or are contained by that object. A domain object's composition is the set of domain objects that should appear immediately beneath it in a tree hierarchy. It is described in its model as an array of ids, providing a means to asynchronously retrieve the actual domain object instances associated with these identifiers. |\n| _description_ | When used as an object property, this term refers to the human-readable description of a thing, usually a single sentence or short paragraph. It is most often used in the context of extensions, domain object models, or other similar application-specific objects.                                                                                           |\n| _domain object_ | A meaningful object to the user and a distinct thing in the work supported by Open MCT. Anything that appears in the left-hand tree is a domain object.                                                                                                                                                                                                       |\n| _identifier_  | A tuple consisting of a namespace and a key, which together uniquely identifies a domain object.                                                                                                                                                                                                                                                              |\n| _model_       | The persistent state associated with a domain object. A domain object's model is a JavaScript object that can be converted to JSON without losing information, meaning it contains no methods.                                                                                                                                                                 |\n| _name_        | When used as an object property, this term refers to the human-readable name for a thing. It is most often used in the context of extensions, domain object models, or other similar application-specific objects.                                                                                                                                             |\n| _navigation_  | This term refers to the current state of the application with respect to the user's expressed interest in a specific domain object. For example, when a user clicks on a domain object in the tree, they are navigating to it, and it is thereafter considered the navigated object until the user makes another such choice.                                    |\n| _namespace_   | A name used to identify a persistence store. A running Open MCT application could potentially use multiple persistence stores.                                                                                                                                                                                                                                 |\n\n## Open MCT v2.0.0\n\nSupport for our legacy bundle-based API, and the libraries that it was built on (like Angular 1.x), have now been removed entirely from this repository.\n\nFor now if you have an Open MCT application that makes use of the legacy API, [a plugin](https://github.com/nasa/openmct-legacy-plugin) is provided that bootstraps the legacy bundling mechanism and API. This plugin will not be maintained over the long term however, and the legacy support plugin will not be tested for compatibility with future versions of Open MCT. It is provided for convenience only.\n\n### How do I know if I am using legacy API?\n\nYou might still be using legacy API if your source code\n\n- Contains files named bundle.js, or bundle.json,\n- Makes calls to `openmct.$injector()`, or `openmct.$angular`,\n- Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.\n\n### What should I do if I am using legacy API?\n\nPlease refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.\n\n## Related Repos\n\n> [!NOTE]\n> Although Open MCT functions as a standalone project, it is primarily an extensible framework intended to be used as a dependency with users' own plugins and packaging. Furthermore, Open MCT is intended to be used with an HTTP server such as Apache or Nginx. A great example of hosting Open MCT with Apache is `openmct-quickstart` and can be found in the table below.\n\n| Repository | Description |\n| --- | --- |\n| [openmct-tutorial](https://github.com/nasa/openmct-tutorial) | A great place for beginners to learn how to use and extend Open MCT. |\n| [openmct-quickstart](https://github.com/scottbell/openmct-quickstart) | A working example of Open MCT integrated with Apache HTTP server, YAMCS telemetry, and Couch DB for persistence.\n| [Open MCT YAMCS Plugin](https://github.com/akhenry/openmct-yamcs) | Plugin for integrating YAMCS telemetry and command server with Open MCT. |\n| [openmct-performance](https://github.com/unlikelyzero/openmct-performance) | Resources for performance testing Open MCT. |\n| [openmct-as-a-dependency](https://github.com/unlikelyzero/openmct-as-a-dependency) | An advanced guide for users on how to build, develop, and test Open MCT when it's used as a dependency. |\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nOpen MCT is an open source project and may contain externally provided code. External contributions must follow the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md).\n\nThe Open MCT team secures our code base using a combination of code review, dependency review, and periodic security reviews. Static analysis performed during automated verification additionally safeguards against common coding errors which may result in vulnerabilities.\n\n### Reporting a Vulnerability\n\nFor general defects, please for a [Bug Report](https://github.com/nasa/openmct/issues/new/choose)\n\nTo report a vulnerability for Open MCT please send a detailed report to [arc-dl-openmct](mailto:arc-dl-openmct@mail.nasa.gov). \n\nSee our [top-level security policy](https://github.com/nasa/openmct/security/policy) for additional information.\n\n### CodeQL and LGTM\n\nThe [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master/.github/workflows/codeql-analysis.yml) is available to the public. To review the results, fork the repository and run the CodeQL workflow. \n\nCodeQL is run for every pull-request in GitHub Actions.\n\n### ESLint\n\nStatic analysis is run for every push on the master branch and every pull request on all branches in Github Actions. \n\nFor more information about ESLint, visit https://eslint.org/.\n\n### General Support\n\nFor additional support, please open a [Github Discussion](https://github.com/nasa/openmct/discussions). \n\nIf you wish to report a cybersecurity incident or concern, please contact the NASA Security Operations Center either by phone at 1-877-627-2732 or via email address soc@nasa.gov.\n"
  },
  {
    "path": "TESTING.md",
    "content": "# Testing\nOpen MCT Testing is iterating and improving at a rapid pace. This document serves to capture and index existing testing documentation and house documentation which no other obvious location as our testing evolves.\n\n## General Testing Process\nDocumentation located [here](./docs/src/process/testing/plan.md)\n\n## Unit Testing\nUnit testing is essential part of our test strategy and complements our e2e testing strategy.\n\n#### Unit Test Guidelines\n* Unit Test specs should reside alongside the source code they test, not in a separate directory.\n* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.\n* Unit tests for API or for utility functions and classes may be defined at a per-source file level.\n* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).\n* Where builtin functions have been mocked, be sure to clear them between tests.\n* Test at an appropriate level of isolation. Eg. \n    * If you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created. \n    * You do not need to test that the view switcher works, there should be separate tests for that. \n    * You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.\n    * Use your best judgement when deciding on appropriate scope.\n* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.\n* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.\n* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.\n\n#### Unit Test Examples\n* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)\n* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)\n\n#### Unit Testing Execution\n\nThe unit tests can be executed in one of two ways:\n`npm run test` which runs the entire suite against headless chrome\n`npm run test:debug` for debugging the tests in realtime in an active chrome session.\n\n## e2e, performance, and visual testing\nDocumentation located [here](./e2e/README.md)\n\n## Code Coverage\n\nIt's up to the individual developer as to whether they want to add line coverage in the form of a unit test or e2e test.\n\nLine Code Coverage is generated by our unit tests and e2e tests, then combined by ([Codecov.io Flags](https://docs.codecov.com/docs/flags)), and finally reported in GitHub PRs by Codecov.io's PR Bot. This workflow gives a comprehensive (if flawed) view of line coverage.\n\n### Karma-istanbul\n\nLine coverage is generated by our `karma-coverage-istanbul-reporter` package as defined in our `karma.conf.js` file:\n\n```js\n    coverageIstanbulReporter: {\n      fixWebpackSourcePaths: true,\n      skipFilesWithNoCoverage: true,\n      dir: 'coverage/unit', //Sets coverage file to be consumed by codecov.io\n      reports: ['lcovonly']\n    },\n```\n\nOnce the file is generated, it can be published to codecov with\n\n```json\n    \"cov:unit:publish\": \"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit\",\n```\n\n### e2e\nThe e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events:\n\n1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.mjs` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js). \n1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.\n1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`\n1. Most of the tests focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:ci:publish`.\n1. The rest of our coverage only appears when run against persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.\n\n\n### Limitations in our code coverage reporting\nOur code coverage implementation has some known limitations:\n- [Variability](https://github.com/nasa/openmct/issues/5811)\n- [Accuracy](https://github.com/nasa/openmct/issues/7015)\n- [Vue instrumentation gaps](https://github.com/nasa/openmct/issues/4973)\n\n## Troubleshooting CI\nThe following is an evolving guide to troubleshoot CI and PR issues.\n\n### Github Checks failing\nThere are a few reasons that your GitHub PR could be failing beyond simple failed tests.\n* Required Checks. We're leveraging required checks in GitHub so that we can quickly and precisely control what becomes and informational failure vs a hard requirement. The only way to determine the difference between a required vs information check is check for the `(Required)` emblem next to the step details in GitHub Checks.\n* Not all required checks are run per commit. You may need to manually trigger addition GitHub checks with a `pr:<label>` label added to your PR.\n\n### Flaky tests\n\n(CircleCI's test insights feature)[https://circleci.com/blog/introducing-test-insights-with-flaky-test-detection/] collects historical data about the individual test results for both unit and e2e tests. Note: only a 14 day window of flake is available.\n\n### Local=Pass and CI=Fail\nAlthough rare, it is possible that your test can pass locally but fail in CI.\n\n### Reset your workspace\nIt's possible that you're running with dependencies or a local environment which is out of sync with the branch you're working on. Make sure to execute the following:\n\n```sh\nnvm use\nnpm run clean\nnpm install\n```\n\n#### Run tests in the same container as CI\n\nIn extreme cases, tests can fail due to the constraints of running within a container. To execute tests in exactly the same way as run in CircleCI. \n\n```sh\n// Replace {X.X.X} with the current Playwright version \n// from our package.json or circleCI configuration file\ndocker run --rm --network host --cpus=\"2\" -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash\nnpm install\n```\n\nAt this point, you're running inside the same container and with 2 cpu cores. You can specify the unit tests:\n```sh\nnpm run test\n```\nor e2e tests:\n\n```sh\nnpx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep <the testcase name>\n```"
  },
  {
    "path": "build-docs.sh",
    "content": "#!/bin/bash\n\n#*****************************************************************************\n#* Open MCT, Copyright (c) 2014-2024, United States Government\n#* as represented by the Administrator of the National Aeronautics and Space\n#* Administration. All rights reserved.\n#*\n#* Open MCT is licensed under the Apache License, Version 2.0 (the\n#* \"License\"); you may not use this file except in compliance with the License.\n#* You may obtain a copy of the License at\n#* http://www.apache.org/licenses/LICENSE-2.0.\n#*\n#* Unless required by applicable law or agreed to in writing, software\n#* distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n#* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n#* License for the specific language governing permissions and limitations\n#* under the License.\n#*\n#* Open MCT includes source code licensed under additional open source\n#* licenses. See the Open Source Licenses file (LICENSES.md) included with\n#* this source code distribution or the Licensing information page available\n#* at runtime from the About dialog for additional information.\n#*****************************************************************************\n\n# Script to build and deploy docs.\n\nOUTPUT_DIRECTORY=\"dist/docs\"\n# Docs, once built, are pushed to the private website repo\nREPOSITORY_URL=\"git@github.com:nasa/openmct-website.git\"\nWEBSITE_DIRECTORY=\"website\"\n\nBUILD_SHA=`git rev-parse HEAD`\n\n# A remote will be created for the git repository we are pushing to.\n# Don't worry, as this entire directory will get trashed in between builds.\nREMOTE_NAME=\"documentation\"\nWEBSITE_BRANCH=\"master\"\n\n# Clean output directory, JSDOC will recreate\nif [ -d $OUTPUT_DIRECTORY ]; then\n    rm -rf $OUTPUT_DIRECTORY || exit 1\nfi\n\nnpm run docs\n\necho \"git clone $REPOSITORY_URL website\"\ngit clone $REPOSITORY_URL website || exit 1\necho \"cp -r $OUTPUT_DIRECTORY $WEBSITE_DIRECTORY\"\ncp -r $OUTPUT_DIRECTORY $WEBSITE_DIRECTORY\necho \"cd $WEBSITE_DIRECTORY\"\ncd $WEBSITE_DIRECTORY || exit 1\n\n# Configure github for CircleCI user.\ngit config user.email \"buildbot@circleci.com\"\ngit config user.name \"BuildBot\"\n\necho \"git add .\"\ngit add .\necho \"git commit -m \\\"Docs updated from build $BUILD_SHA\\\"\"\ngit commit -m \"Docs updated from build $BUILD_SHA\"\n# Push to the website repo\ngit push\n"
  },
  {
    "path": "codecov.yml",
    "content": "codecov:\n  require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass\n\n# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.\ngithub_checks:\n  annotations: false\n\ncoverage:\n  status:\n    project:\n      default:\n        informational: true\n    patch:\n      default:\n        informational: true\n  precision: 2\n  round: down\n  range: \"66...100\"\n\nflags:\n  unit:\n    carryforward: false\n  e2e-ci:\n    carryforward: false\n  e2e-full:\n    carryforward: true\n\ncomment:\n  layout: \"diff,flags,files,footer\"\n  behavior: default\n  require_changes: false\n  show_carryforward_flags: true\n"
  },
  {
    "path": "copyright-notice.html",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n"
  },
  {
    "path": "copyright-notice.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n"
  },
  {
    "path": "docs/src/guide/security.md",
    "content": "# Security Guide\n\nOpen MCT is a rich client with plugin support that executes as a single page\nweb application in a browser environment. Security concerns and\nvulnerabilities associated with the web as a platform should be considered\nbefore deploying Open MCT (or any other web application) for mission or\nproduction usage.\n\nThis document describes several important points to consider when developing\nfor or deploying Open MCT securely. Other resources such as\n[Open Web Application Security Project (OWASP)](https://www.owasp.org)\nprovide a deeper and more general overview of security for web applications.\n\n\n## Security Model\n\nOpen MCT has been architected assuming the following deployment pattern:\n\n* A tagged, tested Open MCT version will be used.\n* Externally authored plugins will be installed.\n* A server will provide persistent storage, telemetry, and other shared data.\n* Authorization, authentication, and auditing will be handled by a server.\n\n\n## Security Procedures\n\nThe Open MCT team secures our code base using a combination of code review,\ndependency review, and periodic security reviews. Static analysis performed \nduring automated verification additionally safeguards against common \ncoding errors which may result in vulnerabilities.\n\n\n### Code Review\n\nAll contributions are reviewed by internal team members. External\ncontributors receive increased scrutiny for security and quality,\nand must sign a licensing agreement.\n\n### Dependency Review\n\nBefore integrating third-party dependencies, they are reviewed for security\nand quality, with consideration given to authors and users of these\ndependencies, as well as review of open source code.\n\n### Periodic Security Reviews\n\nOpen MCT's code, design, and architecture are periodically reviewed\n(approximately annually) for common security issues, such as the\n[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).\n\n\n## Security Concerns\n\nCertain security concerns deserve special attention when deploying Open MCT,\nor when authoring plugins.\n\n### Identity Spoofing\n\nOpen MCT issues calls to web services with the privileges of a logged in user.\nCompromised sources (either for Open MCT itself or a plugin) could\ntherefore allow malicious code to execute with those privileges.\n\nTo avoid this:\n\n* Serve Open MCT and other scripts over SSL (https rather than http)\n  to prevent man-in-the-middle attacks.\n* Exercise precautions such as security reviews for any plugins or\n  applications built for or with Open MCT to reject malicious changes.\n\n### Information Disclosure\n\nIf Open MCT is used to handle or display sensitive data, any components\n(such as adapter plugins) must take care to avoid leaking or disclosing\nthis information. For example, avoid sending sensitive data to third-party\nservers or insecure APIs.\n\n### Data Tampering\n\nThe web application architecture leaves open the possibility that direct\ncalls will be made to back-end services, circumventing Open MCT entirely.\nAs such, Open MCT assumes that server components will perform any necessary\ndata validation during calls issues to the server.\n\nAdditionally, plugins which serialize and write data to the server must\nescape that data to avoid database injection attacks, and similar.\n\n### Repudiation\n\nOpen MCT assumes that servers log any relevant interactions and associates\nthese with a user identity; the specific user actions taken within the\napplication are assumed not to be of concern for auditing.\n\nIn the absence of server-side logging, users may disclaim (maliciously,\nmistakenly, or otherwise) actions taken within the system without any\nway to prove otherwise.\n\nIf keeping client-level interactions is important, this will need to be\nimplemented via a plugin.\n\n### Denial-of-service\n\nOpen MCT assumes that server-side components will be insulated against\ndenial-of-service attacks. Services should only permit resource-intensive\ntasks to be initiated by known or trusted users.\n\n### Elevation of Privilege\n\nCorollary to the assumption that servers guide against identity spoofing,\nOpen MCT assumes that services do not allow a user to act with\ninappropriately escalated privileges. Open MCT cannot protect against\nsuch escalation; in the clearest case, a malicious actor could interact\nwith web services directly to exploit such a vulnerability.\n\n## Additional Reading\n\nThe following resources have been used as a basis for identifying potential\nsecurity threats to Open MCT deployments in preparation of this document:\n\n* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)\n* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)\n* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)\n"
  },
  {
    "path": "docs/src/index.md",
    "content": "# Open MCT Documentation\n\n## Overview\n\n Documentation is provided to support the use and development of\n Open MCT. It's recommended that before doing\n any development with Open MCT you take some time to familiarize yourself\n with the documentation below.\n\n Open MCT provides functionality out of the box, but it's also a platform for\n building rich mission operations applications based on modern web technology. \n The platform is configured by plugins which extend the platform at a variety\n of extension points. The details of how to\n extend the platform are provided in the following documentation.\n\n## Sections\n\n* The [API](api/) uses inline documentation.\n using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and\n functions that make up the software platform.\n\n* The [Development Process](process/) document describes the\n Open MCT software development cycle.\n\n* The [tutorial](https://github.com/nasa/openmct-tutorial) and [plugin template](https://github.com/nasa/openmct-hello) give examples of extending the platform to add\n functionality and integrate with data sources.\n"
  },
  {
    "path": "docs/src/process/cycle.md",
    "content": "# Development Cycle\n\nDevelopment of Open MCT occurs on an iterative cycle of\nsprints and releases.\n\n* A _sprint_ is three weeks in duration, and represents a\n  set of improvements that can be completed and tested by the\n  development team. Software at the end of the sprint is\n  \"semi-stable\"; it will have undergone reduced testing and may carry\n  defects or usability issues of lower severity, particularly if\n  there are workarounds.\n* A _release_ occurs every four sprints. Releases are stable, and\n  will have undergone full acceptance testing to ensure that the\n  software behaves correctly and usably.\n\n## Roles\n\nThe sprint process assumes the presence of a __project manager.__\nThe project manager is responsible for\nmaking tactical decisions about what development work will be\nperformed, and for coordinating with stakeholders to arrive at\nhigher-level strategic decisions about desired functionality\nand characteristics of the software, major external milestones,\nand so forth.\n\nIn the absence of a dedicated project manager, this role may be rotated\namong members of the development team on a per-sprint basis.\n\nResponsibilities of the project manager including:\n\n* Maintaining (with agreement of stakeholders) a \"road map\" of work\n  planned for future releases/sprints; this should be higher-level,\n  usually expressed as \"themes\",\n  with just enough specificity to gauge feasibility of plans,\n  relate work back to milestones, and identify longer-term\n  dependencies.\n* Determining (with assistance from the rest of the team) which\n  issues to work on in a given sprint and how they shall be\n  assigned.\n* Pre-planning subsequent sprints to ensure that all members of the\n  team always have a clear direction.\n* Scheduling and/or ensuring adherence to\n  [process points](#process-points).\n* Responding to changes within the sprint (shifting priorities,\n  new issues) and re-allocating work for the sprint as needed.\n\n## Sprint Calendar\n\nCertain [process points](#process-points) are regularly scheduled in\nthe sprint cycle.\n\n### Sprints by Release\n\nAllocation of work among sprints should be planned relative to release\ngoals and milestones. As a general guideline, higher-risk work (large\nnew features which may carry new defects, major refactoring, design\nchanges with uncertain effects on usability) should be allocated to\nearlier sprints, allowing for time in later sprints to ensure stability.\n\n| Sprint | Focus                                                   |\n|:------:|:--------------------------------------------------------|\n| __1__  | Prototyping, design, experimentation.                   |\n| __2__  | New features, refinements, enhancements.                |\n| __3__  | Feature completion, low-risk enhancements, bug fixing.  |\n| __4__  | Stability & quality assurance.                          |\n\n### Sprints 1-3\n\nThe first three sprints of a release are primarily centered around\ndevelopment work, with regular acceptance testing in the third\nweek. During this third week, the top priority should be passing\nacceptance testing (e.g. by resolving any blockers found); any\nresources not needed for this effort should be used to begin work\nfor the subsequent sprint.\n\n| Week  | Mon                       | Tue    | Wed | Thu                          | Fri                                   |\n|:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-------------------------------------:|\n| __1__ | Sprint plan               | Tag-up |     |                              |                                       |\n| __2__ |                           | Tag-up |     |                              | Code freeze  and sprint branch        |\n| __3__ | Per-sprint testing        | Triage |     | _Per-sprint testing*_        | Ship and merge sprint branch to master|\n\n&ast; If necessary.\n\n### Sprint 4\n\nThe software must be stable at the end of the fourth sprint; because of\nthis, the fourth sprint is scheduled differently, with a heightened\nemphasis on testing.\n\n| Week   | Mon                       | Tue    | Wed | Thu                          | Fri         |\n|-------:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|\n| __1__  | Sprint plan               | Tag-up |     |                              | Code freeze |\n| __2__  | Per-release testing       | Triage |     |                              |             |\n| __3__  | _Per-release testing*_    | Triage |     | _Per-release testing*_       | Ship        |\n\n&ast; If necessary.\n\n## Process Points\n\n* __Sprint plan.__ Project manager allocates issues based on\n  theme(s) for sprint, then reviews with team. Each team member\n  should have roughly two weeks of work allocated (to allow time\n  in the third week for testing of work completed.)\n  * Project manager should also sketch out subsequent sprint so\n    that team may begin work for that sprint during the\n    third week, since testing and blocker resolution is unlikely\n    to require all available resources.\n  * Testing success criteria identified per issue (where necessary). This could be in the form of acceptance tests on the issue or detailing performance tests, for example.\n* __Tag-up.__ Check in and status update among development team.\n  May amend plan for sprint as-needed.\n* __Code freeze.__ Any new work from this sprint\n  (features, bug fixes, enhancements) must be integrated by the\n  end of the second week of the sprint. After code freeze, a sprint\n  branch will be created (and until the end of the sprint) the only \n  changes that should be merged into the sprint branch should \n  directly address issues needed to pass acceptance testing.\n  During this time, any other feature development will continue to\n  be merged into the master branch for the next sprint.\n* __Sprint branch merge to master.__ After acceptance testing, the sprint branch\n  will be merged back to the master branch. Any code conflicts that \n  arise will be resolved by the team.\n* [__Per-release Testing.__](testing/plan.md#per-release-testing)\n  Structured testing with predefined\n  success criteria. No release should ship without passing\n  acceptance tests. Time is allocated in each sprint for subsequent\n  rounds of acceptance testing if issues are identified during a\n  prior round. Specific details of acceptance testing need to be\n  agreed-upon with relevant stakeholders and delivery recipients,\n  and should be flexible enough to allow changes to plans\n  (e.g. deferring delivery of some feature in order to ensure\n  stability of other features.) Baseline testing includes:\n  * [__Testathon.__](testing/plan.md#user-testing)\n    Multi-user testing, involving as many users as\n    is feasible, plus development team. Open-ended; should verify\n    completed work from this sprint using the sprint branch, test \n    exploratory for regressions, et cetera.\n  * [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A\n    test to verify that the software remains\n    stable after running for longer durations. May include some\n    combination of automated testing and user verification (e.g.\n    checking to verify that software remains subjectively\n    responsive at conclusion of test.)\n  * [__Unit Testing.__](testing/plan.md#unit-testing)\n    Automated testing integrated into the\n    build. (These tests are verified to pass more often than once\n    per sprint, as they run before any merge to master, but still\n    play an important role in per-release testing.)\n* [__Per-sprint Testing.__](testing/plan.md#per-sprint-testing)\n  Subset of Pre-release Testing\n  which should be performed before shipping at the end of any\n  sprint. Time is allocated for a second round of\n  Pre-release Testing if the first round is not passed. Smoke tests collected from issues/PRs\n* __Triage.__ Team reviews issues from acceptance testing and uses\n  success criteria to determine whether or not they should block\n  release, then formulates a plan to address these issues before\n  the next round of acceptance testing. Focus here should be on\n  ensuring software passes that testing in order to ship on time;\n  may prefer to disable malfunctioning components and fix them\n  in a subsequent sprint, for example.\n* [__Ship.__](version.md) Tag a code snapshot that has passed release/sprint\n  testing and deploy that version. (Only true if relevant\n  testing has passed by this point; if testing has not\n  been passed, will need to make ad hoc decisions with stakeholders,\n  e.g. \"extend the sprint\" or \"defer shipment until end of next\n  sprint.\")\n"
  },
  {
    "path": "docs/src/process/index.md",
    "content": "# Development Process\n\nThe process used to develop Open MCT is described in the following\ndocuments:\n\n* The [Development Cycle](cycle.md) describes how and when specific\n  process points are repeated during development.\n* The [Version Guide](version.md) describes version numbering for\n  Open MCT (both semantics and process.)\n* The [Test Plan](testing/plan.md) summarizes the approaches used\n  to test Open MCT.\n"
  },
  {
    "path": "docs/src/process/release.md",
    "content": "\n# Release of NASA Open MCT NPM Package\n\nThis document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.\n\n## 1. Pre-requisites\n\nBefore releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.\n\n## 2. Versioning\n\nVersioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.\n\n## 3. Changelog Maintenance\n\nA comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.\n\n## 4. Notable Changes Labels on GitHub PRs\n\nFor the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':\n\n- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.\n- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.\n- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.\n\n## 6. Community & Contributions\n\nA flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.\n\nThank you for your collaboration and commitment to moving the project onto a text big club. \n"
  },
  {
    "path": "docs/src/process/testing/plan.md",
    "content": "# Test Plan\n\n## Test Levels\n\nTesting for Open MCT includes:\n\n* _Smoke testing_: Brief, informal testing to verify that no major issues\n  or regressions are present in the software, or in specific features of\n  the software.\n* _Unit testing_: Automated verification of the performance of individual\n  software components.\n* _User testing_: Testing with a representative user base to verify\n  that application behaves usably and as specified.\n* _Long-duration testing_: Testing which takes place over a long period\n  of time to detect issues which are not readily noticeable during\n  shorter test periods.\n\n### Smoke Testing\n\nManual, non-rigorous testing of the software and/or specific features\nof interest. Verifies that the software runs and that basic functionality\nis present. The outcome of Smoke Testing should be a simplified list of Acceptance Tests which could be executed by another team member with sufficient context.\n\n### Unit Testing\n\nUnit tests are automated tests which exercise individual software\ncomponents. Tests are subject to code review along with the actual\nimplementation, to ensure that tests are applicable and useful.\n\nUnit tests should meet\n[test standards](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#test-standards)\nas described in the contributing guide.\n\n### User Testing\n\nUser testing is performed at scheduled times involving target users\nof the software or reasonable representatives, along with members of\nthe development team exercising known use cases. Users test the\nsoftware directly; the software should be configured as similarly to\nits planned production configuration as is feasible without introducing\nother risks (e.g. damage to data in a production instance.)\n\nUser testing will focus on the following activities:\n\n* Verifying issues resolved since the last test session.\n* Checking for regressions in areas related to recent changes.\n* Using major or important features of the software,\n  as determined by the user.\n* General \"trying to break things.\"\n\nDuring user testing, users will\n[report issues](https://github.com/nasa/openmct/issues/new/choose)\nas they are encountered.\n\nDesired outcomes of user testing are:\n\n* Identified software defects.\n* Areas for usability improvement.\n* Feature requests (particularly missed requirements.)\n* Recorded issue verification.\n\n### Long-duration Testing\n\nLong-duration testing occurs over a twenty-four hour period. The\nsoftware is run in one or more stressing cases representative of expected\nusage. After twenty-four hours, the software is evaluated for:\n\n* Performance metrics: Have memory usage or CPU utilization increased\n  during this time period in unexpected or undesirable ways?\n* Subjective usability: Does the software behave in the same way it did\n  at the start of the test? Is it as responsive?\n\nAny defects or unexpected behavior identified during testing should be\n[reported as issues](https://github.com/nasa/openmct/issues/new/choose)\nand reviewed for severity.\n\n## Test Performance\n\nTests are performed at various levels of frequency.\n\n* _Per-merge_: Performed before any new changes are integrated into\n  the software.\n* _Per-sprint_: Performed at the end of every [sprint](../cycle.md).\n* _Per-release_: Performed at the end of every [release](../cycle.md).\n\n### Per-merge Testing\n\nBefore changes are merged, the author of the changes must perform:\n\n* _Smoke testing_ (both generally, and for areas which interact with\n  the new changes.)\n* _Unit testing_ (as part of the automated build step.)\n\nChanges are not merged until the author has affirmed that both\nforms of testing have been performed successfully; this is documented\nby the [Author Checklist](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#author-checklist).\n\n### Per-sprint Testing\n\nBefore a sprint is closed, the development team must additionally\nperform:\n\n* A relevant subset of [_user testing_](procedures.md#user-test-procedures)\n  identified by the acting [project manager](../cycle.md#roles).\n* [_Long-duration testing_](procedures.md#long-duration-testing)\n  (specifically, for 24 hours.)\n\nIssues are reported as a product of both forms of testing.\n\nA sprint is not closed until both categories have been performed on\nthe latest snapshot of the software, _and_ no issues labelled as\n[\"blocker\"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)\nremain open.\n\n### Per-release Testing\n\nAs [per-sprint testing](#per-sprint-testing), except that _user testing_\nshould cover all test cases, with less focus on changes from the specific\nsprint or release.\n\nPer-release testing should also include any acceptance testing steps\nagreed upon with recipients of the software.\n\nA release is not closed until both categories have been performed on\nthe latest snapshot of the software, _and_ no issues labelled as\n[\"blocker\" or \"critical\"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)\nremain open.\n\n### Testathons\nTestathons can be used as a means of performing per-sprint and per-release testing. \n\n#### Timing\nFor per-sprint testing, a testathon is typically performed at the beginning of the third week of a sprint, and again later that week to verify any fixes. For per-release testing, a testathon is typically performed prior to any formal testing processes that are applicable to that release.\n\n#### Process\n\n1. Prior to the scheduled testathon, a list will be compiled of all issues that are closed and unverified.\n2. For each issue, testers should review the associated PR for testing instructions. See the contributing guide for instructions on [pull requests](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md#merging).\n3. As each issue is verified via testing, any team members testing it should leave a comment on that issue indicating that it has been verified fixed.\n4. If a bug is found that relates to an issue being tested, notes should be included on the associated issue, and the issue should be reopened. Bug notes should include reproduction steps.\n5. For any bugs that are not obviously related to any of the issues under test, a new issue should be created with details about the bug, including reproduction steps. If unsure about whether a bug relates to an issue being tested, just create a new issue.\n6. At the end of the testathon, triage will take place, where all tested issues will be reviewed.\n7. If verified fixed, an issue will remain closed, and will have the “unverified” label removed.\n8. For any bugs found, a severity will be assigned.\n9. A second testathon will be scheduled for later in the week that will aim to address all issues identified as blockers, as well as any other issues scoped by the team during triage.\n10. Any issues that were not tested will remain \"unverified\" and will be picked up in the next testathon.\n"
  },
  {
    "path": "docs/src/process/version.md",
    "content": "# Version Guide\n\nThis document describes semantics and processes for providing version\nnumbers for Open MCT, and additionally provides guidelines for dependent\nprojects developed by the same team.\n\nVersions are incremented at specific points in Open MCT's\n[Development Cycle](cycle.md); see that document for a description of\nsprints and releases.\n\n## Audience\n\nIndividuals interested in consuming version numbers can be categorized as\nfollows:\n\n* _Users_: Generally disinterested, occasionally wish to identify version\n  to cross-reference against documentation, or to report issues.\n* _Testers_: Want to identify which version of the software they are\n  testing, e.g. to file issues for defects.\n* _Internal developers_: Often, inverse of testers; want to identify which\n  version of software was/is in use when certain behavior is observed. Want\n  to be able to correlate versions in use with “streams” of development\n  (e.g. dev vs. prod), when possible.\n* _External developers_: Need to understand which version of software is\n  in use when developing/maintaining plug-ins, in order to ensure\n  compatibility of their software.\n\n## Version Reporting\n\nSoftware versions should be reflected in the user interface of the\napplication in three ways:\n\n* _Version number_: A semantic version (see below) which serves both to\n  uniquely identify releases, as well as to inform plug-in developers\n  about compatibility with previous releases.\n* _Revision identifier_: While using git, the commit hash. Supports\n  internal developers and testers by uniquely identifying client\n  software snapshots.\n* _Branding_: Identifies which variant is in use. (Typically, Open MCT\n  is re-branded when deployed for a specific mission or center.)\n\n## Version Numbering\n\nOpen MCT shall provide version numbers consistent with\n[Semantic Versioning 2.0.0](http://semver.org/). In summary, versions\nare expressed in a \"major.minor.patch\" form, and incremented based on\nnature of changes to external API. Breaking changes require a \"major\"\nversion increment; backwards-compatible changes require a \"minor\"\nversion increment; neutral changes (such as bug fixes) require a \"patch\"\nversion increment. A hyphen-separated suffix indicates a pre-release\nversion, which may be unstable or may not fully meet compatibility\nrequirements.\n\nAdditionally, the following project-specific standards will be used:\n\n* During development, a \"-next\" suffix shall be appended to the\n  version number. The version number before the suffix shall reflect\n  the next expected version number for release.\n* Prior to a 1.0.0 release, the _minor_ version will be incremented\n  on a per-release basis; the _patch_ version will be incremented on a\n  per-sprint basis.\n* Starting at version 1.0.0, version numbers will be updated with each\n  completed sprint. The version number for the sprint shall be\n  determined relative to the previous released version; the decision\n  to increment the _major_, _minor_, or _patch_ version should be\n  made based on the nature of changes during that release. (It is\n  recommended that these numbers are incremented as changes are\n  introduced, such that at end of release the version number may\n  be chosen by simply removing the suffix.)\n* The first three sprints in a release may be unstable; in these cases, a\n  unique version identifier should still be generated, but a suffix\n  should be included to indicate that the version is not necessarily\n  production-ready. Recommended suffixes are:\n\n Sprint |  Suffix\n:------:|:--------:\n   1    | `-alpha`\n   2    | `-beta`\n   3    | `-rc`\n\n### Scope of External API\n\n\"External API\" refers to the API exposed to, documented for, and used by\nplug-in developers. Changes to interfaces used internally by Open MCT\n(or otherwise not documented for use externally) require only a _patch_\nversion bump.\n\n## Incrementing Versions\n\nAt the end of a sprint, the [project manager](cycle.md#roles)\nshould update (or delegate the task of updating) Open MCT version\nnumbers by the following process:\n\n1. Update version number in `package.json`\n  1. Checkout branch created for the last sprint that has been successfully tested.\n  2. Remove a `-next` suffix from the version in `package.json`.\n  3. Verify that resulting version number meets semantic versioning\n     requirements relative to previous stable version. Increment the \n     version number if necessary.\n  4. If version is considered unstable (which may be the case during\n     the first three sprints of a release), apply a new suffix per\n     [Version Numbering](#version-numbering) guidance above.\n2. Tag the release.\n  1. Commit changes to `package.json` on the new branch created in \n     the previous step.\n     The commit message should reference the sprint being closed,\n     preferably by a URL reference to the associated Milestone in\n     GitHub.\n  2. Verify that build still completes, that application passes\n     smoke-testing, and that only differences from tested versions\n     are the changes to version number above.\n  3. Push the new branch.\n  4. Tag this commit with the version number, prepending the letter \"v\".\n     (e.g. `git tag v0.9.3-alpha`)\n  5. Push the tag to GitHub. (e.g. `git push origin v0.9.3-alpha`).\n3. Upload a release archive.\n  1. Use the [GitHub release interface](https://github.com/nasa/openmct/releases)\n     to draft a new release.\n  2. Choose the existing tag for the new version (created and pushed above.)\n     Enter the tag name as the release name as well; see existing releases\n     for examples. (e.g. `Open MCT v0.9.3-alpha`)\n  3. Designate the release as a \"pre-release\" as appropriate (for instance,\n     when the version number has been suffixed as unstable, or when\n     the version number is below 1.0.0.)\n  4. Add release notes including any breaking changes, enhancements, \n     bug fixes with solutions in brief.\n  5. Publish the release.\n4. Publish the release to npm\n  1. Login to npm\n  2. Checkout the tag created in the previous step.\n  3. In `package.json` change package to be public (private: false)\n  4. Test the package before publishing by doing `npm publish --dry-run` \n     if necessary.\n  5. Publish the package to the npmjs registry (e.g. `npm publish --access public`) \n     NOTE: Use the `--tag unstable` flag to the npm publish if this is a prerelease.\n  6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)\n5. Update snapshot status in `package.json`\n  1. Create a new branch off the `master` branch.\n  2. Remove any suffix from the version number, \n     or increment the _patch_ version if there is no suffix.\n  3. Append a `-next` suffix.\n  4. Commit changes to `package.json` on the `master` branch.\n     The commit message should reference the sprint being opened,\n     preferably by a URL reference to the associated Milestone in\n     GitHub.\n  5. Verify that build still completes, that application passes\n     smoke-testing.\n  6. Create a PR to be merged into the `master` branch.\n\nProjects dependent on Open MCT being co-developed by the Open MCT\nteam should follow a similar process, except that they should\nadditionally update their dependency on Open MCT to point to the\nlatest archive when removing their `-next` status, and\nthat they should be pointed back to the `master` branch after\nthis has completed.\n"
  },
  {
    "path": "e2e/.eslintrc.cjs",
    "content": "/* eslint-disable no-undef */\nmodule.exports = {\n  extends: ['plugin:playwright/recommended'],\n  rules: {\n    'playwright/expect-expect': 'off'\n  },\n  overrides: [\n    {\n      //Apply Best Practices to externalFixtures and exampleTemplate.e2e.spec.js\n      files: [\n        'appActions.js',\n        'baseFixtures.js',\n        'pluginFixtures.js',\n        '**/exampleTemplate.e2e.spec.js'\n      ],\n      rules: {\n        'playwright/no-raw-locators': 'error',\n        'playwright/no-nth-methods': 'error',\n        'playwright/no-get-by-title': 'error',\n        'playwright/prefer-comparison-matcher': 'error'\n      }\n    },\n    {\n      // Disable no-raw-locators for .contract.perf.spec.js files until https://github.com/grafana/xk6-browser/issues/1226\n      files: ['**/*.contract.perf.spec.js'],\n      rules: {\n        'playwright/no-raw-locators': 'off'\n      }\n    },\n    {\n      files: ['**/*.visual.spec.js'],\n      rules: {\n        'playwright/no-networkidle': 'off' //https://github.com/nasa/openmct/issues/7549\n      }\n    }\n  ]\n};\n"
  },
  {
    "path": "e2e/.npmignore",
    "content": "*\n!appActions.js\n!baseFixtures.js\n!pluginFixtures.js\n!avpFixtures.js\n!index.js\n!*.md\n"
  },
  {
    "path": "e2e/.percy.ci.yml",
    "content": "version: 2\nsnapshot:\n  widths: [1024]\n  min-height: 1440 # px\n  percyCSS: |\n    /* Clock indicator... your days are numbered  */\n    .t-indicator-clock {\n      display: none !important;\n    }\n    .c-input--datetime {\n      opacity: 0 !important;\n    }\n    /* Timer object text */\n    .c-ne__time-and-creator {\n      opacity: 0 !important;\n    }\n    /* Time Conductor ticks */\n    div.c-conductor-axis.c-conductor__ticks > svg {\n      opacity: 0 !important;\n    }\n    /* Embedded timestamp in notebooks */\n    .c-ne__embed__time{\n      opacity: 0 !important;\n    }\n    /* Time Conductor Start Time */\n    .c-compact-tc__setting-value{\n      opacity: 0 !important;\n    }\n    /* Chart Area for Plots */\n    .gl-plot-chart-area{\n      opacity: 0 !important;\n    }\n    /* SWG Time values on plot */\n    .gl-plot-x{\n      opacity: 0 !important;\n    }\n    /* Notification Time in modal */\n    .c-ne__time{\n      opacity: 0 !important;\n    }\n    /* Snapshot name with embedded time */\n    .l-browse-bar__snapshot-datetime{\n      opacity: 0 !important;\n    }\n"
  },
  {
    "path": "e2e/.percy.nightly.yml",
    "content": "version: 2\nsnapshot:\n  widths: [1024, 2000]\n  min-height: 1440 # px\n  percyCSS: |\n    /* Clock indicator... your days are numbered  */\n    .t-indicator-clock {\n      display: none !important;\n    }\n    .c-input--datetime {\n      opacity: 0 !important;\n    }\n    /* Timer object text */\n    .c-ne__time-and-creator {\n      opacity: 0 !important;\n    }\n    /* Time Conductor ticks */\n    div.c-conductor-axis.c-conductor__ticks > svg {\n      opacity: 0 !important;\n    }\n    /* Embedded timestamp in notebooks */\n    .c-ne__embed__time{\n      opacity: 0 !important;\n    }\n    /* Time Conductor Start Time */\n    .c-compact-tc__setting-value{\n      opacity: 0 !important;\n    }\n    /* Chart Area for Plots */\n    .gl-plot-chart-area{\n      opacity: 0 !important;\n    }\n    /* SWG Time values on plot */\n    .gl-plot-x{\n      opacity: 0 !important;\n    }\n    /* Notification Time in modal */\n    .c-ne__time{\n      opacity: 0 !important;\n    }\n    /* Snapshot name with embedded time */\n    .l-browse-bar__snapshot-datetime{\n      opacity: 0 !important;\n    }\n"
  },
  {
    "path": "e2e/README.md",
    "content": "# e2e testing\n\nThis document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests).\n\n## Table of Contents\n\nThis document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT. Please use the built-in Github Table of Contents functionality at the top left of this page or the markup.\n\n1. [Getting Started](#getting-started)\n2. [Types of Testing](#types-of-e2e-testing)\n3. [Architecture](#test-architecture-and-ci)\n\n## Getting Started\n\nWhile our team does our best to lower the barrier to entry to working with our e2e framework and Open MCT, there is a bit of work required to get from 0 to 1 test contributed.\n\n### Getting started with Playwright\n\nIf this is your first time ever using the Playwright framework, we recommend going through the [Getting Started Guide](https://playwright.dev/docs/next/intro) which can be completed in about 15 minutes. This will give you a concise tour of Playwright's functionality and an understanding of the official Playwright documentation which we leverage in Open MCT.\n\n### Getting started with Open MCT's implementation of Playwright\n\nOnce you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:\n\n1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)\n2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.\n3. Create a 'Plot' Object (e.g.: 'Stacked Plot')\n4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')\n5. Expand the Tree and note the hierarchy of objects which were created.\n6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.\n7. Modify the embedded plot with Telemetry Data.\n\nWhat you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.\n\nRecreate the steps above with Playwright's codegen tool:\n\n1. `npm run start` in a terminal window to serve Open MCT locally\n2. `npx @playwright/test install` to install playwright and dependencies\n3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`\n4. Navigate the browser to `http://localhost:8080`\n5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector\n6. Continue through the steps 2-6 above\n\nWhat you've created is an automated test which mimics the creation of a mission control display.\n\nNext, you should walk through our implementation of Playwright in Open MCT:\n\n1. Close any terminals which are serving up a local instance of Open MCT\n2. Run our 'Getting Started' test in debug mode with `npm run test:e2e:local -- exampleTemplate --debug`\n3. Step through each test step in the Playwright Inspector to see how we leverage Playwright's capabilities to test Open MCT\n\n## Types of e2e Testing\n\ne2e testing describes the layer at which a test is performed without prescribing the assertions which are made. Generally, when writing an e2e test, we have five choices to make on an assertion strategy:\n\n1. Functional - Verifies the functional correctness of the application. Sometimes interchanged with e2e or regression testing.\n2. Visual - Verifies the \"look and feel\" of the application and can only detect _undesirable changes when compared to a previous baseline_.\n3. Snapshot - Similar to Visual in that it captures the \"look\" of the application and can only detect _undesirable changes when compared to a previous baseline_. **Generally not preferred due to advanced setup necessary.**\n4. Accessibility - Verifies that the application meets the accessibility standards defined by the [WCAG organization](https://www.w3.org/WAI/standards-guidelines/wcag/).\n5. Performance - Verifies that application provides a performant experience. Like Snapshot testing, these tests are generally not recommended due to their difficulty in providing a consistent result.\n\nWhen choosing between the different testing strategies, think only about the assertion that is made at the end of the series of test steps. \"I want to verify that the Timer plugin functions correctly\" vs \"I want to verify that the Timer plugin does not look different than originally designed\".\n\nWe do not want to interleave visual and functional testing inside the same suite because visual test verification of correctness must happen with a 3rd party service. This service is not available when executing these tests in other contexts (i.e. VIPER).\n\n### Functional Testing\n\nThe bulk of our e2e coverage lies in \"functional\" test coverage which verifies that Open MCT is functionally correct as well as defining _how we expect it to behave_. This enables us to test the application exactly as a user would, while prescribing exactly how a user can interact with the application via a web browser.\n\n### Visual Testing\n\nVisual Testing is an essential part of our e2e strategy as it ensures that the application _appears_ correctly to a user while it compliments the functional e2e suite. It would be impractical to make thousands of assertions functional assertions on the look and feel of the application. Visual testing is interested in getting the DOM into a specified state and then comparing that it has not changed against a baseline.\n\nFor a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)\nTo read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).\n\n`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.\n\n- `npm run test:e2e:visual:ci` will run against every commit and PR.\n- `npm run test:e2e:visual:full` will run every night with additional comparisons made for Larger Displays and with the `snow` theme.\n\n#### Percy.io\n\nTo make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics).\n\nAt present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.\n\n### Advanced: Snapshot Testing (Not Recommended)\n\nWhile snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.\n\n#### CI vs Manual Checks\n\nSnapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.\n\n#### Example\n\nA single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.\n\n#### Further Reading\n\nFor those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.\n\n#### Open MCT's implementation\n\n- Our Snapshot tests receive a `@snapshot` tag.\n- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:\n- Note, microsoft might have retired older images like `-jammy`. Run the following command to find the available tags:\n`curl -s https://mcr.microsoft.com/v2/playwright/tags/list | jq -r '.tags[]' | grep \"v{X.X.X}`\n\n```sh\n// Replace {X.X.X} with the current Playwright version \n// from our package.json configuration file\ndocker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-jammy /bin/bash\nnpm install\nnpm run test:e2e:checksnapshots\n```\n\n### Updating Snapshots\n\nWhen the `@snapshot` tests fail, they will need to be evaluated to determine if the failure is an acceptable and desirable or an unintended regression.\n\nTo compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.\n\n```sh\n// Replace {X.X.X} with the current Playwright version \n// from our package.json configuration file\ndocker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-jammy /bin/bash\nnpm install\nnpm run test:e2e:updatesnapshots\n```\n\nOnce that's done, you'll need to run the following to verify that the changes do not cause more problems:\n\n```sh\nnpm run test:e2e:checksnapshots\n```\n\n## Automated Accessibility (a11y) Testing\n\nOpen MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:\n\n1. **Usage of Playwright's Locator Strategy**: Open MCT utilizes Playwright's locator strategy, specifically the [page.getByRole('') function](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role), to ensure that web elements are accessible via assistive technologies. This approach focuses on the accessibility of elements rather than full adherence to a11y guidelines, which is covered in the second method.\n\n2. **Enforcing a11y Guidelines with Playwright Axe Plugin**: To rigorously enforce a11y guideline compliance, Open MCT employs the [playwright axe plugin](https://playwright.dev/docs/accessibility-testing). This is achieved through the `scanForA11yViolations` function within the visual testing suite. This method not only benefits from the existing coverage of the visual tests but also targets specific a11y issues, such as `color-contrast` violations, which are particularly pertinent in the context of visual testing.\n\n### a11y Standards (WCAG and Section 508)\n\nPlaywright axe supports a wide range of [WCAG Standards](https://playwright.dev/docs/accessibility-testing#scanning-for-wcag-violations) to test against. Open MCT is testing against the [Section 508](https://www.section508.gov/test/testing-overview/) accessibility guidelines with the intent to support higher standards over time. As of 2024, Section508 requirements now map completely to WCAG 2.0 AA. In the future, Section 508 requirements may map to WCAG 2.1 AA.\n\n### Reading an a11y test failure\n\nWhen an a11y test fails, the result must be interpreted in the html test report or the a11y report json artifact stored in the `/test-results/` folder. The json structure should be parsed for `\"violations\"` by `\"id\"` and identified `\"target\"`. Example provided for the 'color-contrast-enhanced' violation.\n\n```json\n  \"violations\": \n    {\n      \"id\": \"color-contrast-enhanced\",\n      \"impact\": \"serious\",\n      \"html\": \"<span class=\\\"label c-indicator__label\\\">0 Snapshots <button aria-label=\\\"Show Snapshots\\\">Show</button></span>\",\n        \"target\": [\n          \".s-status-off > .label.c-indicator__label\"\n        ],\n        \"failureSummary\": \"Fix any of the following:\\n  Element has insufficient color contrast of 6.51 (foreground color: #aaaaaa, background color: #262626, font size: 8.1pt (10.8px), font weight: normal). Expected contrast ratio of 7:1\"\n      }\n```\n\n## Performance Testing\n\nThe open source performance tests function in three ways which match their naming and folder structure:\n\n`tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.\n`tests/performance/contract/` -  These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.\n`tests/performance/memory/` -  These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.\n\nThese tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.\n\nIn addition to the explicit definition of performance tests, we also ensure that our test timeout timing is \"tight\" to catch performance regressions detectable by action timeouts. i.e. [Notebooks load much slower than they used to #6459](https://github.com/nasa/openmct/issues/6459)\n\n## Test Architecture and CI\n\n### Architecture\n\n### File Structure\n\nOur file structure follows the type of type of testing being exercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.\n\n|File Path|Description|\n|:-:|-|\n|`./helper`                    | Contains helper functions or scripts which are leveraged directly within the test suites (e.g.: non-default plugin scripts injected into the DOM)|\n|`./test-data`                 | Contains test data which is leveraged or generated in the functional, performance, or visual test suites (e.g.: localStorage data).|\n|`./tests/functional`          | The bulk of the tests are contained within this folder to verify the functionality of Open MCT.|\n|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|\n|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|\n|`./tests/framework/`          | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|\n|`./tests/performance/`        | Performance tests which should be run on every commit.|\n|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|\n|`./tests/performance/memory`  | A subset of performance tests which are designed to test for memory leaks.|\n|`./tests/visual-a11y/`             | Visual tests and accessibility tests.|\n|`./tests/visual-a11y/component/`             | Visual and accessibility tests which are only run against a single component.|\n|`./appActions.js`             | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|\n|`./baseFixture.js`            | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|\n\nOur functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.\n\n### Configuration\n\nWhere possible, we try to run Open MCT without modification or configuration change so that the Open MCT doesn't fail exclusively in \"test mode\" or in \"production mode\".\n\nOpen MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run\n\n|Config File|Description|\n|:-:|-|\n|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|\n|`./playwright-local.config.js` | Used when running locally|\n|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|\n|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|\n|`./playwright-visual-a11y.config.js` | Used to run the visual and a11y tests in CI or locally|\n\n#### Test Tags\n\nTest tags are a great way of organizing tests outside of a file structure. To learn more see the official documentation [here](https://playwright.dev/docs/test-annotations#tag-tests).\n\nCurrent list of test tags:\n\n|Test Tag|Description|\n|:-:|-|\n|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|\n|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|\n|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|\n|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|\n|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|\n|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|\n|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|\n|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.\n|`@framework` | A test for open mct e2e capabilities. This is primarily to ensure we don't break projects which depend on sourcing this project's fixtures like appActions.js.\n\n### Continuous Integration\n\nThe cheapest time to catch a bug is pre-merge. Unfortunately, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.\n\nWe leverage Github Actions / Workflows to execute tests as it gives us the ability to run against multiple operating systems with greater control over git event triggers (i.e. Run on a PR Comment event).\n\nOur CI environment consists of 3 main modes of operation:\n\n#### 1. Per-Commit Testing\n\nGitHub Actions\n\n- e2e tests against ubuntu and chrome\n- Performance tests against ubuntu and chrome\n- e2e tests are linted\n- Visual and a11y tests are run in a single resolution on the default `espresso` theme\n\n#### 2. Per-Merge Testing\n\nGithub Actions\n\n- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'\n- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'\n\n#### 3. Scheduled / Batch Testing\n\nNightly Testing in Github Action\n\n- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile\n- Performance tests against ubuntu and chrome\n- CouchDB suite\n- Visual and a11y Tests are run in the full profile\n\n#### Parallelism and Fast Feedback\n\nIn order to provide fast feedback in the Per-Commit context, we try to keep total test feedback at 5 minutes or less. That is to say, A developer should have a pass/fail result in under 5 minutes.\n\nPlaywright has native support for semi-intelligent sharding. Read about it [here](https://playwright.dev/docs/test-parallel#shard-tests-between-multiple-machines).\n\nWe will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.\n\nIn addition to the Parallelization of Test Runners (Sharding), we're also running four concurrent threads on every shard.\n\nSo for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.\n\nAt the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.\n\n### Cross-browser and Cross-operating system\n\n#### **What's supported:**\n\nWe are leveraging the `browserslist` project to declare our supported list of browsers. We support macOS, Windows, and ubuntu 20+.\n\n#### **Where it's tested:**\n\nWe lint on `browserslist` to ensure that we're not implementing deprecated browser APIs and are aware of browser API improvements over time.\n\nWe also have the need to execute our e2e tests across this published list of browsers. Our browsers and browser version matrix is found inside of our `./playwright-*.config.js`, but mostly follows in order of bleeding edge to stable:\n\n- `playwright-chromium channel:beta`\n  - A beta version of Chromium from official chromium channels. As close to the bleeding edge as we can get.\n- `playwright-chromium`\n  - A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.\n- `playwright-chrome`\n  - The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.\n- `playwright-firefox`\n  - Firefox Latest Stable. Modified slightly by the playwright team to support a CDP Shim.\n\nIn terms of operating system testing, we're only limited by what the CI providers are able to support. The bulk of our testing is performed on the official playwright container which is based on ubuntu. Github Actions allows us to use `windows-latest` and `mac-latest` and is run as needed.\n\n#### **Mobile**\n\nWe have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.\n\nIn general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.\n\nFor now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the\n\n```sh\nnpm run test:e2e:mobile\n```\n\ncommand.\n\n#### **Skipping or executing tests based on browser, os, and/os browser version:**\n\nConditionally skipping tests based on browser (**RECOMMENDED**):\n\n```js\ntest('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {\n  // eslint-disable-next-line playwright/no-skipped-test\n  test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');\n\n  // ...\n```\n\nConditionally skipping tests based on OS:\n\n```js\ntest('Can adjust image brightness/contrast by dragging the sliders', async ({ page }) => {\n  // eslint-disable-next-line playwright/no-skipped-test\n  test.skip(process.platform === 'darwin', 'This test needs to be updated to work with MacOS');\n\n  // ...\n```\n\nSkipping based on browser version (Rarely used): <https://github.com/microsoft/playwright/discussions/17318>\n\n## Test Design, Best Practices, and Tips & Tricks\n\n### Test Design\n\n#### Test as the User\n\nIn general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):\n\n> \"Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output.\"\n\nBy adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.\n\n#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)\n\n  1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.\n  1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.\n  1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.\n  1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.\n  1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.\n  \n#### How to make tests faster and more resilient to application changes\n  1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:\n\n  ```js\n    // You can capture the CreatedObjectInfo returned from this appAction:\n    const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });\n\n    // ...and use its `url` property to navigate directly to it later in the test:\n    await page.goto(clock.url);\n  ```\n\n  1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`\n    - Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.\n  1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.\n  This ensures that your changes will be picked up with large refactors.\n  1. Use [user-facing locators](https://playwright.dev/docs/best-practices#use-locators) (Now a eslint rule!)\n  \n  ```js\n  page.getByRole('button', { name: 'Create' } )\n  ```\n  Instead of \n  ```js\n  page.locator('.c-create-button')\n  ```\n  Note: `page.locator()` can be used in performance tests as xk6-browser does not yet support the new `page.getBy` pattern and css lookups can be [1.5x faster](https://serpapi.com/blog/css-selectors-faster-than-getbyrole-playwright/)\n\n##### Utilizing LocalStorage\n\n  1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.\n  1. To generate a localStorage state to be used in a test:\n    - Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:\n    ```js\n    // Save localStorage for future test execution\n    await context.storageState({\n      path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')\n    });\n    ```\n    - Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite):\n    ```js\n      const LOCALSTORAGE_PATH = path.resolve(\n        __dirname,\n        '../../../../test-data/display_layout_with_child_layouts.json'\n      );\n      test.use({\n        storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)\n      }); \n    ```\n\n### How to write a great test\n\n- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`\n- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.\n  - Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.\n- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:\n\n  ```js\n  // Fill the \"Notes\" section with information about the\n  // currently running test and its project.\n  const { testNotes } = page;\n  const notesInput = page.locator('form[name=\"mctForm\"] #notes-textarea');\n  await notesInput.fill(testNotes);\n  ```\n\n#### How to Write a Great Visual Test\n\n1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.\n\n2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.\n\n3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.\n\n4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.\n\n- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).\n- Use Open MCT's fixed-time mode unless explicitly testing realtime clock\n- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.\n- Avoid creating objects with a time component like timers and clocks.\n- Utilize the playwright clock() API. See @clock Annotations for examples.\n\n5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:\n    - `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`\n\n6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual-a11y/component/` folder and limit the scope of the comparison to that component. For instance:\n\n    ```js\n    await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {\n        scope: treePane\n    });\n    ```\n\n    - Note: The `scope` variable can be any valid CSS selector.\n\n7. **Write many `percySnapshot` commands in a single test**: In line with our approach to longer functional tests, we recommend that many test percySnapshots are taken in a single test. For instance:\n\n    ```js\n    //<Some interesting state>\n    await percySnapshot(page, `Before object expanded (theme: ${theme})`);\n    //<Click on object>\n    await percySnapshot(page, `object expanded (theme: ${theme})`);\n    //Select from object\n    await percySnapshot(page, `object selected (theme: ${theme})`)\n    ```\n8. **Use `networkidle` to wait for network requests to complete**: This is necessary to ensure that all network requests have completed before taking a snapshot. This ensures that icons are loaded and other assets are available. https://github.com/nasa/openmct/issues/7549\n\n#### How to write a great network test\n\n- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.\n- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.\n- Make sure to only mock requests which are relevant to the specific behavior being tested.\n- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.\n\nSome examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.\n\n### Best Practices\n\nFor now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.\n\nFor best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.\n\n### Tips & Tricks\n\nThe following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.\n\n- (Advanced) Overriding the Browser's Clock\nIt is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior -- i.e. Tree not rendering -- only use this sparingly. Use the `page.clock()` API as such:\n\n```js\nimport { test, expect } from '../../pluginFixtures.js';\n\ntest.describe('foo test suite @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    //Set clock time\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    //Navigate to page with new clock\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('bar here', async ({ page }) => {\n    /// ...\n  });\n  ```\n\n- Working with multiple pages\nThere are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.\n\n- Working with file downloads and JSON data\nOpen MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.\n\n```js\nconst [download] = await Promise.all([\n  page.waitForEvent('download'), // Waits for the download event\n  page.getByLabel('Export as JSON').click() // Triggers the download\n]);\n\n// Wait for the download process to complete\nconst path = await download.path();\n\n// Read the contents of the downloaded file using readFile from fs/promises\nconst fileContents = await fs.readFile(path, 'utf8');\nconst jsonData = JSON.parse(fileContents);\n\n// Use the function to retrieve the key\nconst key = getFirstKeyFromOpenMctJson(jsonData);\n\n// Verify the contents of the JSON file\nexpect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');\n```\n\n### Reporting\n\nTest Reporting is done through official Playwright reporters and the CI Systems which execute them.\n\nWe leverage the following official Playwright reporters:\n\n- HTML\n- junit\n- github annotations\n- Tracefile\n- Screenshots\n\nWhen running the tests locally with the `npm run test:e2e:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.\n\nWhen looking at the reports run in CI, you'll leverage this same HTML Report which is hosted in Github Actions as a build artifact.\n\n### e2e Code Coverage\n\nOur e2e code coverage is captured and combined with our unit test coverage. For more information, please see our [code coverage documentation](../TESTING.md)\n\n#### Generating e2e code coverage\n\nPlease read more about our code coverage [here](../TESTING.md#code-coverage)\n\n## Other\n\n### About e2e testing\n\ne2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions.\n\nHistorically, the abstraction necessary to replicate real user behavior meant that:\n\n- e2e tests were \"expensive\" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more efficiently.\n- e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser.\n- e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests.\n- e2e frameworks provided insufficient debug information on test failure\n\nHowever, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer \"trade-offs\" when choosing to write an e2e test over any other type of test.\n\nModern e2e frameworks:\n\n- Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state.\n- These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself.\n- Provide test debug tooling which enables developers to pinpoint failure\n\nFurthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.\n\nA single e2e test in Open MCT is extended to run:\n\n- Against a matrix of browser versions.\n- Against a matrix of OS platforms.\n- Against a local development version of Open MCT.\n- A version of Open MCT loaded as a dependency (VIPER, VISTA, etc)\n- Against a variety of data sources or telemetry endpoints.\n\n### Why Playwright?\n\n[Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs:\n\n1. First-class support for Automated Performance Testing\n2. Official Chrome, Chrome Canary, and iPad Capabilities\n3. Support for Browserless.io to run tests in a \"hermetically sealed\" environment\n4. Ability to generate code coverage reports\n\n### FAQ\n\n- How does this help NASA missions?\n- When should I write an e2e test instead of a unit test?\n- When should I write a functional vs visual test?\n- How is Open MCT extending default Playwright functionality?\n- What about Component Testing?\n\n### Writing Tests\n\nPlaywright provides 3 supported methods of debugging and authoring tests:\n\n- A 'watch mode' for running tests locally and debugging on the fly\n- A 'debug mode' for debugging tests and writing assertions against tests\n- A 'VSCode plugin' for debugging tests within the VSCode IDE.\n\nGenerally, we encourage folks to use the watch mode and provide a script `npm run test:e2e:watch` which launches the launch mode ui and enables hot reloading on the dev server.\n\n### e2e Troubleshooting\n\nPlease follow the general guide troubleshooting in [the general troubleshooting doc](../TESTING.md#troubleshooting-ci)\n\n- Tests won't start because 'Error: <http://localhost:8080/># is already used...'\nThis error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:\n```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```\n\n### Upgrading Playwright\n\nIn order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.\n\nFor reference, all of the locations where the version should be updated are listed below:\n\n#### **In `openmct`:**\n\n- `package.json`\n  - Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.\n- `.github/workflows/ci.yml`\n- `.github/workflows/e2e-couchdb.yml`\n- `.github/workflows/e2e-pr.yml`\n\n#### **In `openmct-yamcs`:**\n\n- `package.json`\n  - `@playwright/test` should be updated to the target version.\n- `.github/workflows/yamcs-quickstart-e2e.yml`\n"
  },
  {
    "path": "e2e/appActions.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * The fixtures in this file are to be used to consolidate common actions performed by the\n * various test suites. The goal is only to avoid duplication of code across test suites and not to abstract\n * away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md)\n *\n * For example, if two functions are nearly identical in\n * timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file.\n */\n\n/**\n * Defines parameters to be used in the creation of a domain object.\n * @typedef {Object} CreateObjectOptions\n * @property {string} type the type of domain object to create (e.g.: \"Sine Wave Generator\").\n * @property {string} [name] the desired name of the created domain object.\n * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.\n */\n\n/**\n * Contains information about the newly created domain object.\n * @typedef {Object} CreatedObjectInfo\n * @property {string} name the name of the created object\n * @property {string} uuid the uuid of the created object\n * @property {string} url the relative url to the object (for use with `page.goto()`)\n */\n\n/**\n * Defines parameters to be used in the creation of a notification.\n * @typedef {Object} CreateNotificationOptions\n * @property {string} message the message\n * @property {'info' | 'alert' | 'error'} severity the severity\n * @property {import('../src/api/notifications/NotificationAPI').NotificationOptions} [notificationOptions] additional options\n */\n\nimport { expect } from '@playwright/test';\nimport { Buffer } from 'buffer';\nimport { v4 as genUuid } from 'uuid';\n\n/**\n * This common function creates a domain object with the default options. It is the preferred way of creating objects\n * in the e2e suite when uninterested in properties of the objects themselves.\n *\n * @param {import('@playwright/test').Page} page - The Playwright page object.\n * @param {Object} options - Options for creating the domain object.\n * @param {string} options.type - The type of domain object to create (e.g., \"Sine Wave Generator\").\n * @param {string} [options.name] - The desired name of the created domain object.\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.\n */\nasync function createDomainObjectWithDefaults(\n  page,\n  { type, name, parent = 'mine' },\n  additionalOptions = {}\n) {\n  if (!name) {\n    name = `${type}:${genUuid()}`;\n  }\n\n  const parentUrl = await getHashUrlToDomainObject(page, parent);\n\n  // Navigate to the parent object. This is necessary to create the object\n  // in the correct location, such as a folder, layout, or plot.\n  await page.goto(parentUrl);\n\n  // Click the Create button\n  await page.getByRole('button', { name: 'Create', exact: true }).click();\n\n  // Click the object specified by 'type'-- case insensitive\n  await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();\n\n  // Fill in the name of the object\n  await page.getByLabel('Title', { exact: true }).fill('');\n  await page.getByLabel('Title', { exact: true }).fill(name);\n\n  if (additionalOptions) {\n    for (const [key, value] of Object.entries(additionalOptions)) {\n      // eslint-disable-next-line playwright/no-raw-locators\n      await page.locator(`#form-${key}`).fill(value);\n    }\n  }\n\n  if (page.testNotes) {\n    // Fill the \"Notes\" section with information about the\n    // currently running test and its project.\n    // eslint-disable-next-line playwright/no-raw-locators\n    await page.locator('#notes-textarea').fill(page.testNotes);\n  }\n\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Wait until the URL is updated\n  await page.waitForURL(`**/${parent}/*`);\n  const uuid = await getFocusedObjectUuid(page);\n  const objectUrl = await getHashUrlToDomainObject(page, uuid);\n\n  if (await _isInEditMode(page, uuid)) {\n    // Save (exit edit mode)\n    await page.getByRole('button', { name: 'Save', exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n  }\n\n  return {\n    name,\n    uuid,\n    url: objectUrl\n  };\n}\n\n/**\n * Generate a notification with the given options.\n * @param {import('@playwright/test').Page} page\n * @param {CreateNotificationOptions} createNotificationOptions\n */\nasync function createNotification(page, createNotificationOptions) {\n  await page.evaluate((_createNotificationOptions) => {\n    const { message, severity, options } = _createNotificationOptions;\n    const notificationApi = window.openmct.notifications;\n    if (severity === 'info') {\n      notificationApi.info(message, options);\n    } else if (severity === 'alert') {\n      notificationApi.alert(message, options);\n    } else {\n      notificationApi.error(message, options);\n    }\n  }, createNotificationOptions);\n}\n\n/**\n * Create a Plan object from JSON with the provided options. Must be used with a json based plan.\n * Please check appActions.e2e.spec.js for an example of how to use this function.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} name\n * @param {Object} json\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.\n */\nasync function createPlanFromJSON(page, { name, json, parent = 'mine' }) {\n  const parentUrl = await getHashUrlToDomainObject(page, parent);\n\n  // Navigate to the parent object. This is necessary to create the object\n  // in the correct location, such as a folder, layout, or plot.\n  await page.goto(`${parentUrl}`);\n\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  await page.getByRole('menuitem', { name: 'Plan' }).click();\n\n  // Fill in the name of the object or generate a random one\n  if (!name) {\n    name = `Plan:${genUuid()}`;\n  }\n  await page.getByLabel('Title', { exact: true }).fill('');\n  await page.getByLabel('Title', { exact: true }).fill(name);\n\n  // Upload buffer from memory\n  await page.getByLabel('Select File...').setInputFiles({\n    name: 'plan.txt',\n    mimeType: 'text/plain',\n    buffer: Buffer.from(JSON.stringify(json))\n  });\n\n  await page.getByLabel('Save').click();\n\n  // Wait until the URL is updated\n  await page.waitForURL(`**/${parent}/*`);\n  const uuid = await getFocusedObjectUuid(page);\n  const objectUrl = await getHashUrlToDomainObject(page, uuid);\n\n  return {\n    uuid,\n    name,\n    url: objectUrl\n  };\n}\n\n/**\n * Create a standardized Telemetry Object (Sine Wave Generator) for use in visual tests\n * and tests against plotting telemetry (e.g. logPlot tests).\n * @param {import('@playwright/test').Page} page\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.\n */\nasync function createExampleTelemetryObject(page, parent = 'mine') {\n  const parentUrl = await getHashUrlToDomainObject(page, parent);\n\n  await page.goto(`${parentUrl}`);\n\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();\n\n  const name = 'VIPER Rover Heading';\n  await page.getByLabel('Title', { exact: true }).fill(name);\n\n  // Fill out the fields with default values\n  await page.getByRole('spinbutton', { name: 'Period' }).fill('10');\n  await page.getByRole('spinbutton', { name: 'Amplitude' }).fill('1');\n  await page.getByRole('spinbutton', { name: 'Offset' }).fill('0');\n  await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('1');\n  await page.getByRole('spinbutton', { name: 'Phase (radians)' }).fill('0');\n  await page.getByRole('spinbutton', { name: 'Randomness' }).fill('0');\n  await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('0');\n\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Wait until the URL is updated\n  await page.waitForURL(`**/${parent}/*`);\n\n  const uuid = await getFocusedObjectUuid(page);\n  const url = await getHashUrlToDomainObject(page, uuid);\n\n  return {\n    name,\n    uuid,\n    url\n  };\n}\n\n/**\n * Create a Stable State Telemetry Object (State Generator) for use in visual tests\n * and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.\n * @param {import('@playwright/test').Page} page\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.\n */\nasync function createStableStateTelemetry(page, parent = 'mine') {\n  const parentUrl = await getHashUrlToDomainObject(page, parent);\n\n  await page.goto(`${parentUrl}`);\n  const createdObject = await createDomainObjectWithDefaults(page, {\n    type: 'State Generator',\n    name: 'Stable State Generator'\n  });\n  // edit the state generator to have a 1 second update rate\n  await page.getByLabel('More actions').click();\n  await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n  await page.getByLabel('State Duration (seconds)', { exact: true }).fill('2');\n  await page.getByLabel('Save').click();\n  // Wait until the URL is updated\n  const uuid = await getFocusedObjectUuid(page);\n  const url = await getHashUrlToDomainObject(page, uuid);\n\n  return {\n    name: createdObject.name,\n    uuid,\n    url\n  };\n}\n\n/**\n * Create a Out of order State Telemetry Object (State Generator) for use in visual tests\n * and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.\n * @param {import('@playwright/test').Page} page\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.\n */\nasync function createOutOfOrderStateTelemetry(page, parent = 'mine', duration = 0.25) {\n  const parentUrl = await getHashUrlToDomainObject(page, parent);\n\n  await page.goto(`${parentUrl}`);\n  const createdObject = await createDomainObjectWithDefaults(\n    page,\n    {\n      type: 'State Generator',\n      name: 'Stable State Generator'\n    },\n    { outOfOrder: true, duration: duration.toString() }\n  );\n  // Wait until the URL is updated\n  const uuid = await getFocusedObjectUuid(page);\n  const url = await getHashUrlToDomainObject(page, uuid);\n\n  return {\n    name: createdObject.name,\n    uuid,\n    url\n  };\n}\n\n/**\n * Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set\n * default view type.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url The url to the domainObject\n * @param {string | number} start The starting time bound in milliseconds since epoch\n * @param {string | number} end The ending time bound in milliseconds since epoch\n */\nasync function navigateToObjectWithFixedTimeBounds(page, url, start, end) {\n  await page.goto(\n    `${url}?tc.mode=fixed&tc.timeSystem=utc&tc.startBound=${start}&tc.endBound=${end}`\n  );\n}\n\n/**\n * Navigates directly to a given object url, in real-time mode. Note: does not set\n * default view type.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url The url to the domainObject\n * @param {string | number} start The start offset in milliseconds\n * @param {string | number} end The end offset in milliseconds\n */\nasync function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {\n  await page.goto(\n    `${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`\n  );\n}\n\n/**\n * Expands the entire object tree (every expandable tree item). Can be used to\n * ensure that the tree is fully expanded before performing actions on objects.\n * Can be applied to either the main tree or the create modal tree.\n *\n * @param {import('@playwright/test').Page} page\n * @param {\"Main Tree\" | \"Create Modal Tree\"} [treeName=\"Main Tree\"]\n */\nasync function expandEntireTree(page, treeName = 'Main Tree') {\n  const treeLocator = page.getByRole('tree', {\n    name: treeName\n  });\n  const collapsedTreeItems = treeLocator\n    .getByRole('treeitem', {\n      expanded: false\n    })\n    .getByLabel(/Expand/);\n\n  while ((await collapsedTreeItems.count()) > 0) {\n    //eslint-disable-next-line playwright/no-nth-methods\n    await collapsedTreeItems.nth(0).click();\n\n    // FIXME: Replace hard wait with something event-driven.\n    // Without the wait, this fails periodically due to a race condition\n    // with Vue rendering (loop exits prematurely).\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(200);\n  }\n}\n\n/**\n * Gets the UUID of the currently focused object by parsing the current URL\n * and returning the last UUID in the path.\n * @param {import('@playwright/test').Page} page\n * @returns {Promise<string>} the uuid of the focused object\n */\nasync function getFocusedObjectUuid(page) {\n  const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;\n  const focusedObjectUuid = await page.evaluate((regexp) => {\n    return window.location.href.split('?')[0].match(regexp).at(-1);\n  }, UUIDv4Regexp);\n\n  return focusedObjectUuid;\n}\n\n/**\n * Returns the hashUrl to the domainObject given its uuid.\n * Useful for directly navigating to the given domainObject.\n *\n * URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`\n *\n * @param {import('@playwright/test').Page} page\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier the uuid or identifier of the object to get the url for\n * @returns {Promise<string>} the url of the object\n */\nasync function getHashUrlToDomainObject(page, identifier) {\n  await page.waitForLoadState('domcontentloaded');\n  const hashUrl = await page.evaluate(async (objectIdentifier) => {\n    const path = await window.openmct.objects.getOriginalPath(objectIdentifier);\n    let url =\n      './#/browse/' +\n      [...path]\n        .reverse()\n        .map((object) => window.openmct.objects.makeKeyString(object.identifier))\n        .join('/');\n\n    // Drop the vestigial '/ROOT' if it exists\n    if (url.includes('/ROOT')) {\n      url = url.split('/ROOT').join('');\n    }\n\n    return url;\n  }, identifier);\n\n  return hashUrl;\n}\n\n/**\n * Utilizes the OpenMCT API to detect if the UI is in Edit mode.\n * @private\n * @param {import('@playwright/test').Page} page\n * @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier\n * @return {Promise<boolean>} true if the Open MCT is in Edit Mode\n */\nasync function _isInEditMode(page, identifier) {\n  // eslint-disable-next-line no-return-await\n  return await page.evaluate(() => window.openmct.editor.isEditing());\n}\n\n/**\n * Set the time conductor mode to either fixed timespan or realtime mode.\n * @private\n * @param {import('@playwright/test').Page} page\n * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true\n */\nasync function _setTimeConductorMode(page, isFixedTimespan = true) {\n  // Click 'mode' button\n  await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n  await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();\n  // Switch time conductor mode. Note, need to wait here for URL to update as the router is debounced.\n  if (isFixedTimespan) {\n    await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();\n    await page.waitForURL(/tc\\.mode=fixed/);\n  } else {\n    await page.getByRole('menuitem', { name: /Real-Time/ }).click();\n    await page.waitForURL(/tc\\.mode=local/);\n  }\n  //dismiss the time conductor popup\n  await page.getByLabel('Discard changes and close time popup').click();\n}\n\n/**\n * Set the time conductor to fixed timespan mode\n * @param {import('@playwright/test').Page} page\n */\nasync function setFixedTimeMode(page) {\n  await _setTimeConductorMode(page, true);\n}\n\n/**\n * Set the time conductor to realtime mode\n * @param {import('@playwright/test').Page} page\n */\nasync function setRealTimeMode(page) {\n  await _setTimeConductorMode(page, false);\n}\n\n/**\n * @typedef {Object} OffsetValues\n * @property {string | undefined} startHours\n * @property {string | undefined} startMins\n * @property {string | undefined} startSecs\n * @property {string | undefined} endHours\n * @property {string | undefined} endMins\n * @property {string | undefined} endSecs\n */\n\n/**\n * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode\n * @param {import('@playwright/test').Page} page\n * @param {OffsetValues} offset - Object containing offset values\n * @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them\n */\nasync function setTimeConductorOffset(\n  page,\n  { startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true }\n) {\n  if (startHours) {\n    await page.getByLabel('Start offset hours').fill(startHours);\n  }\n\n  if (startMins) {\n    await page.getByLabel('Start offset minutes').fill(startMins);\n  }\n\n  if (startSecs) {\n    await page.getByLabel('Start offset seconds').fill(startSecs);\n  }\n\n  if (endHours) {\n    await page.getByLabel('End offset hours').fill(endHours);\n  }\n\n  if (endMins) {\n    await page.getByLabel('End offset minutes').fill(endMins);\n  }\n\n  if (endSecs) {\n    await page.getByLabel('End offset seconds').fill(endSecs);\n  }\n\n  // Click the check button\n  if (submitChanges) {\n    await page.getByLabel('Submit time offsets').click();\n  } else {\n    await page.getByLabel('Discard changes and close time popup').click();\n  }\n}\n\n/**\n * Set the values (hours, mins, secs) for the start time offset when in realtime mode\n * @param {import('@playwright/test').Page} page\n * @param {OffsetValues} offset\n * @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them\n */\nasync function setStartOffset(page, { submitChanges = true, ...offset }) {\n  // Click 'mode' button\n  await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n  await setTimeConductorOffset(page, { submitChanges, ...offset });\n}\n\n/**\n * Set the values (hours, mins, secs) for the end time offset when in realtime mode\n * @param {import('@playwright/test').Page} page\n * @param {OffsetValues} offset\n * @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them\n */\nasync function setEndOffset(page, { submitChanges = true, ...offset }) {\n  // Click 'mode' button\n  await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n  await setTimeConductorOffset(page, { submitChanges, ...offset });\n}\n\n/**\n * Set the time conductor bounds in fixed time mode\n *\n * NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead\n * navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.\n * @param {import('@playwright/test').Page} page\n * @param {Object} bounds - The time conductor bounds\n * @param {string} [bounds.startDate] - The start date in YYYY-MM-DD format\n * @param {string} [bounds.startTime] - The start time in HH:mm:ss format\n * @param {string} [bounds.endDate] - The end date in YYYY-MM-DD format\n * @param {string} [bounds.endTime] - The end time in HH:mm:ss format\n * @param {boolean} [bounds.submitChanges=true] - If true, submit the changes; otherwise, discard them.\n */\nasync function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) {\n  const { startDate, endDate, startTime, endTime } = bounds;\n\n  // Open the time conductor popup\n  await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n  // FIXME: https://github.com/nasa/openmct/pull/7818\n  // eslint-disable-next-line playwright/no-wait-for-timeout\n  await page.waitForTimeout(500);\n\n  if (startDate) {\n    await page.getByLabel('Start date').fill(startDate);\n  }\n\n  if (startTime) {\n    await page.getByLabel('Start time').fill(startTime);\n  }\n\n  if (endDate) {\n    await page.getByLabel('End date').fill(endDate);\n  }\n\n  if (endTime) {\n    await page.getByLabel('End time').fill(endTime);\n  }\n\n  if (submitChanges) {\n    await page.getByLabel('Submit time bounds').click();\n  } else {\n    await page.getByLabel('Discard changes and close time popup').click();\n  }\n}\n\n/**\n * Set the bounds of the visible conductor in fixed time mode.\n * Requires that page already has an independent time conductor in view.\n * @param {import('@playwright/test').Page} page\n * @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format\n * @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format\n */\nasync function setFixedIndependentTimeConductorBounds(page, { start, end }) {\n  // Activate Independent Time Conductor\n  await page.getByLabel('Enable Independent Time Conductor').click();\n\n  // Bring up the time conductor popup\n  await page.getByLabel('Independent Time Conductor Panel').click();\n  await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();\n  await _setTimeBounds(page, start, end);\n\n  await page.keyboard.press('Enter');\n}\n\n/**\n * Set the bounds of the visible conductor in fixed time mode\n * @private\n * @param {import('@playwright/test').Page} page\n * @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format\n * @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format\n */\nasync function _setTimeBounds(page, startDate, endDate) {\n  if (startDate) {\n    // Fill start time\n    await page\n      .getByRole('textbox', { name: 'Start date' })\n      .fill(startDate.toString().substring(0, 10));\n    await page\n      .getByRole('textbox', { name: 'Start time' })\n      .fill(startDate.toString().substring(11, 19));\n  }\n\n  if (endDate) {\n    // Fill end time\n    await page.getByRole('textbox', { name: 'End date' }).fill(endDate.toString().substring(0, 10));\n    await page\n      .getByRole('textbox', { name: 'End time' })\n      .fill(endDate.toString().substring(11, 19));\n  }\n}\n\n/**\n * Waits and asserts that all plot series data on the page\n * is loaded and drawn.\n *\n * In lieu of a better way to detect when a plot is done rendering,\n * we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27)\n * once all pending series data has been loaded. The following appAction retrieves\n * all plots on the page and waits up to the default timeout for the class to be\n * attached to each plot.\n * @param {import('@playwright/test').Page} page\n * @param {number} [timeout] Provide a custom timeout in milliseconds to override the default timeout\n */\nasync function waitForPlotsToRender(page, { timeout } = {}) {\n  //eslint-disable-next-line playwright/no-raw-locators\n  const plotLocator = page.locator('.gl-plot');\n  for (const plot of await plotLocator.all()) {\n    await expect(plot).toHaveClass(/js-series-data-loaded/, { timeout });\n  }\n}\n\n/**\n * @typedef {Object} PlotPixel\n * @property {number} r The value of the red channel (0-255)\n * @property {number} g The value of the green channel (0-255)\n * @property {number} b The value of the blue channel (0-255)\n * @property {number} a The value of the alpha channel (0-255)\n * @property {string} strValue The rgba string value of the pixel\n */\n\n/**\n * Wait for all plots to render and then retrieve and return an array\n * of canvas plot pixel data (RGBA values).\n * @param {import('@playwright/test').Page} page\n * @param {string} canvasSelector The selector for the canvas element\n * @return {Promise<PlotPixel[]>}\n */\nasync function getCanvasPixels(page, canvasSelector) {\n  const canvasHandle = await page.evaluateHandle(\n    (canvas) => document.querySelector(canvas),\n    canvasSelector\n  );\n  const canvasContextHandle = await page.evaluateHandle(\n    (canvas) => canvas.getContext('2d'),\n    canvasHandle\n  );\n\n  await waitForPlotsToRender(page);\n  return page.evaluate(\n    ([canvas, ctx]) => {\n      // The document canvas is where the plot points and lines are drawn.\n      // The only way to access the canvas is using document (using page.evaluate)\n      /** @type {ImageData} */\n      const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;\n      /** @type {number[]} */\n      const imageDataValues = Object.values(data);\n      /** @type {PlotPixel[]} */\n      const plotPixels = [];\n      // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.\n      // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.\n      for (let i = 0; i < imageDataValues.length; ) {\n        if (imageDataValues[i] > 0) {\n          plotPixels.push({\n            r: imageDataValues[i],\n            g: imageDataValues[i + 1],\n            b: imageDataValues[i + 2],\n            a: imageDataValues[i + 3],\n            strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${\n              imageDataValues[i + 2]\n            }, ${imageDataValues[i + 3]})`\n          });\n        }\n\n        i = i + 4;\n      }\n\n      return plotPixels;\n    },\n    [canvasHandle, canvasContextHandle]\n  );\n}\n\n/**\n * Search for telemetry and link it to an object. objectName should come from the domainObject.name function.\n * @param {import('@playwright/test').Page} page\n * @param {string} parameterName\n * @param {string} objectName\n */\nasync function linkParameterToObject(page, parameterName, objectName) {\n  await page.getByRole('searchbox', { name: 'Search Input' }).click();\n  await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName);\n  await page.getByLabel('Object Results').getByText(parameterName).click();\n  await page.getByLabel('More actions').click();\n  await page.getByLabel('Create Link').click();\n  await page.getByLabel('Modal Overlay').getByLabel('Search Input').click();\n  await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName);\n  await page.getByLabel('Modal Overlay').getByLabel(`Navigate to ${objectName}`).click();\n  await page.getByLabel('Save').click();\n}\n\n/**\n * Rename the currently viewed `domainObject` from the browse bar.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} newName\n */\nasync function renameCurrentObjectFromBrowseBar(page, newName) {\n  const nameInput = page.getByLabel('Browse bar object name');\n  await nameInput.click();\n  await nameInput.fill('');\n  await nameInput.fill(newName);\n  // Click the browse bar container to save changes\n  await page.getByLabel('Browse bar', { exact: true }).click();\n}\n\n/**\n * Util for subscribing to a telemetry object by object identifier\n * Limitations: Currently only works to return telemetry once to the node scope\n * To Do: See if there's a way to await this multiple times to allow for multiple\n * values to be returned over time\n * @param {import('@playwright/test').Page} page\n * @param {string} objectIdentifier identifier for object\n * @returns {Promise<string>} the formatted sin telemetry value\n */\nasync function getNextSineValueFromSWG(page, objectIdentifier, returnOnlyValue = true) {\n  // Generate a unique function name for this subscription\n  const uniqueFunctionName = `getTelemValue_${genUuid().replace(/-/g, '_')}`;\n\n  const getTelemValuePromise = new Promise((resolve) =>\n    page.exposeFunction(uniqueFunctionName, resolve)\n  );\n\n  await page.evaluate(\n    async ({ telemetryIdentifier, functionName, onlyValue }) => {\n      const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);\n      const metadata = window.openmct.telemetry.getMetadata(telemetryObject);\n      const formats = await window.openmct.telemetry.getFormatMap(metadata);\n      window.openmct.telemetry.subscribe(telemetryObject, (obj) => {\n        const sinVal = obj.sin;\n        const formattedSinVal = formats.sin.format(sinVal);\n        const formattedTimestamp = formats.utc.format(obj.utc);\n        window[functionName](onlyValue ? formattedSinVal : { ...obj, formattedTimestamp });\n      });\n    },\n    {\n      telemetryIdentifier: objectIdentifier,\n      functionName: uniqueFunctionName,\n      onlyValue: returnOnlyValue\n    }\n  );\n\n  return getTelemValuePromise;\n}\n\nasync function expandInspectorPane(page) {\n  await page.getByRole('button', { name: 'Inspect' }).click();\n  // eslint-disable-next-line playwright/no-raw-locators\n  await expect(page.locator('.l-shell__pane-inspector > .l-pane__contents')).toHaveCSS(\n    'opacity',\n    '1'\n  );\n}\n\nasync function expandTreePane(page) {\n  await page.getByRole('button', { name: 'Browse' }).click();\n  // eslint-disable-next-line playwright/no-raw-locators\n  await expect(page.locator('.l-shell__pane-tree > .l-pane__contents')).toHaveCSS('opacity', '1');\n}\n\nexport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  createNotification,\n  createOutOfOrderStateTelemetry,\n  createPlanFromJSON,\n  createStableStateTelemetry,\n  expandEntireTree,\n  expandInspectorPane,\n  expandTreePane,\n  getCanvasPixels,\n  getNextSineValueFromSWG,\n  linkParameterToObject,\n  navigateToObjectWithFixedTimeBounds,\n  navigateToObjectWithRealTime,\n  renameCurrentObjectFromBrowseBar,\n  setEndOffset,\n  setFixedIndependentTimeConductorBounds,\n  setFixedTimeMode,\n  setRealTimeMode,\n  setStartOffset,\n  setTimeConductorBounds,\n  waitForPlotsToRender\n};\n"
  },
  {
    "path": "e2e/avpFixtures.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * avpFixtures.js\n *\n * @file This module provides custom fixtures specifically tailored for Accessibility, Visual, and Performance (AVP) tests.\n * These fixtures extend the base functionality of the Playwright fixtures and appActions, and are designed to be\n * generalized across all plugins. They offer functionalities like scanning for accessibility violations, integrating\n * with axe-core, and more.\n *\n * IMPORTANT NOTE: This fixture file is not intended to be extended further by other fixtures. If you find yourself\n * needing to do so, please consult the documentation and consider creating a specialized fixture or modifying the\n * existing ones.\n */\n\nimport AxeBuilder from '@axe-core/playwright';\nimport fs from 'fs/promises';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nimport { expect, test } from './pluginFixtures.js';\n// Constants for repeated values\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst TEST_RESULTS_DIR = path.join(__dirname, './test-results');\n\nconst extendedTest = test.extend({\n  /**\n   * Overrides the default screenshot function to apply default options that should apply to all\n   * screenshots taken in the AVP tests.\n   *\n   * @param {import('@playwright/test').PlaywrightTestArgs} args - The Playwright test arguments.\n   * @param {Function} use - The function to use the page object.\n   * Defaults:\n   * - Disables animations\n   * - Masks the clock indicator\n   * - Masks the time conductor last update time in realtime mode\n   * - Masks the time conductor start bounds in fixed mode\n   * - Masks the time conductor end bounds in fixed mode\n   */\n  page: async ({ page }, use) => {\n    const playwrightScreenshot = page.screenshot;\n\n    /**\n     * Override the screenshot function to always mask a given set of locators which will always\n     * show variance across screenshots. Defaults may be overridden by passing in options to the\n     * screenshot function.\n     * @param {import('@playwright/test').PageScreenshotOptions} options - The options for the screenshot.\n     * @returns {Promise<Buffer>} Returns the screenshot as a buffer.\n     */\n    page.screenshot = async function (options = {}) {\n      const mask = [\n        this.getByLabel('Clock Indicator'), // Mask the clock indicator\n        this.getByLabel('Last update'), // Mask the time conductor last update time in realtime mode\n        this.getByLabel('Start bounds'), // Mask the time conductor start bounds in fixed mode\n        this.getByLabel('End bounds') // Mask the time conductor end bounds in fixed mode\n      ];\n\n      const result = await playwrightScreenshot.call(this, {\n        animations: 'disabled',\n        mask,\n        ...options // Pass through or override any options\n      });\n      return result;\n    };\n\n    await use(page);\n  }\n});\n\n/**\n * Writes the accessibility report to the specified path.\n *\n * @param {string} reportPath - The path to write the report to.\n * @param {Object} accessibilityScanResults - The results of the accessibility scan.\n * @returns {Promise<Object>} The accessibility scan results.\n * @throws Will throw an error if writing the report fails.\n */\nasync function writeAccessibilityReport(reportPath, accessibilityScanResults) {\n  try {\n    await fs.mkdir(path.dirname(reportPath), { recursive: true });\n    const data = JSON.stringify(accessibilityScanResults, null, 2);\n    await fs.writeFile(reportPath, data);\n    console.log(`Accessibility report with violations saved successfully as ${reportPath}`);\n    return accessibilityScanResults;\n  } catch (err) {\n    console.error(`Error writing the accessibility report to file ${reportPath}:`, err);\n    throw err;\n  }\n}\n\n/**\n * Scans for accessibility violations on a page and writes a report to disk if violations are found.\n * Automatically asserts that no violations should be present.\n *\n * @param {import('playwright').Page} page - The page object from Playwright.\n * @param {string} testCaseName - The name of the test case.\n * @param {{ reportName?: string }} [options={}] - The options for the report generation.\n * @returns {Promise<Object|null>} Returns the accessibility scan results if violations are found, otherwise returns null.\n */\n\nexport async function scanForA11yViolations(page, testCaseName, options = {}) {\n  const builder = new AxeBuilder({ page });\n  builder.withTags(['wcag2aa']);\n  // https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md\n  const accessibilityScanResults = await builder.analyze();\n\n  // Assert that no violations should be present\n  expect\n    .soft(\n      accessibilityScanResults.violations,\n      `Accessibility violations found in test case: ${testCaseName}`\n    )\n    .toEqual([]);\n\n  // Check if there are any violations\n  if (accessibilityScanResults.violations.length > 0) {\n    const reportName = options.reportName || testCaseName;\n    const sanitizedReportName = reportName.replace(/\\//g, '_');\n    const reportPath = path.join(\n      TEST_RESULTS_DIR,\n      'a11y-json-reports',\n      `${sanitizedReportName}.json`\n    );\n\n    try {\n      await page.screenshot({\n        path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)\n      });\n\n      return await writeAccessibilityReport(reportPath, accessibilityScanResults);\n    } catch (err) {\n      console.error(`Error writing the accessibility report to file ${reportPath}:`, err);\n      throw err;\n    }\n  } else {\n    console.log('No accessibility violations found, no report generated.');\n    return null;\n  }\n}\n\nexport { expect, extendedTest as test };\n"
  },
  {
    "path": "e2e/baseFixtures.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This file is dedicated to extending the base functionality of the `@playwright/test` framework.\n * The functions in this file should be viewed as temporary or a shim to be removed as the RFEs in\n * the Playwright GitHub repo are implemented. Functions which serve those RFEs are marked with corresponding\n * GitHub issues.\n */\n\nimport { expect, request, test } from '@playwright/test';\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { v4 as uuid } from 'uuid';\n\n/**\n * Takes a `ConsoleMessage` and returns a formatted string. Used to enable console log error detection.\n * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}\n * @private\n * @param {import('@playwright/test').ConsoleMessage} msg\n * @returns {string} formatted string with message type, text, url, and line and column numbers\n */\nfunction _consoleMessageToString(msg) {\n  const { url, lineNumber, columnNumber } = msg.location();\n\n  return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`;\n}\n\n/**\n * Wait for all animations within the given element and subtrees to finish. Useful when\n * verifying that css transitions have completed.\n * @see {@link https://github.com/microsoft/playwright/issues/15660 Github RFE}\n * @param {import('@playwright/test').Locator} locator\n * @return {Promise<Animation[]>}\n */\nfunction waitForAnimations(locator) {\n  return locator.evaluate((element) =>\n    Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished))\n  );\n}\n\nconst istanbulCLIOutput = fileURLToPath(new URL('.nyc_output', import.meta.url));\n\nconst extendedTest = test.extend({\n  /**\n   * Path to output raw coverage files. Can be overridden in Playwright config file.\n   * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}\n   * @constant {string}\n   */\n\n  coveragePath: [istanbulCLIOutput, { option: true }],\n  /**\n   * Extends the base context class to add codecoverage shim.\n   * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}\n   */\n  context: async ({ context, coveragePath }, use) => {\n    await context.addInitScript(() =>\n      window.addEventListener('beforeunload', () =>\n        window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))\n      )\n    );\n    await fs.promises.mkdir(coveragePath, { recursive: true });\n    await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {\n      if (coverageJSON) {\n        fs.writeFileSync(\n          path.join(coveragePath, `playwright_coverage_${uuid()}.json`),\n          coverageJSON\n        );\n      }\n    });\n\n    await use(context);\n    for (const page of context.pages()) {\n      await page.evaluate(() => {\n        window.collectIstanbulCoverage(JSON.stringify(window.__coverage__));\n      });\n    }\n  },\n  /**\n   * If true, will assert against any console.error calls that occur during the test. Assertions occur\n   * during test teardown (after the test has completed).\n   *\n   * Default: `true`\n   */\n  failOnConsoleError: [true, { option: true }],\n  ignore404s: [[], { option: true }],\n  /**\n   * Extends the base page class to enable console log error detection.\n   * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}\n   */\n  page: async ({ page, failOnConsoleError, ignore404s }, use) => {\n    // Capture any console errors during test execution\n    let messages = [];\n    page.on('console', (msg) => messages.push(msg));\n\n    await use(page);\n\n    if (ignore404s.length > 0) {\n      messages = messages.filter((msg) => {\n        let keep = true;\n\n        if (msg.text().match(/404 \\((Object )?Not Found\\)/) !== null) {\n          keep = ignore404s.every((ignoreRule) => {\n            return msg.location().url.match(ignoreRule) === null;\n          });\n        }\n\n        return keep;\n      });\n    }\n\n    // Assert against console errors during teardown\n    if (failOnConsoleError) {\n      messages.forEach((msg) => {\n        // eslint-disable-next-line playwright/no-standalone-expect\n        expect\n          .soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)\n          .not.toEqual('error');\n      });\n    }\n  }\n});\n\nexport { expect, request, extendedTest as test, waitForAnimations };\n"
  },
  {
    "path": "e2e/constants.js",
    "content": "/**\n * Constants which may be used across all e2e tests.\n */\n\n/**\n * Time Constants\n * - Used for overriding the browser clock in tests.\n */\nexport const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time)\n// Subtracting 30 minutes from MISSION_TIME\nexport const MISSION_TIME_FIXED_START = 1732413600000 - 1800000; // 1732411800000\n\n// Adding 1 minute to MISSION_TIME\nexport const MISSION_TIME_FIXED_END = 1732413600000 + 60000; // 1732413660000\n/**\n * URL Constants\n * These constants are used for initial navigation in visual tests, in either fixed or realtime mode.\n * They navigate to the 'My Items' folder at MISSION_TIME.\n * They set the following url parameters:\n *  - tc.mode - The time conductor mode ('fixed' or 'local')\n *  - tc.startBound - The time conductor start bound (when in fixed mode)\n *  - tc.endBound - The time conductor end bound (when in fixed mode)\n *  - tc.startDelta - The time conductor start delta (when in realtime mode)\n *  - tc.endDelta - The time conductor end delta (when in realtime mode)\n *  - tc.timeSystem - The time conductor time system ('utc')\n *  - view - The view to display ('grid')\n *  - hideInspector - Whether to hide the inspector (true)\n *  - hideTree - Whether to hide the tree (true)\n * @typedef {string} VisualUrl\n */\n\n/** @type {VisualUrl} */\nexport const VISUAL_FIXED_URL = `./#/browse/mine?tc.mode=fixed&tc.startBound=${MISSION_TIME_FIXED_START}&tc.endBound=${MISSION_TIME_FIXED_END}&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true`;\n/** @type {VisualUrl} */\nexport const VISUAL_REALTIME_URL =\n  './#/browse/mine?tc.mode=local&tc.timeSystem=utc&view=grid&tc.startDelta=1800000&tc.endDelta=30000&hideTree=true&hideInspector=true';\n"
  },
  {
    "path": "e2e/helper/addInitDataVisualization.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example User\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.example.ExampleDataVisualizationSourcePlugin());\n  openmct.install(\n    openmct.plugins.InspectorDataVisualization({ type: 'exampleDataVisualizationSource' })\n  );\n});\n"
  },
  {
    "path": "e2e/helper/addInitDerivedTelemetryPlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Derived Telemetry Provider, this will also install the DerivedTelemetryPlugin (neither of which are installed by default).\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.DerivedTelemetry());\n});\n"
  },
  {
    "path": "e2e/helper/addInitExampleFaultProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.example.ExampleFaultSource());\n});\n"
  },
  {
    "path": "e2e/helper/addInitExampleFaultProviderStatic.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  const staticFaults = true;\n\n  openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults));\n});\n"
  },
  {
    "path": "e2e/helper/addInitExampleStalenessProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example Staleness Provider\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.example.ExampleStaleness({ stalenessInterval: 2500 }));\n});\n"
  },
  {
    "path": "e2e/helper/addInitExampleUser.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example User\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.example.ExampleUser());\n});\n"
  },
  {
    "path": "e2e/helper/addInitFaultManagementPlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.FaultManagement());\n});\n"
  },
  {
    "path": "e2e/helper/addInitFileInputObject.js",
    "content": "class DomainObjectViewProvider {\n  constructor(openmct) {\n    this.key = 'doViewProvider';\n    this.name = 'Domain Object View Provider';\n    this.openmct = openmct;\n  }\n\n  canView(domainObject) {\n    return domainObject.type === 'imageFileInput' || domainObject.type === 'jsonFileInput';\n  }\n\n  view(domainObject, objectPath) {\n    let content;\n\n    return {\n      show: function (element) {\n        const body = domainObject.selectFile.body;\n        const type = typeof body;\n\n        content = document.createElement('div');\n        content.id = 'file-input-type';\n        content.textContent = JSON.stringify(type);\n        element.appendChild(content);\n      },\n      destroy: function (element) {\n        element.removeChild(content);\n        content = undefined;\n      }\n    };\n  }\n}\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n\n  openmct.types.addType('jsonFileInput', {\n    key: 'jsonFileInput',\n    name: 'JSON File Input Object',\n    creatable: true,\n    form: [\n      {\n        name: 'Upload File',\n        key: 'selectFile',\n        control: 'file-input',\n        required: true,\n        text: 'Select File...',\n        type: 'application/json',\n        property: ['selectFile']\n      }\n    ]\n  });\n\n  openmct.types.addType('imageFileInput', {\n    key: 'imageFileInput',\n    name: 'Image File Input Object',\n    creatable: true,\n    form: [\n      {\n        name: 'Upload File',\n        key: 'selectFile',\n        control: 'file-input',\n        required: true,\n        text: 'Select File...',\n        type: 'image/*',\n        property: ['selectFile']\n      }\n    ]\n  });\n\n  openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct));\n});\n"
  },
  {
    "path": "e2e/helper/addInitNotebookWithUrls.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the re-instal default Notebook plugin with a simple url whitelist.\n// e.g.\n// await page.addInitScript({ path: path.join(__dirname, 'addInitNotebookWithUrls.js') });\nconst NOTEBOOK_NAME = 'Notebook';\nconst URL_WHITELIST = ['google.com'];\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST));\n});\n"
  },
  {
    "path": "e2e/helper/addInitOperatorStatus.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Operator Status\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.OperatorStatus());\n});\n"
  },
  {
    "path": "e2e/helper/addInitRestrictedNotebook.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the non-default Restricted Notebook plugin since it is not installed by default.\n// e.g.\n// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));\n});\n"
  },
  {
    "path": "e2e/helper/addNoneditableObject.js",
    "content": "(function () {\n  document.addEventListener('DOMContentLoaded', () => {\n    const PERSISTENCE_KEY = 'persistence-tests';\n    const openmct = window.openmct;\n\n    openmct.objects.addRoot({\n      namespace: PERSISTENCE_KEY,\n      key: PERSISTENCE_KEY\n    });\n\n    openmct.objects.addProvider(PERSISTENCE_KEY, {\n      get(identifier) {\n        if (identifier.key !== PERSISTENCE_KEY) {\n          return undefined;\n        } else {\n          return Promise.resolve({\n            identifier,\n            type: 'folder',\n            name: 'Persistence Testing',\n            location: 'ROOT',\n            composition: []\n          });\n        }\n      }\n    });\n  });\n})();\n"
  },
  {
    "path": "e2e/helper/faultUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { fileURLToPath } from 'url';\n\nimport { expect } from '../pluginFixtures.js';\n\n/**\n * @param {import('@playwright/test').Page} page\n * @returns {Promise<void>}\n */\nexport async function navigateToFaultManagementWithExample(page) {\n  await page.addInitScript({\n    path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))\n  });\n\n  await navigateToFaultItemInTree(page);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @returns {Promise<void>}\n */\nexport async function navigateToFaultManagementWithStaticExample(page) {\n  await page.addInitScript({\n    path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))\n  });\n\n  await navigateToFaultItemInTree(page);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @returns {Promise<void>}\n */\nexport async function navigateToFaultManagementWithoutExample(page) {\n  await page.addInitScript({\n    path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))\n  });\n\n  await navigateToFaultItemInTree(page);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @returns {Promise<void>}\n */\nasync function navigateToFaultItemInTree(page) {\n  await page.goto('./', { waitUntil: 'domcontentloaded' });\n  await page.waitForURL('**/#/browse/mine?**');\n\n  const faultManagementTreeItem = page\n    .getByRole('tree', {\n      name: 'Main Tree'\n    })\n    .getByRole('treeitem', {\n      name: 'Fault Management'\n    });\n\n  // Navigate to \"Fault Management\" from the tree\n  await faultManagementTreeItem.click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<void>}\n */\nexport async function acknowledgeFault(page, rowNumber) {\n  await openFaultRowMenu(page, rowNumber);\n  await page.getByLabel('Acknowledge', { exact: true }).click();\n  await page.getByLabel('Save').click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {...number} nums\n * @returns {Promise<void>}\n */\nexport async function shelveMultipleFaults(page, ...nums) {\n  const selectRows = nums.map((num) => {\n    return selectFaultItem(page, num);\n  });\n  await Promise.all(selectRows);\n\n  await page.getByLabel('Shelve selected faults').click();\n  await page.getByLabel('Save').click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {...number} nums\n * @returns {Promise<void>}\n */\nexport async function acknowledgeMultipleFaults(page, ...nums) {\n  const selectRows = nums.map((num) => {\n    return selectFaultItem(page, num);\n  });\n  await Promise.all(selectRows);\n\n  await page.getByLabel('Acknowledge selected faults').click();\n  await page.getByLabel('Save').click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<void>}\n */\nexport async function shelveFault(page, rowNumber) {\n  await openFaultRowMenu(page, rowNumber);\n  await page.getByLabel('Shelve', { exact: true }).click();\n  await page.getByLabel('Save').click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {'unacknowledged-first' | 'severity' | 'newest-first' | 'oldest-first'} sort\n * @returns {Promise<void>}\n */\nexport async function sortFaultsBy(page, sort) {\n  await page.getByTitle('Sort By').getByRole('combobox').selectOption(sort);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {'acknowledged' | 'shelved' | 'standard view'} view\n * @returns {Promise<void>}\n */\nexport async function changeViewTo(page, view) {\n  await page.getByTitle('View Filter').getByRole('combobox').selectOption(view);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<void>}\n */\nexport async function selectFaultItem(page, rowNumber) {\n  await page\n    .getByLabel('Select fault')\n    .nth(rowNumber - 1)\n    .check({\n      // Need force here because checkbox state is changed by an event emitted by the checkbox\n      // eslint-disable-next-line playwright/no-force-option\n      force: true\n    });\n  await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {import('@playwright/test').Locator}\n */\nexport function getFault(page, rowNumber) {\n  const fault = page.getByLabel('Fault triggered at').nth(rowNumber - 1);\n\n  return fault;\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {string} name\n * @returns {import('@playwright/test').Locator}\n */\nexport function getFaultByName(page, name) {\n  const fault = page.getByLabel('Fault triggered at').filter({\n    hasText: name\n  });\n\n  return fault;\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<string>}\n */\nexport async function getFaultName(page, rowNumber) {\n  const faultName = await page\n    .getByLabel('Fault name', { exact: true })\n    .nth(rowNumber - 1)\n    .textContent();\n\n  return faultName;\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<string>}\n */\nexport async function getFaultNamespace(page, rowNumber) {\n  const faultNamespace = await page\n    .getByLabel('Fault namespace')\n    .nth(rowNumber - 1)\n    .textContent();\n\n  return faultNamespace;\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<string>}\n */\nexport async function getFaultTriggerTime(page, rowNumber) {\n  const faultTriggerTime = await page\n    .getByLabel('Last Trigger Time')\n    .nth(rowNumber - 1)\n    .textContent();\n\n  return faultTriggerTime.toString().trim();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {number} rowNumber\n * @returns {Promise<void>}\n */\nexport async function openFaultRowMenu(page, rowNumber) {\n  // select\n  await page\n    .getByLabel('Fault triggered at')\n    .nth(rowNumber - 1)\n    .getByLabel('Disposition Actions')\n    .click();\n}\n"
  },
  {
    "path": "e2e/helper/hotkeys/clipboard.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst isMac = process.platform === 'darwin';\nconst modifier = isMac ? 'Meta' : 'Control';\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function selectAll(page) {\n  await page.keyboard.press(`${modifier}+KeyA`);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function copy(page) {\n  await page.keyboard.press(`${modifier}+KeyC`);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function paste(page) {\n  await page.keyboard.press(`${modifier}+KeyV`);\n}\n\nexport { copy, paste, selectAll };\n"
  },
  {
    "path": "e2e/helper/hotkeys/hotkeys.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport * from './clipboard.js';\n"
  },
  {
    "path": "e2e/helper/imageryUtils.js",
    "content": "import { createDomainObjectWithDefaults } from '../appActions.js';\nimport { expect } from '../pluginFixtures.js';\n\nconst IMAGE_LOAD_DELAY = 5 * 1000;\nconst FIVE_MINUTES = 1000 * 60 * 5;\nconst THIRTY_SECONDS = 1000 * 30;\nconst MOUSE_WHEEL_DELTA_Y = 120;\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function createImageryViewWithShortDelay(page, { name, parent }) {\n  await createDomainObjectWithDefaults(page, {\n    name,\n    type: 'Example Imagery',\n    parent\n  });\n\n  await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');\n  await page.getByLabel('More actions').click();\n  await page.getByLabel('Edit Properties').click();\n  // Clear and set Image load delay to minimum value\n  await page.locator('input[type=\"number\"]').fill(`${IMAGE_LOAD_DELAY}`);\n  await page.getByLabel('Save').click();\n}\n\nexport {\n  createImageryViewWithShortDelay,\n  FIVE_MINUTES,\n  IMAGE_LOAD_DELAY,\n  MOUSE_WHEEL_DELTA_Y,\n  THIRTY_SECONDS\n};\n"
  },
  {
    "path": "e2e/helper/notebookUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../appActions.js';\n\nconst NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';\nconst CUSTOM_NAME = 'CUSTOM_NAME';\nimport { fileURLToPath } from 'url';\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {string} text\n */\nasync function enterTextEntry(page, text) {\n  await addNotebookEntry(page);\n  await enterTextInLastEntry(page, text);\n  await commitEntry(page);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function addNotebookEntry(page) {\n  await page.locator(NOTEBOOK_DROP_AREA).click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function enterTextInLastEntry(page, text) {\n  await page.getByLabel('Notebook Entry Input').last().fill(text);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function dragAndDropEmbed(page, notebookObject) {\n  // Create example telemetry object\n  const swg = await createDomainObjectWithDefaults(page, {\n    type: 'Sine Wave Generator'\n  });\n  // Navigate to notebook\n  await page.goto(notebookObject.url);\n  // Expand the tree to reveal the notebook\n  await page.getByLabel('Show selected item in tree').click();\n  // Drag and drop the SWG into the notebook\n  await page.getByLabel(`Navigate to ${swg.name}`).dragTo(page.locator(NOTEBOOK_DROP_AREA));\n  await commitEntry(page);\n}\n\n/**\n * @private\n * @param {import('@playwright/test').Page} page\n */\nasync function commitEntry(page) {\n  //Click the Commit Entry button\n  await page.locator('.c-ne__save-button > button').click();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function startAndAddRestrictedNotebookObject(page) {\n  await page.addInitScript({\n    path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))\n  });\n  await page.goto('./', { waitUntil: 'domcontentloaded' });\n  await page.waitForURL('**/browse/mine?**');\n\n  return createDomainObjectWithDefaults(page, {\n    type: CUSTOM_NAME,\n    name: 'Restricted Test Notebook'\n  });\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function lockPage(page) {\n  // Click the Commit Entries button\n  await page.getByLabel('Commit Entries').click();\n  // Wait until Lock Banner is visible\n  await page.locator('text=Lock Page').click();\n}\n\n/**\n * Creates a notebook object and adds an entry.\n * @param {import('@playwright/test').Page} - page to load\n * @param {number} [iterations = 1] - the number of entries to create\n */\nasync function createNotebookAndEntry(page, iterations = 1) {\n  const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });\n\n  for (let iteration = 0; iteration < iterations; iteration++) {\n    await enterTextEntry(page, `Entry ${iteration}`);\n  }\n\n  return notebook;\n}\n\n/**\n * Creates a notebook object, adds an entry, and adds a tag.\n * @param {import('@playwright/test').Page} page\n * @param {number} [iterations = 1] - the number of entries (and tags) to create\n */\nasync function createNotebookEntryAndTags(page, iterations = 1) {\n  const notebook = await createNotebookAndEntry(page, iterations);\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  for (let iteration = 0; iteration < iterations; iteration++) {\n    // Hover and click \"Add Tag\" button\n    // Hover is needed here to \"slow down\" the actions while running in headless mode\n    await page.locator(`[aria-label=\"Notebook Entry\"] >> nth = ${iteration}`).click();\n    await page.hover(`button:has-text(\"Add Tag\")`);\n    await page.locator(`button:has-text(\"Add Tag\")`).click();\n\n    // Click inside the tag search input\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n    // Select the \"Driving\" tag\n    await page.locator('[aria-label=\"Autocomplete Options\"] >> text=Driving').click();\n\n    // Hover and click \"Add Tag\" button\n    // Hover is needed here to \"slow down\" the actions while running in headless mode\n    await page.hover(`button:has-text(\"Add Tag\")`);\n    await page.locator(`button:has-text(\"Add Tag\")`).click();\n    // Click inside the tag search input\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n    // Select the \"Science\" tag\n    await page.locator('[aria-label=\"Autocomplete Options\"] >> text=Science').click();\n  }\n\n  return notebook;\n}\n\nexport {\n  addNotebookEntry,\n  commitEntry,\n  createNotebookAndEntry,\n  createNotebookEntryAndTags,\n  dragAndDropEmbed,\n  enterTextEntry,\n  enterTextInLastEntry,\n  lockPage,\n  startAndAddRestrictedNotebookObject\n};\n"
  },
  {
    "path": "e2e/helper/planningUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';\nimport { expect } from '../pluginFixtures.js';\n\n/**\n * Asserts that the number of activities in the plan view matches the number of\n * activities in the plan data within the specified time bounds. Performs an assertion\n * for each activity in the plan data per group, using the earliest activity's\n * start time as the start bound and the current activity's end time as the end bound.\n * @param {import('@playwright/test').Page} page the page\n * @param {Object} plan The raw plan json to assert against\n * @param {string} planObjectUrl The URL of the object to assert against (plan or gantt chart)\n */\nexport async function assertPlanActivities(page, plan, planObjectUrl) {\n  const groups = Object.keys(plan);\n  for (const group of groups) {\n    for (let i = 0; i < plan[group].length; i++) {\n      // Set the startBound to the start time of the first activity in the group\n      const startBound = plan[group][0].start;\n      // Set the endBound to the end time of the current activity\n      let endBound = plan[group][i].end;\n      if (endBound === startBound) {\n        // Prevent oddities with setting start and end bound equal\n        // via URL params\n        endBound += 1;\n      }\n\n      // Switch to fixed time mode with all plan events within the bounds\n      await page.goto(\n        `${planObjectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`\n      );\n\n      // Assert that the number of activities in the plan view matches the number of\n      // activities in the plan data within the specified time bounds\n      await expect(page.locator('.activity-bounds')).toHaveCount(\n        Object.values(plan)\n          .flat()\n          .filter((event) =>\n            activitiesWithinTimeBounds(event.start, event.end, startBound, endBound)\n          ).length\n      );\n    }\n  }\n}\n\n/**\n * Returns true if the activities time bounds overlap, false otherwise.\n * @param {number} start1 the start time of the first activity\n * @param {number} end1 the end time of the first activity\n * @param {number} start2 the start time of the second activity\n * @param {number} end2 the end time of the second activity\n * @returns {boolean} true if the activities overlap, false otherwise\n */\nfunction activitiesWithinTimeBounds(start1, end1, start2, end2) {\n  return (\n    (start1 >= start2 && start1 <= end2) ||\n    (end1 >= start2 && end1 <= end2) ||\n    (start2 >= start1 && start2 <= end1) ||\n    (end2 >= start1 && end2 <= end1)\n  );\n}\n\n/**\n * Asserts that the swim lanes / groups in the plan view matches the order of\n * groups in the plan data.\n * @param {import('@playwright/test').Page} page the page\n * @param {Object} plan The raw plan json to assert against\n * @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)\n */\nexport async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {\n  // Switch to the plan view\n  await page.goto(`${objectUrl}?view=plan.view`);\n  const planGroups = await page\n    .locator('.c-plan__contents > div > .c-swimlane__lane-label .c-object-label__name')\n    .all();\n\n  const groups = plan.Groups;\n\n  for (let i = 0; i < groups.length; i++) {\n    // Assert that the order of groups in the plan view matches the order of\n    // groups in the plan data\n    const groupName = planGroups[i];\n    await expect(groupName).toHaveText(groups[i].name);\n  }\n}\n\n/**\n * Navigate to the plan view, switch to fixed time mode,\n * and set the bounds to span all activities.\n * @param {import('@playwright/test').Page} page\n * @param {Object} planJson\n * @param {string} planObjectUrl\n */\nexport async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) {\n  // Get the earliest start value\n  const start = getEarliestStartTime(planJson);\n  // Get the latest end value\n  const end = getLatestEndTime(planJson);\n  // Set the start and end bounds to the earliest start and latest end\n  await page.goto(\n    `${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`\n  );\n}\n\n/**\n * @param {Object} planJson\n * @returns {number}\n */\nexport function getEarliestStartTime(planJson) {\n  const activities = Object.values(planJson).flat();\n\n  return Math.min(...activities.map((activity) => activity.start));\n}\n\n/**\n *\n * @param {Object} planJson\n * @returns {number}\n */\nexport function getLatestEndTime(planJson) {\n  const activities = Object.values(planJson).flat();\n\n  return Math.max(...activities.map((activity) => activity.end));\n}\n\n/**\n *\n * @param {object} planJson\n * @returns {object}\n */\nexport function getFirstActivity(planJson) {\n  const groups = Object.keys(planJson);\n  const firstGroupKey = groups[0];\n  const firstGroupItems = planJson[firstGroupKey];\n\n  return firstGroupItems[0];\n}\n\n/**\n * Uses the Open MCT API to set the status of a plan to 'draft'.\n * @param {import('@playwright/test').Page} page\n * @param {import('../../appActions').CreatedObjectInfo} plan\n */\nexport async function setDraftStatusForPlan(page, plan) {\n  await page.evaluate(async (planObject) => {\n    await window.openmct.status.set(planObject.uuid, 'draft');\n  }, plan);\n}\n\nexport async function addPlanGetInterceptor(page) {\n  await page.waitForLoadState('load');\n  await page.evaluate(async () => {\n    await window.openmct.objects.addGetInterceptor({\n      appliesTo: (identifier, domainObject) => {\n        return domainObject && domainObject.type === 'plan';\n      },\n      invoke: (identifier, object) => {\n        if (object) {\n          object.sourceMap = {\n            orderedGroups: 'Groups'\n          };\n        }\n\n        return object;\n      }\n    });\n  });\n}\n\n/**\n * Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view\n * @param {import('@playwright/test').Page} page\n */\nexport async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {\n  await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n  const timelist = await createDomainObjectWithDefaults(page, {\n    name: 'Time List',\n    type: 'Time List'\n  });\n\n  await createPlanFromJSON(page, {\n    name: 'Test Plan',\n    json: planJson,\n    parent: timelist.uuid\n  });\n\n  // Ensure that all activities are shown in the expanded view\n  const groups = Object.keys(planJson);\n  const firstGroupKey = groups[0];\n  const firstGroupItems = planJson[firstGroupKey];\n  const firstActivityForPlan = firstGroupItems[0];\n  const lastActivity = firstGroupItems[firstGroupItems.length - 1];\n  const startBound = firstActivityForPlan.start;\n  const endBound = lastActivity.end;\n\n  // Switch to fixed time mode with all plan events within the bounds\n  await page.goto(\n    `${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`\n  );\n\n  // Change the object to edit mode\n  await page.getByRole('button', { name: 'Edit Object' }).click();\n\n  // Find the display properties section in the inspector\n  await page.getByRole('tab', { name: 'Config' }).click();\n  // Switch to expanded view and save the setting\n  await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });\n\n  // Click on the \"Save\" button\n  await page.getByRole('button', { name: 'Save' }).click();\n  await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n  const anActivity = page.getByRole('row').nth(0);\n\n  // Set the activity to in progress\n  await anActivity.click();\n  await page.getByRole('tab', { name: 'Activity' }).click();\n  await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });\n}\n"
  },
  {
    "path": "e2e/helper/plotTagsUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { waitForPlotsToRender } from '../appActions.js';\nimport { expect } from '../pluginFixtures.js';\n\n/**\n * Given a canvas and a set of points, tags the points on the canvas.\n * @param {import('@playwright/test').Page} page\n * @param {HTMLCanvasElement} canvas a telemetry item with a plot\n * @param {number} xEnd a telemetry item with a plot\n * @param {number} yEnd a telemetry item with a plot\n * @returns {Promise}\n */\nexport async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {\n  await canvas.hover({ trial: true });\n\n  //Alt+Shift Drag Start to select some points to tag\n  await page.keyboard.down('Alt');\n  await page.keyboard.down('Shift');\n\n  await canvas.dragTo(canvas, {\n    sourcePosition: {\n      x: 1,\n      y: 1\n    },\n    targetPosition: {\n      x: xEnd,\n      y: yEnd\n    }\n  });\n\n  //Alt Drag End\n  await page.keyboard.up('Alt');\n  await page.keyboard.up('Shift');\n\n  //Wait for canvas to stabilize.\n  await canvas.hover({ trial: true });\n\n  // add some tags\n  await page.getByText('Annotations').click();\n  await page.getByRole('button', { name: /Add Tag/ }).click();\n  await page.getByPlaceholder('Type to select tag').click();\n  await page.getByText('Driving').click();\n\n  await page.getByRole('button', { name: /Add Tag/ }).click();\n  await page.getByPlaceholder('Type to select tag').click();\n  await page.getByText('Science').click();\n}\n\n/**\n * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.\n * @param {import('@playwright/test').Page} page\n * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot\n * @returns {Promise}\n */\nexport async function testTelemetryItem(page, telemetryItem) {\n  // Check that telemetry item also received the tag\n  await page.goto(telemetryItem.url);\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  await expect(page.getByText('No tags to display for this item')).toBeVisible();\n\n  const canvas = page.locator('canvas').nth(1);\n  //Wait for canvas to stabilize.\n  await waitForPlotsToRender(page);\n\n  await expect(canvas).toBeInViewport();\n  await canvas.hover({ trial: true });\n\n  // click on the tagged plot point\n  await canvas.click({\n    position: {\n      x: 100,\n      y: 100\n    }\n  });\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  await expect(page.getByText('Science')).toBeVisible();\n  await expect(page.getByText('Driving')).toBeHidden();\n}\n\n/**\n * Given a page, tests that tags are searchable, deletable, and persist across reloads.\n * @param {import('@playwright/test').Page} page\n * @returns {Promise}\n */\nexport async function basicTagsTests(page) {\n  // Search for Driving\n  await page.getByRole('searchbox', { name: 'Search Input' }).click();\n\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  // Clicking elsewhere should cause annotation selection to be cleared\n  await expect(page.getByText('No tags to display for this item')).toBeVisible();\n  //\n  await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');\n\n  // Always click on the first Sine Wave result\n  await page\n    .getByLabel('Search Result')\n    .getByText(/Sine Wave/)\n    .first()\n    .click();\n\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  // Delete Driving Tag\n  await page.hover('[aria-label=\"Tag\"]:has-text(\"Driving\")');\n  await page.locator('[aria-label=\"Remove tag Driving\"]').click();\n\n  // Search for Science Tag\n  await page.getByRole('searchbox', { name: 'Search Input' }).click();\n  await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');\n\n  //Expect Science Tag to be present and and Driving Tags to be deleted\n  await expect(page.getByLabel('Search Result').first()).toContainText('Science');\n  await expect(page.getByLabel('Search Result').first()).not.toContainText('Driving');\n\n  // Search for Driving Tag and expect nothing found\n  await page.getByRole('searchbox', { name: 'Search Input' }).click();\n  await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');\n  await expect(page.getByText('No results found')).toBeVisible();\n\n  await page.reload({ waitUntil: 'domcontentloaded' });\n\n  await waitForPlotsToRender(page);\n\n  //Navigate to the Inspector and check that all tags have been removed\n  await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n  await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);\n  await expect(page.getByText('No tags to display for this item')).toBeVisible();\n\n  const canvas = page.locator('canvas').nth(1);\n  // click on the tagged plot point\n  await canvas.click({\n    position: {\n      x: 100,\n      y: 100\n    }\n  });\n\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n\n  //Expect Science to be visible but Driving to be hidden\n  await expect(page.getByText('Science')).toBeVisible();\n  await expect(page.getByText('Driving')).toBeHidden();\n\n  //Click elsewhere\n  await page.locator('body').click();\n  //Click on tagged plot point again\n  await canvas.click({\n    position: {\n      x: 100,\n      y: 100\n    }\n  });\n\n  // Add Driving Tag again\n  await page.getByRole('tab', { name: 'Annotations' }).click();\n  await page.getByRole('button', { name: /Add Tag/ }).click();\n  await page.getByPlaceholder('Type to select tag').click();\n  await page.getByText('Driving').click();\n\n  //Science and Driving Tags should be visible\n  await expect(page.getByText('Science')).toBeVisible();\n  await expect(page.getByText('Driving')).toBeVisible();\n\n  // Delete Driving Tag again\n  await page.hover('[aria-label=\"Tag\"]:has-text(\"Driving\")');\n  await page.locator('[aria-label=\"Remove tag Driving\"]').click();\n\n  //Science Tag should be visible and Driving Tag should be hidden\n  await expect(page.getByText('Science')).toBeVisible();\n  await expect(page.getByText('Driving')).toBeHidden();\n}\n"
  },
  {
    "path": "e2e/helper/stylingUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { expect } from '../pluginFixtures.js';\n\n/**\n * Converts a hex color value to its RGB equivalent.\n *\n * @param {string} hex - The hex color value. i.e. '#5b0f00'\n * @returns {string} The RGB equivalent of the hex color.\n */\nfunction hexToRGB(hex) {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n  return result\n    ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})`\n    : null;\n}\n\n/**\n * Sets the background and text color of a given element.\n *\n * @param {import('@playwright/test').Page} page - The Playwright page object.\n * @param {string} borderColorHex - The hex value of the border color to set, or 'No Style'.\n * @param {string} backgroundColorHex - The hex value of the background color to set, or 'No Style'.\n * @param {string} textColorHex - The hex value of the text color to set, or 'No Style'.\n * @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be set.\n */\nasync function setStyles(page, borderColorHex, backgroundColorHex, textColorHex, locator) {\n  await locator.click(); // Assuming the locator is clickable and opens the style setting UI\n  await page.getByLabel('Set border color').click();\n  await page.getByLabel(borderColorHex).click();\n  await page.getByLabel('Set background color').click();\n  await page.getByLabel(backgroundColorHex).click();\n  await page.getByLabel('Set text color').click();\n  await page.getByLabel(textColorHex).click();\n}\n\n/**\n * Checks if the styles of an element match the expected values.\n *\n * @param {string} expectedBorderColor - The expected border color in RGB format. Default is '#e6b8af' or 'rgb(230, 184, 175)'\n * @param {string} expectedBackgroundColor - The expected background color in RGB format.\n * @param {string} expectedTextColor - The expected text color in RGB format. Default is #aaaaaa or 'rgb(170, 170, 170)'\n * @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be checked.\n */\nasync function checkStyles(\n  expectedBorderColor,\n  expectedBackgroundColor,\n  expectedTextColor,\n  locator\n) {\n  const layoutStyles = await locator.evaluate((el) => {\n    return {\n      border: window.getComputedStyle(el).getPropertyValue('border-top-color'), //infer the left, right, and bottom\n      background: window.getComputedStyle(el).getPropertyValue('background-color'),\n      fontColor: window.getComputedStyle(el).getPropertyValue('color')\n    };\n  });\n  expect(layoutStyles.border).toContain(expectedBorderColor);\n  expect(layoutStyles.background).toContain(expectedBackgroundColor);\n  expect(layoutStyles.fontColor).toContain(expectedTextColor);\n}\n\n/**\n * Checks if the font Styles of an element match the expected values.\n *\n * @param {string} expectedFontSize - The expected font size in '72px' format. Default is 'Default'\n * @param {string} expectedFontWeight - The expected font Type. Format as '700' for bold. Default is 'Default'\n * @param {string} expectedFontFamily - The expected font Type. Format as \"\\\"Andale Mono\\\", sans-serif\". Default is 'Default'\n * @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be checked.\n */\nasync function checkFontStyles(expectedFontSize, expectedFontWeight, expectedFontFamily, locator) {\n  const layoutStyles = await locator.evaluate((el) => {\n    return {\n      fontSize: window.getComputedStyle(el).getPropertyValue('font-size'),\n      fontWeight: window.getComputedStyle(el).getPropertyValue('font-weight'),\n      fontFamily: window.getComputedStyle(el).getPropertyValue('font-family')\n    };\n  });\n  expect(layoutStyles.fontSize).toContain(expectedFontSize);\n  expect(layoutStyles.fontWeight).toContain(expectedFontWeight);\n  expect(layoutStyles.fontFamily).toContain(expectedFontFamily);\n}\n\nexport { checkFontStyles, checkStyles, hexToRGB, setStyles };\n"
  },
  {
    "path": "e2e/helper/useDarkmatterTheme.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Darkmatter theme for Open MCT.\n// e.g.\n// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.DarkmatterTheme());\n});\n"
  },
  {
    "path": "e2e/helper/useSnowTheme.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This should be used to install the Snow theme for Open MCT. Espresso is the default\n// e.g.\n// await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') });\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  const openmct = window.openmct;\n  openmct.install(openmct.plugins.Snow());\n});\n"
  },
  {
    "path": "e2e/index.js",
    "content": "// Import everything from the specific fixture files\nimport * as appActions from './appActions.js';\nimport * as avpFixtures from './avpFixtures.js';\nimport * as baseFixtures from './baseFixtures.js';\nimport * as pluginFixtures from './pluginFixtures.js';\n\n// Export these as named exports\nexport { appActions, avpFixtures, baseFixtures, pluginFixtures };\n"
  },
  {
    "path": "e2e/package.json",
    "content": "{\n  \"name\": \"openmct-e2e\",\n  \"version\": \"4.1.0-next\",\n  \"description\": \"The Open MCT e2e framework\",\n  \"type\": \"module\",\n  \"module\": \"index.js\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.js\"\n    }\n  },\n  \"scripts\": {\n    \"test\": \"npx playwright test\",\n    \"test:visual\": \"percy exec\"\n  },\n  \"devDependencies\": {\n    \"@axe-core/playwright\": \"4.8.5\",\n    \"@percy/cli\": \"1.27.4\",\n    \"@percy/playwright\": \"1.0.4\",\n    \"@playwright/test\": \"1.57.0\"\n  },\n  \"author\": {\n    \"name\": \"National Aeronautics and Space Administration\",\n    \"url\": \"https://www.nasa.gov\"\n  },\n  \"license\": \"Apache-2.0\"\n}\n"
  },
  {
    "path": "e2e/playwright-ci.config.js",
    "content": "// playwright.config.js\n// @ts-check\n\n// eslint-disable-next-line no-unused-vars\nimport { devices } from '@playwright/test';\nimport { fileURLToPath } from 'url';\nconst MAX_FAILURES = 5;\nconst NUM_WORKERS = 4;\n\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite\n  testDir: 'tests',\n  grepInvert: /@mobile/, //Ignore mobile tests\n  testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js\n  timeout: 60 * 1000,\n  webServer: {\n    command: 'npm run start:coverage',\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.\n  },\n  maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste\n  workers: NUM_WORKERS,\n  use: {\n    baseURL: 'http://localhost:8080/',\n    headless: true,\n    ignoreHTTPSErrors: true,\n    screenshot: 'only-on-failure',\n    trace: 'on-first-retry',\n    video: 'off',\n    // @ts-ignore - custom configuration option for nyc codecoverage output path\n    coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))\n  },\n  projects: [\n    {\n      name: 'chrome',\n      testMatch: '**/*.e2e.spec.js', // only run e2e tests\n      use: {\n        browserName: 'chromium'\n      }\n    },\n    {\n      name: 'MMOC',\n      testMatch: '**/*.e2e.spec.js', // only run e2e tests\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'chromium',\n        viewport: {\n          width: 2560,\n          height: 1440\n        }\n      }\n    },\n    {\n      name: 'firefox',\n      testMatch: '**/*.e2e.spec.js', // only run e2e tests\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'firefox'\n      }\n    },\n    {\n      name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary\n      testMatch: '**/*.e2e.spec.js', // only run e2e tests\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'chromium',\n        channel: 'chrome-beta'\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    [\n      'html',\n      {\n        open: 'never',\n        outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840\n      }\n    ],\n    ['junit', { outputFile: '../test-results/results.xml' }]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-local.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { fileURLToPath } from 'url';\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 0,\n  testDir: 'tests',\n  testMatch: '**/*.e2e.spec.js', // only run e2e tests\n  testIgnore: '**/*.perf.spec.js',\n  timeout: 30 * 1000,\n  webServer: {\n    command: 'npm run start:coverage',\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 120 * 1000,\n    reuseExistingServer: true\n  },\n  workers: 1,\n  use: {\n    browserName: 'chromium',\n    baseURL: 'http://localhost:8080/',\n    headless: false,\n    ignoreHTTPSErrors: true,\n    screenshot: 'only-on-failure',\n    trace: 'retain-on-failure',\n    video: 'off'\n  },\n  projects: [\n    {\n      name: 'chrome',\n      use: {\n        browserName: 'chromium'\n      }\n    },\n    {\n      name: 'MMOC',\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'chromium',\n        viewport: {\n          width: 2560,\n          height: 1440\n        }\n      }\n    },\n    {\n      name: 'safari',\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'webkit'\n      }\n    },\n    {\n      name: 'firefox',\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'firefox'\n      }\n    },\n    {\n      name: 'canary',\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'chromium',\n        channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI\n      }\n    },\n    {\n      name: 'chrome-beta',\n      grepInvert: /@snapshot/,\n      use: {\n        browserName: 'chromium',\n        channel: 'chrome-beta'\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    [\n      'html',\n      {\n        open: 'on-failure',\n        outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840\n      }\n    ]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-mobile.config.js",
    "content": "// playwright.config.js\n// @ts-check\n\nimport { devices } from '@playwright/test';\nconst MAX_FAILURES = 5;\n\nimport { fileURLToPath } from 'url';\n\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite\n  testDir: 'tests',\n  testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js\n  timeout: 30 * 1000,\n  webServer: {\n    command: 'npm run start:coverage',\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.\n  },\n  maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste\n  workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067\n\n  use: {\n    baseURL: 'http://localhost:8080/',\n    headless: true,\n    ignoreHTTPSErrors: true,\n    screenshot: 'only-on-failure',\n    trace: 'on-first-retry',\n    video: 'off',\n    // @ts-ignore - custom configuration option for nyc codecoverage output path\n    coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))\n  },\n  projects: [\n    {\n      name: 'ipad',\n      grep: /@mobile/,\n      use: {\n        storageState: fileURLToPath(\n          new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)\n        ),\n        browserName: 'webkit',\n        ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json\n      }\n    },\n    {\n      name: 'iphone',\n      grep: /@mobile/,\n      use: {\n        storageState: fileURLToPath(\n          new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)\n        ),\n        browserName: 'webkit',\n        ...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    [\n      'html',\n      {\n        open: 'never',\n        outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840\n      }\n    ],\n    ['junit', { outputFile: '../test-results/results.xml' }]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-performance-dev.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { fileURLToPath } from 'url';\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 1, //Only for debugging purposes for trace: 'on-first-retry'\n  testDir: 'tests/performance/',\n  testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode\n  timeout: 60 * 1000,\n  workers: 1, //Only run in serial with 1 worker\n  webServer: {\n    command: 'npm run start', //need development mode for performance.marks and others\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: false\n  },\n  use: {\n    browserName: 'chromium',\n    baseURL: 'http://localhost:8080/',\n    headless: true,\n    ignoreHTTPSErrors: false, //HTTP performance varies!\n    screenshot: 'off',\n    trace: 'on-first-retry',\n    video: 'off'\n  },\n  projects: [\n    {\n      name: 'chrome',\n      testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here\n      use: {\n        browserName: 'chromium'\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    ['junit', { outputFile: '../test-results/results.xml' }],\n    ['json', { outputFile: '../test-results/results.json' }]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-performance-prod.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { fileURLToPath } from 'url';\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 0, //Only for debugging purposes for trace: 'on-first-retry'\n  testDir: 'tests/performance/',\n  testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode\n  timeout: 60 * 1000,\n  workers: 1, //Only run in serial with 1 worker\n  webServer: {\n    command: 'npm run start:prod', //Production mode\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: false //Must be run with this option to prevent dev mode\n  },\n  use: {\n    baseURL: 'http://localhost:8080/',\n    headless: true,\n    ignoreHTTPSErrors: false, //HTTP performance varies!\n    screenshot: 'off',\n    trace: 'on-first-retry',\n    video: 'off'\n  },\n  projects: [\n    {\n      name: 'chrome-memory',\n      testMatch: '*.memory.perf.spec.js', //Only run memory tests\n      use: {\n        browserName: 'chromium',\n        launchOptions: {\n          args: [\n            '--no-sandbox',\n            '--disable-notifications',\n            '--use-fake-ui-for-media-stream',\n            '--use-fake-device-for-media-stream',\n            '--js-flags=--no-move-object-start --expose-gc',\n            '--enable-precise-memory-info',\n            '--display=:100'\n          ]\n        }\n      }\n    },\n    {\n      name: 'chrome',\n      testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags\n      use: {\n        browserName: 'chromium'\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    ['junit', { outputFile: '../test-results/results.xml' }],\n    ['json', { outputFile: '../test-results/results.json' }]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-visual-a11y.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { fileURLToPath } from 'url';\n/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */\nconst config = {\n  retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim\n  testDir: 'tests/visual-a11y',\n  testMatch: '**/*.visual.spec.js', // only run visual tests\n  timeout: 60 * 1000,\n  workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067\n  webServer: {\n    command: 'npm run start:coverage',\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.\n  },\n  use: {\n    baseURL: 'http://localhost:8080/',\n    headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers\n    ignoreHTTPSErrors: true,\n    screenshot: 'only-on-failure',\n    trace: 'on-first-retry',\n    video: 'off'\n  },\n  projects: [\n    {\n      name: 'chrome',\n      use: {\n        browserName: 'chromium'\n      }\n    },\n    {\n      name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled\n      use: {\n        browserName: 'chromium',\n        theme: 'snow'\n      }\n    },\n    {\n      name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme\n      use: {\n        browserName: 'chromium',\n        theme: 'darkmatter'\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    ['junit', { outputFile: '../test-results/results.xml' }],\n    [\n      'html',\n      {\n        open: 'on-failure',\n        outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840\n      }\n    ]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/playwright-watch.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { devices } from '@playwright/test';\nimport { fileURLToPath } from 'url';\n\n/** @type {import('@playwright/test').PlaywrightTestConfig} */\nconst config = {\n  retries: 0, //Retries are not needed with watch mode\n  testDir: 'tests',\n  timeout: 60 * 1000,\n  webServer: {\n    command: 'npm run start', //Start in dev mode for hot reloading\n    cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project\n    url: 'http://localhost:8080/#',\n    timeout: 200 * 1000,\n    reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.\n  },\n  workers: '75%', //Limit to 75% of the CPU to support running with dev server\n  use: {\n    baseURL: 'http://localhost:8080/',\n    headless: true,\n    ignoreHTTPSErrors: true,\n    screenshot: 'only-on-failure',\n    trace: 'on-first-retry',\n    video: 'off'\n  },\n  projects: [\n    {\n      name: 'chrome',\n      testMatch: '**/*.spec.js', // run all tests\n      use: {\n        browserName: 'chromium'\n      }\n    },\n    {\n      name: 'ipad',\n      grep: /@mobile/,\n      use: {\n        storageState: fileURLToPath(\n          new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)\n        ),\n        browserName: 'webkit',\n        ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json\n      }\n    },\n    {\n      name: 'iphone',\n      grep: /@mobile/,\n      use: {\n        storageState: fileURLToPath(\n          new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)\n        ),\n        browserName: 'webkit',\n        ...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json\n      }\n    }\n  ],\n  reporter: [\n    ['list'],\n    [\n      'html',\n      {\n        open: 'never',\n        outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840\n      }\n    ],\n    ['junit', { outputFile: '../test-results/results.xml' }]\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "e2e/pluginFixtures.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * The file contains custom fixtures which extend the base functionality of the Playwright fixtures\n * and appActions. These fixtures should be generalized across all plugins.\n */\n\n// import { createDomainObjectWithDefaults } from './appActions.js';\nimport { fileURLToPath } from 'url';\n\nimport { expect, request, test } from './baseFixtures.js';\n\n/**\n * @typedef {Object} ObjectCreateOptions\n * @property {string} type\n * @property {string} name\n */\n\n/**\n * **NOTE: This feature is a work-in-progress and should not currently be used.**\n *\n * Used to create a new domain object as a part of getOrCreateDomainObject.\n * @type {Map<string, string>}\n */\n// const createdObjects = new Map();\n\n/**\n * This action will create a domain object for the test to reference and return the uuid. If an object\n * of a given name already exists, it will return the uuid of that object to the test instead of creating\n * a new file. The intent is to move object creation out of test suites which are not explicitly worried\n * about object creation, while providing a consistent interface to retrieving objects in a persistentContext.\n * @param {import('@playwright/test').Page} page\n * @param {ObjectCreateOptions} options\n * @returns {Promise<string>} uuid of the domain object\n */\n// async function getOrCreateDomainObject(page, options) {\n//     const { type, name } = options;\n//     const objectName = name ? `${type}:${name}` : type;\n\n//     if (createdObjects.has(objectName)) {\n//         return createdObjects.get(objectName);\n//     }\n\n//     await createDomainObjectWithDefaults(page, type, name);\n\n//     const uuid = getHashUrlToDomainObject(page);\n\n//     createdObjects.set(objectName, uuid);\n\n//     return uuid;\n// }\n\n/**\n * **NOTE: This feature is a work-in-progress and should not currently be used.**\n *\n * If provided, these options will be used to get or create the desired domain object before\n * any tests or test hooks have run.\n * The `uuid` of the `domainObject` will then be available to use within the scoped tests.\n *\n * ### Example:\n * ```js\n * test.describe(\"My test suite\", () => {\n *    test.use({ objectCreateOptions: { type: \"Telemetry Table\", name: \"My Telemetry Table\" }});\n *    test(\"'My Telemetry Table' is created and provides a uuid\", async ({ page, domainObject }) => {\n *         const { uuid } = domainObject;\n *         expect(uuid).toBeDefined();\n *     }))\n * });\n * ```\n * @type {ObjectCreateOptions}\n */\n// const objectCreateOptions = null;\n\n/**\n * The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js\n * will override the default theme by injecting the 'snow' theme on launch.\n *\n * ### Example:\n * ```js\n * projects: [\n * {\n *     name: 'chrome-snow-theme',\n *     use: {\n *         browserName: 'chromium',\n *         theme: 'snow'\n * ```\n * @type {'snow' | 'espresso'}\n */\nconst theme = 'espresso';\n\n/**\n * The name of the \"My Items\" folder in the domain object tree.\n *\n * Default: `\"My Items\"`\n *\n * @type {string}\n */\nconst myItemsFolderName = 'My Items';\n\nconst extendedTest = test.extend({\n  // This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js\n  theme: [theme, { option: true }],\n  // eslint-disable-next-line no-shadow\n  page: async ({ page, theme }, use, testInfo) => {\n    if (theme === 'snow') {\n      //inject snow theme\n      await page.addInitScript({\n        path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))\n      });\n    } else if (theme === 'darkmatter') {\n      //inject darkmatter theme\n      await page.addInitScript({\n        path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))\n      });\n    }\n\n    // Attach info about the currently running test and its project.\n    // This will be used by appActions to fill in the created\n    // domain object's notes.\n    page.testNotes = [`${testInfo.titlePath.join('\\n')}`, `${testInfo.project.name}`].join('\\n');\n\n    await use(page);\n  },\n  myItemsFolderName: [myItemsFolderName, { option: true }],\n  // eslint-disable-next-line no-shadow\n  openmctConfig: async ({ myItemsFolderName }, use) => {\n    await use({ myItemsFolderName });\n  }\n});\n\nexport { expect, request, extendedTest as test };\n\n/**\n * Takes a readable stream and returns a string.\n * @param {ReadableStream} readable - the readable stream\n * @return {Promise<String>} the stringified stream\n */\nexport async function streamToString(readable) {\n  let result = '';\n  for await (const chunk of readable) {\n    result += chunk;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "e2e/test-data/ExampleLayouts.json",
    "content": "{\n  \"openmct\": {\n    \"45b24009-dfed-4023-a30b-d31f5e3a2d87\": {\n      \"identifier\": {\n        \"key\": \"45b24009-dfed-4023-a30b-d31f5e3a2d87\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Open MCT Examples 10-05-22\",\n      \"type\": \"folder\",\n      \"composition\": [\n        {\n          \"key\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"modified\": 1664993018467,\n      \"location\": \"mine\",\n      \"persisted\": 1664993018469\n    },\n    \"832cd297-38ab-478a-a5ff-f3bfd278e41b\": {\n      \"identifier\": {\n        \"key\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Display Layout Example 1\",\n      \"type\": \"layout\",\n      \"composition\": [\n        {\n          \"key\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"da187902-64f6-460e-bfbe-c84bd628e126\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"03d9a461-8bcd-4821-b405-46b03d6da9b0\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"c68ff290-5891-41bd-bfc1-84734fb86a19\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"8f72d5f7-7074-4745-9a0b-c5854d31790b\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"caf64735-99c2-4906-a2fa-bb47912b2a79\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"4b6f1b0d-8a70-4dee-bb6c-36af255bc139\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"ee70fbec-2890-4eb5-9e15-15e5890b3138\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"152d2a9f-f0f3-408e-bf55-af007840cdef\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"6fed1053-3e10-44e3-8e60-4655d19266e0\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"e940f59d-e6ef-4a2b-bf82-d81d94627da2\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"items\": [\n          {\n            \"width\": 29,\n            \"height\": 54,\n            \"x\": 0,\n            \"y\": 5,\n            \"identifier\": {\n              \"key\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": false,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"e04ae658-c538-464b-a042-363da827717e\"\n          },\n          {\n            \"stroke\": \"transparent\",\n            \"x\": 30,\n            \"y\": 5,\n            \"width\": 27,\n            \"height\": 15,\n            \"url\": \"https://s.abcnews.com/images/Technology/mars-rover-2020-illustration-nasa-ht-jc-200305_hpMain_16x9_992.jpg\",\n            \"type\": \"image-view\",\n            \"id\": \"ed63cc29-80e2-4e2b-a472-3d6d4adbf310\"\n          },\n          {\n            \"identifier\": {\n              \"key\": \"e3131777-c0ef-44f3-9202-4c9c7b864dcc\",\n              \"namespace\": \"\"\n            },\n            \"x\": 30,\n            \"y\": 21,\n            \"width\": 27,\n            \"height\": 3,\n            \"displayMode\": \"all\",\n            \"value\": \"sin\",\n            \"stroke\": \"\",\n            \"fill\": \"\",\n            \"color\": \"\",\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"telemetry-view\",\n            \"id\": \"a212530c-2c8c-4c46-b8e3-62452832d749\"\n          },\n          {\n            \"identifier\": {\n              \"key\": \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\",\n              \"namespace\": \"\"\n            },\n            \"x\": 30,\n            \"y\": 25,\n            \"width\": 27,\n            \"height\": 3,\n            \"displayMode\": \"all\",\n            \"value\": \"sin\",\n            \"stroke\": \"\",\n            \"fill\": \"\",\n            \"color\": \"\",\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"telemetry-view\",\n            \"id\": \"06c90c9c-6982-4aba-a89d-588530592fa4\"\n          },\n          {\n            \"width\": 27,\n            \"height\": 30,\n            \"x\": 30,\n            \"y\": 29,\n            \"identifier\": {\n              \"key\": \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"b50b1748-ec6f-4ed8-a3d0-065b9f421aed\"\n          },\n          {\n            \"width\": 33,\n            \"height\": 23,\n            \"x\": 58,\n            \"y\": 5,\n            \"identifier\": {\n              \"key\": \"8f72d5f7-7074-4745-9a0b-c5854d31790b\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"bbb714a8-545e-460e-85e8-43a70fa2fd80\"\n          },\n          {\n            \"width\": 39,\n            \"height\": 10,\n            \"x\": 92,\n            \"y\": 49,\n            \"identifier\": {\n              \"key\": \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"1a52971a-60c3-4bfa-900d-3344f2703e2c\"\n          },\n          {\n            \"width\": 15,\n            \"height\": 4,\n            \"x\": 52,\n            \"y\": 0,\n            \"identifier\": {\n              \"key\": \"d776581d-443c-47e4-9d5a-f04950842981\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": false,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"d627e236-b63a-48d0-b12d-942ab9594b98\"\n          },\n          {\n            \"width\": 39,\n            \"height\": 43,\n            \"x\": 92,\n            \"y\": 5,\n            \"identifier\": {\n              \"key\": \"136523e4-ae71-4ebd-bd75-5394855d7708\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"da2dfd37-84d3-4b4d-98f4-133a565b6a2c\"\n          },\n          {\n            \"width\": 33,\n            \"height\": 30,\n            \"x\": 58,\n            \"y\": 29,\n            \"identifier\": {\n              \"key\": \"152d2a9f-f0f3-408e-bf55-af007840cdef\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"35d4375c-8e2b-4feb-896c-c76b1a9b5c9e\"\n          },\n          {\n            \"width\": 19,\n            \"height\": 4,\n            \"x\": 16,\n            \"y\": 0,\n            \"identifier\": {\n              \"key\": \"6fed1053-3e10-44e3-8e60-4655d19266e0\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": false,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"09683b4b-474a-479b-bc47-6105c78e14e7\"\n          },\n          {\n            \"fill\": \"\",\n            \"stroke\": \"\",\n            \"color\": \"\",\n            \"x\": 0,\n            \"y\": 0,\n            \"width\": 15,\n            \"height\": 4,\n            \"text\": \"MISSION TIME\",\n            \"fontSize\": \"20\",\n            \"font\": \"default\",\n            \"type\": \"text-view\",\n            \"id\": \"b9321c44-0c82-4687-b115-948678276e62\"\n          },\n          {\n            \"width\": 15,\n            \"height\": 4,\n            \"x\": 36,\n            \"y\": 0,\n            \"identifier\": {\n              \"key\": \"a07a2210-5c85-4fa7-8f83-14979fbf0beb\",\n              \"namespace\": \"\"\n            },\n            \"hasFrame\": false,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"ac0d7eb1-b485-458f-bd2a-a63aa87a3a8a\"\n          }\n        ],\n        \"layoutGrid\": [10, 10],\n        \"objectStyles\": {\n          \"ed63cc29-80e2-4e2b-a472-3d6d4adbf310\": {\n            \"staticStyle\": {\n              \"style\": {\n                \"imageUrl\": \"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQEBUPEBIVFRUVFQ8PFRUVFhUVFRUVFRUWFhUVFRUYHSggGBolHRUVITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OFQ8QFysdHR0rMC0rLS0tLTArLSsrLS4rKzEtLS0tLS0rKy0tKy0tLSsuKy4rLS0tKy0tLS0rKysrOP/AABEIAKgBLAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAAAQIDBAUGB//EADsQAAIBAgQCCAQFAwIHAAAAAAABAgMRBBIhMQVRBhNBYXGBkaEiMrHBFCNC0fBScoIH4RUWU2KDkvH/xAAYAQEBAQEBAAAAAAAAAAAAAAAAAQIDBP/EACERAQEBAQABBAMBAQAAAAAAAAABEQISAxMhMUFRYSJC/9oADAMBAAIRAxEAPwD4eA0hFAAAAwEADAAAAAAAAAAEMAEAwAQDEADEMAAAAAACgABpAIdhgUIYWHYIVgGACAYgpWAYECAAsBEYgIAYhgIBiAYJAIBgIAGAXEAwEMAAQAMDbQ4ZOdNVE0k5Tgr3XyqLbv8A5GjBcIq16KdNJuMqsrN2zK0VZd94smjkjBoCgABpFAFh2AoLAOw7ARsMdh2KhAOw7ARAkFhgiA7BYCNgJWFYCIxgRVQDAyAAABAMAEAAAxAAAAxAAAAASi7O7V+4iX4LDSq1IUo7zlGC82B0MQ6ippRbVKr+YorRafDKy5pqcb9uvM+g9HuHRhBZYtaJK+u3aX4r/TPF4JxjVrKrh4xk4zinLqk3eTUbXvd5rXs9b23Wno/wqtRlJVK9OpBSnFLSNRNOy+GW0fN7m+eLms2vC9PeBdTUWIi1lquzVrNTtdu3J2v43PKZT3PSLC8TxK6urSuoXrZYJSaS+HM2uz4tF3nk54SUXZpp8mrP0ZrwprHlHY1LDPkP8Ox4U1lsGU1dQHUjxNZso8po6oXVjxFGUdi7ILIMFWULFuUWUYK7BYsyisMELCsTsFiKrsFidhWAhYCVgJgoAAMKBAADEAAAAADEMQAAAAAAAa8LgnOLqbJXS72rXS8FJM70eiM5YSGJpz/MlqqTtdxausjv8zXYc2ipdRGjGPxSzT79db+GWMX5HseIYqvhqipU5OEYwopR00+CKb5a2uakS16b/TPprJ4dYTEScoxWWObW1tou/Yd7F4XDzjKplckmlZSaer3VvqfIKmJVDE1Gr3k41GmmrZ4xm7J83K67mj23BeJylRqNvROmrd9/2Z6+edmxx6+EeJpZbxdSNRKtCLUvgy1I5G5R/VJRvZ7a7HoOE8A4Zj8NH8p0q1OMKTbleU8sUnOT/U2+3c4deopC4diXSneLsdLxs+GZ0q4t0OVJvLqjz2I4Pl7D6NLHqpHU5GNoJkn9NeBq4C3YZp4U9bicKjm1sMW8xqdPPSoFbpHZqUDPOiYvLWuY6RFwN8qRVKmYvLTG4EcprdMg4GbBmyicS9xIuJnFUOJFxL3Ei4kFNhWLconEgqsKxY0KxBjEMDm0QDABAOwgAAAAAYWAAN0cJBdrk/KK+7fsaqas1GKUL22Vna19Xv6s6T0r+fhNc2OFn2q392ntu/I24HhSnrKTavZ5V3X0b3ItnR4Z8v8AnL2jH9zXXpTmamuhgqapZ3CpJOdOVF3g2owmrTirKyurK/cdGeJzyjKq6U7JbycJSfY5X3ty+phhU+n8+5dGp7O30JEX4rARr1JVqtPO5ZdYyf6YqK1jLlGK8jThXKlhauZWcq0IpbNpXd7eSME+r0vGPojo8IwSl8U03BNtJvS72S7tjv6dxiunT4dNwU09HFSV1a91ey1MUKx1ni9d7PwTyrlZ6MFll81Oi/8ABxfrGSO86/bGM+HxJoda4PDU2nanldnZxqSettPhktr95zYVi/aY01tTDWpmh1CqbBHPq0jLUpHSqIzVIma3HOnTKZ0zfOBROJzrUYZQK5QNkolUomK0yOJBxNMokHE52qzOJFxNDiQcTOihxItFziRaJopcRWLXEjYmq5gElTb7PdFiw0vD3+lzCqQL1Rj2yflH92voN0qfY5ewGcRa6S7JeuhCVNrsAQER3AZOgryXj9NSu5owS+LwT/Y1xN6hW4jN3JMgz3YwidXh0fy13yqP2gvscyx18Ivy6fhN9n9cl9jPqz/KNMGWwWnuyqD+/wBS+zu+768vc88ElH6XOvhdFCV3puru3ocuS1Vu9e7NuHnol5nXlmtUtHoy2M9ClvYcHr4fY7ystlKraUW+xwb9UcjNlbi+xuPo7HSh89v5pocbHztWn3yc/KfxL6mp0mNcag85hhVLVUJejF0mVSDOQcjnempEJoomi+TKpHO9NSKJIplE6FPBVJ7Rfi9F7mmnwXtqVFHuisz+yRzvTThNFckenXC8PF65n/c7fQ2YaVCD+Cmrrtsl6vdnO9NPIUsBVn8tOT78rt67GilwDES1cVFbfE19rnp6mO1t28rFX4iW19PV+S7DPkOJS6NSfzVErcot+7aNlHotSau5zl3Ky9dDo0qsX3+Ip1G9NSaMv/L+FhrJX5XlJl64Xh1tRhbvWvuEZc5eisNVF2t+5NV84dn2DghSTEqnNAOVMqdP/wCl8ZpjlTAyuK5/YFCS2LZR7iENAFlv8y+z9RSw3J+TL4vmWxsUc2dNrdGrh63fgjWgcUtkueh09Kf6iUmxA2NHtjASOzRj8FNf9jfrUm/uchHSdRQjG938ENlz15mfX+OYNNMvpPV+FvdfsYsNXjN5VdXtuv2O5xDhVTDWdVpJ2s0ptWdn2RPNCqIfz7/Quw99E91p/PJIpwyVTN1LUlHLf5lbNtvFcpehdCjUTzNLW3abjNbf5sizDvK1K12ndcr9mnbrZ+RmlUyRzTVrXbemi0sV0+LUP+rBf5JO3ba7OnkmOnQaS8FL1VmvdI4HHllr/wB0KcvSORr1gzs4CtGu+royjOVnJRjKLdllWyZzOmeFnRqUushKEnTkrSVnlUm0/PM/Ql6WRz4VC6NQ59Ft6JN+Bvo4aW8mo+O5jzXFimXUaM57LTm9F6joqnHsv3v9tidTHrYxe1xpp4CK+aV/DReu/wBC5OlD5Ur9y19XqcmWKb1IOs2YvTWOtUx0dtblDxHfbw/c56mv92JVIJ6sxqt0aut2wlVXYm/OyMTxsVsrmSpOUv1NLlHT3IOnOuk9WOGJOVDKv99S38Ql2gdB4iX7IOulzOV+Jv8AYX4h9rIOnGtYnGuzkxrD65cwPOykRlLQWZizdxcVFOxfTkVacg0LgulETiytSJqYwSUe4FT5Dzk4+JZymiOZFsP7U7+w435/QvjJLex055qWrqfC5OCd7Sla3wx0vtujvdFuFRjVyV1KrJ/HClGKp5nFawlPk156GPjWLSjTjTX9K0bfJdp1a2GhQrRxFOUFUpzVSL6p9j2bvs9U+5nS5P6y2Yrozh6eInWxE3Cn1XWKlGUIycpznkUVOom4RSs2tbpXPBYqgpVJdW5Zb7N89tdT3nSXjC4oljKdNU6co9VGFSphoyvByzS/MadrtrTkeY6IYDrMRVp1Jxp5XbNKDqxvmatHK7Nabp2OP219HwbCSvbLd2Tu3F2Tbtqmet4fRnN06VSMZwjUhNwaeVxi3mjo1ozpYjhWFwE6VatW62M6dWOWjh+pk5Zqbirzl8S+Z37Ld5bg+lOFdVSjhZreNusW7lzyvQ68Rjpk43Cgqk/w+HhRjFUYvLdZ23N7NvZL3Zzet1NPTjjkFBTp4ecVKUYueZSi5JStHZc2/I8PPpLNX+Ffx+xfKc/CZr21HqpxnCtBzg4TvFSyvROV0+atp376XPL8X6I1KKbptzg7TTas2rN/EuxpN9252+gFeWOqygoNuOXMoq7s738rJrzPecYWHg44ac4xqu9WNN2VRpxnmlkeuXKp93wk66lmrJY+JYKrWwlSNalNwnG+WSyytdNPR3T0b3Rq4t0gxOMnGpiqrrShHJFyjCNo3va0Ipb8zrcfoxcG76q36bbtbcjytSNjHfNlal1uXEZpWTSXJKxJY+XccvOSjUMK6n4mT3Y1WfP2Oaqv8sPrSK6Drd7F1rMSrskq75kGvMxMzdfLmyLqvvA1OYs/eZesH1ncTFaHMi5FHWdwOp3BFzmRlMpcwzsotu+fsTsv4mZ88hqUuZMHMykkSyBlZ0xNK40NRZJXLgSiTjTfIFJklUfM1MRZDCye0WaaXDKr2j6uK+rMirvmJ15f1M3LzE+XWhwPEPZU/OrSX1kU4jg9aPzSor/z0ftI5cqr5lTkS9z9Eldb8VkcHNxeS3yTjO7W2z7l2kcZxlzTSurpq77O/c5NwOfkuO/wXj0qVBYfM45ak6kXe11UUFKL07HBNf3SN8OOwlJN1mraX/Mu/RPTz7WeSRdSoylsi89WFj0PEuPzqVVaTqRhpHM5Nduquk1uwocXlF5sq3vv33MeE4NXlqo+6NNXgtaKu8v/ALI78+X3jnc+mTjXE5V5ZnFLRRtZPZt3zNX7djjz1N2IwzTs2vUrhgHLaS9Uce5bW5kPhONdCebVpqzSbT02ejX8Z3qXSSnGTrZZda4SoxdrqMJfM23K7k1eK7EpS56cyj0erT+VxfmaY9Dsa9VTb8BOOs+jyijFcRnV0Sdr3em77O1keqm18r9GXvovj4a9TPyTLqT4hR3jUXirm5L/ANSs7+nLqUJL9L9ClwfI9NT45itpe8S6GNz/ADxg/GKL7fN+qeVeSuxqR7OHC6VTsgvYtXQ1z+WUfUX0anuR4lSHmPZVOgGJ3iovwZgr9DcVDek/KzM+3WvKPO5kGh0q3A60PmpyXkzLPBSW6fozN5v6XWe4XJuiyLgTFIQxXQwIWpLMGYmBahdhmDMMGbMwzMQE1UrsakQuNMaJ3AgJsaYmRZDODkTQ2hWFcaIGo9yLYUBQiaaRqFXYbCJnWw+FS7Dn0p2NdPEtHbnI53XQTcdk/Uy4rFvZkXimZ69e5u9MyMGKqXMirWZqrNMxzijh1XSN2Hx7Xadzh/H5w+WbXgzyNicZtF59SwvMr6pwzp3WhZOamt7TSf2PS4TpzQqK1ShBX3cVf2Z8Op4l8zTTxrXa15m/cl+4x419mxVXAV9bKPcowXuo3Zw8dwGg23Tn6pHgaPFZL9RthxyXM6T1OWbzXZr8Icfll6GVyr09psyf8ab7SufErlvfP4JL+XQjx/Ew2qSXg2XQ6Y4hbzk/FnCniUzPOaMXutTl6ldL5v5nfxRCpx2nPeMeWyPJTZTJk92w8I9RVxNCX6ImOtCk9oo4HWMOvlzM31NXxdOrQgZp0I9hm/EyE8QzOxcqcqXeVuAdcJ1CNFYQ8wrkGYVwAyouK4AQK4rgAAIAIGicQAotiWwlYAKLo1GTVWXMANspdfLmVzqAA0Z5yKJMAM1YrYXADKhMkmAANSZJTYAUWRqssVVgBdE1VYdaMC6hdYJzAAIZhZgAgVwEAAxDAKiAAB//2Q==\"\n              }\n            }\n          }\n        }\n      },\n      \"modified\": 1664992567772,\n      \"location\": \"45b24009-dfed-4023-a30b-d31f5e3a2d87\",\n      \"persisted\": 1664992594970\n    },\n    \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\": {\n      \"identifier\": {\n        \"key\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Flexible Layout Example 1\",\n      \"type\": \"flexible-layout\",\n      \"configuration\": {\n        \"containers\": [\n          {\n            \"id\": \"3c8aedca-7413-494a-b389-a4d657c7103c\",\n            \"frames\": [\n              {\n                \"id\": \"19cd7f7d-3cc6-4c97-b832-e178b2c75602\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 10,\n                \"noFrame\": false\n              },\n              {\n                \"id\": \"8dd99a22-99a9-4e0c-8384-4e5072f11c74\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"1c96da17-bce6-4ddf-bde0-70a5031f9399\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 30,\n                \"noFrame\": false\n              },\n              {\n                \"id\": \"27918937-a5f7-48f3-8c97-ad904da6f885\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 30,\n                \"noFrame\": false\n              },\n              {\n                \"id\": \"872e1cc1-a955-40f9-a67a-d65aca32d420\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"c03cbe46-c5f0-40a8-a703-813ad663e900\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 30,\n                \"noFrame\": false\n              }\n            ],\n            \"size\": 18\n          },\n          {\n            \"id\": \"3c3f9f05-112e-433b-a6f2-c01bbc6a8f4d\",\n            \"frames\": [\n              {\n                \"id\": \"fd1aea5b-6fe8-40fe-b315-e8313a37d56e\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 59,\n                \"noFrame\": false\n              },\n              {\n                \"id\": \"b9bdc867-492e-4c6f-b963-813908e3a259\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"136523e4-ae71-4ebd-bd75-5394855d7708\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 37,\n                \"noFrame\": false\n              }\n            ],\n            \"size\": 66\n          },\n          {\n            \"id\": \"2f145353-b742-4172-b07d-7e2bb8ce17fc\",\n            \"frames\": [\n              {\n                \"id\": \"7d6a2b96-ff11-47ec-a690-0495c7525ff9\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"d776581d-443c-47e4-9d5a-f04950842981\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 5,\n                \"noFrame\": true\n              },\n              {\n                \"id\": \"5f9b34f9-db81-48fc-84e2-2a1709ecc065\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"a07a2210-5c85-4fa7-8f83-14979fbf0beb\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 5,\n                \"noFrame\": true\n              },\n              {\n                \"id\": \"c22e76f8-fc29-4685-a5a8-ff088c2ee3ee\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"4ae303e9-8edb-490b-93c3-cfaadfcdb64a\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 45,\n                \"noFrame\": false\n              },\n              {\n                \"id\": \"c6426188-aec7-466c-a800-10653a3fade6\",\n                \"domainObjectIdentifier\": {\n                  \"key\": \"2a84d91e-8612-43ed-9477-61b9cfbf5c2c\",\n                  \"namespace\": \"\"\n                },\n                \"size\": 45,\n                \"noFrame\": false\n              }\n            ],\n            \"size\": 16\n          }\n        ],\n        \"rowsLayout\": false,\n        \"objectStyles\": {}\n      },\n      \"composition\": [\n        {\n          \"key\": \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"1c96da17-bce6-4ddf-bde0-70a5031f9399\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"4ae303e9-8edb-490b-93c3-cfaadfcdb64a\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"2a84d91e-8612-43ed-9477-61b9cfbf5c2c\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"c03cbe46-c5f0-40a8-a703-813ad663e900\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"136523e4-ae71-4ebd-bd75-5394855d7708\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"d776581d-443c-47e4-9d5a-f04950842981\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"a07a2210-5c85-4fa7-8f83-14979fbf0beb\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"modified\": 1664992971253,\n      \"location\": \"45b24009-dfed-4023-a30b-d31f5e3a2d87\",\n      \"persisted\": 1664992976071\n    },\n    \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\": {\n      \"identifier\": {\n        \"key\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n        \"namespace\": \"\"\n      },\n      \"type\": \"telemetry.plot.stacked\",\n      \"composition\": [\n        {\n          \"key\": \"017ddbe2-1bec-46d2-acc9-5c2ad8fad79d\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"b5c72c1c-03fc-42d5-97c4-a34c7dacf872\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"b95ad884-7c92-4789-b6e0-e8e90155482e\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"10532581-1b49-438f-a2fe-843d412bf201\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"series\": [\n          {\n            \"identifier\": {\n              \"key\": \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\",\n              \"namespace\": \"\"\n            },\n            \"series\": {\n              \"markers\": true\n            }\n          }\n        ],\n        \"yAxis\": {},\n        \"xAxis\": {}\n      },\n      \"name\": \"Merged Telemetry Views\",\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1658875540107,\n      \"modified\": 1658875512306\n    },\n    \"da187902-64f6-460e-bfbe-c84bd628e126\": {\n      \"identifier\": {\n        \"key\": \"da187902-64f6-460e-bfbe-c84bd628e126\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"modified\": 1657232414748,\n      \"persisted\": 1657232414748\n    },\n    \"03d9a461-8bcd-4821-b405-46b03d6da9b0\": {\n      \"identifier\": {\n        \"key\": \"03d9a461-8bcd-4821-b405-46b03d6da9b0\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1657232414746\n    },\n    \"c68ff290-5891-41bd-bfc1-84734fb86a19\": {\n      \"identifier\": {\n        \"key\": \"c68ff290-5891-41bd-bfc1-84734fb86a19\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Needle Dial SWG Large\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"135df6d8-080a-40b8-89b0-301c96174e18\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"dial-needle\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -1000000,\n          \"limitHigh\": 1000000,\n          \"max\": 2000000,\n          \"min\": -2000000,\n          \"precision\": 2\n        },\n        \"objectStyles\": {\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"__no_value\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"styles\": [],\n          \"selectedConditionId\": \"\"\n        }\n      },\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"modified\": 1658421115305,\n      \"persisted\": 1658421118049\n    },\n    \"8f72d5f7-7074-4745-9a0b-c5854d31790b\": {\n      \"identifier\": {\n        \"key\": \"8f72d5f7-7074-4745-9a0b-c5854d31790b\",\n        \"namespace\": \"\"\n      },\n      \"type\": \"table\",\n      \"composition\": [\n        {\n          \"key\": \"f4da3027-8296-4faf-b8c1-30b8919c53f4\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"columnWidths\": {},\n        \"hiddenColumns\": {\n          \"yesterday\": true,\n          \"wavelengths\": true,\n          \"wavelengths-unit\": true,\n          \"cos\": true,\n          \"cos-unit\": true,\n          \"intensities\": true\n        },\n        \"columnOrder\": [],\n        \"cellFormat\": {},\n        \"autosize\": true\n      },\n      \"name\": \"SWG Small\",\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1657660214977,\n      \"modified\": 1657660199738\n    },\n    \"caf64735-99c2-4906-a2fa-bb47912b2a79\": {\n      \"identifier\": {\n        \"key\": \"caf64735-99c2-4906-a2fa-bb47912b2a79\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Meter Horz SWG Small\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"7ef88d73-8a2d-4b8f-92de-6abb76dd02e9\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"meter-horizontal\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -4.8,\n          \"limitHigh\": 4,\n          \"max\": 4.3,\n          \"min\": -5,\n          \"precision\": 2\n        }\n      },\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"modified\": 1657232414736,\n      \"persisted\": 1657232414736\n    },\n    \"4b6f1b0d-8a70-4dee-bb6c-36af255bc139\": {\n      \"identifier\": {\n        \"key\": \"4b6f1b0d-8a70-4dee-bb6c-36af255bc139\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"CW 210\",\n      \"type\": \"conditionWidget\",\n      \"configuration\": {\n        \"objectStyles\": {\n          \"styles\": [\n            {\n              \"conditionId\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#a61c00\",\n                \"border\": \"\",\n                \"color\": \"#ffffff\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#38761d\",\n                \"border\": \"\",\n                \"color\": \"#00ff00\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"14d676d6-5a38-4276-9d15-134f0afa6231\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#cc4125\",\n                \"border\": \"\",\n                \"color\": \"#f3f3f3\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"\",\n                \"border\": \"\",\n                \"color\": \"\",\n                \"output\": \"\"\n              }\n            }\n          ],\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"selectedConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"defaultConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"conditionSetIdentifier\": {\n            \"key\": \"056bbcb6-1a45-4429-97e6-a8ae74cf55e3\",\n            \"namespace\": \"\"\n          }\n        },\n        \"useConditionSetOutputAsLabel\": false\n      },\n      \"label\": \"CW_210 BUS B\",\n      \"conditionalLabel\": \"\",\n      \"modified\": 1664992949199,\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1664992949199\n    },\n    \"ee70fbec-2890-4eb5-9e15-15e5890b3138\": {\n      \"identifier\": {\n        \"key\": \"ee70fbec-2890-4eb5-9e15-15e5890b3138\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Example Imagery\",\n      \"type\": \"example.imagery\",\n      \"configuration\": {\n        \"imageLocation\": \"\",\n        \"imageLoadDelayInMilliSeconds\": \"50000\",\n        \"imageSamples\": [],\n        \"layers\": [\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-16x9.png\",\n            \"name\": \"16:9\",\n            \"visible\": true\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-safe.png\",\n            \"name\": \"Safe\",\n            \"visible\": true\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-scale.png\",\n            \"name\": \"Scale\",\n            \"visible\": true\n          }\n        ]\n      },\n      \"telemetry\": {\n        \"values\": [\n          {\n            \"name\": \"Name\",\n            \"key\": \"name\",\n            \"source\": \"name\",\n            \"hints\": {\n              \"priority\": 0\n            }\n          },\n          {\n            \"name\": \"Time\",\n            \"key\": \"utc\",\n            \"format\": \"utc\",\n            \"hints\": {\n              \"domain\": 2,\n              \"priority\": 1\n            },\n            \"source\": \"utc\"\n          },\n          {\n            \"name\": \"Local Time\",\n            \"key\": \"local\",\n            \"format\": \"local-format\",\n            \"hints\": {\n              \"domain\": 1,\n              \"priority\": 2\n            },\n            \"source\": \"local\"\n          },\n          {\n            \"name\": \"Image\",\n            \"key\": \"url\",\n            \"format\": \"image\",\n            \"hints\": {\n              \"image\": 1,\n              \"priority\": 3\n            },\n            \"layers\": [\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-16x9.png\",\n                \"name\": \"16:9\",\n                \"visible\": true\n              },\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-safe.png\",\n                \"name\": \"Safe\",\n                \"visible\": true\n              },\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-scale.png\",\n                \"name\": \"Scale\",\n                \"visible\": true\n              }\n            ],\n            \"source\": \"url\"\n          },\n          {\n            \"name\": \"Image Download Name\",\n            \"key\": \"imageDownloadName\",\n            \"format\": \"imageDownloadName\",\n            \"hints\": {\n              \"imageDownloadName\": 1,\n              \"priority\": 4\n            },\n            \"source\": \"imageDownloadName\"\n          }\n        ]\n      },\n      \"modified\": 1664993420994,\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1664993420997\n    },\n    \"152d2a9f-f0f3-408e-bf55-af007840cdef\": {\n      \"identifier\": {\n        \"key\": \"152d2a9f-f0f3-408e-bf55-af007840cdef\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Auto Range\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"4625b5f5-415b-42c6-9d97-4f86d5d7a4ef\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"dial-filled\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -12,\n          \"limitHigh\": 12,\n          \"max\": 15,\n          \"min\": -15,\n          \"precision\": 2\n        }\n      },\n      \"modified\": 1664991952387,\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1664991968607\n    },\n    \"6fed1053-3e10-44e3-8e60-4655d19266e0\": {\n      \"identifier\": {\n        \"key\": \"6fed1053-3e10-44e3-8e60-4655d19266e0\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Mission Time\",\n      \"type\": \"timer\",\n      \"configuration\": {\n        \"timerFormat\": \"long\",\n        \"timestamp\": 1664755200000,\n        \"timezone\": \"UTC\",\n        \"timerState\": \"started\",\n        \"fontStyle\": {\n          \"fontSize\": \"20\",\n          \"font\": \"default\"\n        },\n        \"objectStyles\": {\n          \"styles\": [],\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"\",\n              \"border\": \"\",\n              \"color\": \"#ffffff\"\n            }\n          },\n          \"selectedConditionId\": \"\"\n        }\n      },\n      \"modified\": 1664992594980,\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1664992594980\n    },\n    \"e940f59d-e6ef-4a2b-bf82-d81d94627da2\": {\n      \"identifier\": {\n        \"key\": \"e940f59d-e6ef-4a2b-bf82-d81d94627da2\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"CW 210 2\",\n      \"type\": \"conditionWidget\",\n      \"configuration\": {\n        \"objectStyles\": {\n          \"styles\": [\n            {\n              \"conditionId\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#cc4125\",\n                \"border\": \"\",\n                \"color\": \"#f3f3f3\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#38761d\",\n                \"border\": \"\",\n                \"color\": \"#00ff00\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"\",\n                \"border\": \"\",\n                \"color\": \"\",\n                \"output\": \"\"\n              }\n            }\n          ],\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"selectedConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"defaultConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"conditionSetIdentifier\": {\n            \"key\": \"a62b89ea-0fa0-46c5-beac-53d1009ac48c\",\n            \"namespace\": \"\"\n          }\n        },\n        \"useConditionSetOutputAsLabel\": false,\n        \"fontStyle\": {\n          \"fontSize\": \"default\",\n          \"font\": \"default\"\n        }\n      },\n      \"label\": \"CW_210 BUS A\",\n      \"conditionalLabel\": \"\",\n      \"location\": \"832cd297-38ab-478a-a5ff-f3bfd278e41b\",\n      \"persisted\": 1664992949203,\n      \"modified\": 1664992949203\n    },\n    \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\": {\n      \"identifier\": {\n        \"key\": \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Meter Horz SWG Small\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"ec3b4a3b-97f0-405e-be5a-d0ac15313df9\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"meter-horizontal\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -4.8,\n          \"limitHigh\": 4,\n          \"max\": 4.3,\n          \"min\": -5,\n          \"precision\": 2\n        }\n      },\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"modified\": 1657232414736,\n      \"persisted\": 1657232414736\n    },\n    \"1c96da17-bce6-4ddf-bde0-70a5031f9399\": {\n      \"identifier\": {\n        \"key\": \"1c96da17-bce6-4ddf-bde0-70a5031f9399\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Filled Dial SWG Small\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"7a87b0a0-8a37-4c59-ac29-4d2861a55110\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"dial-filled\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -4.8,\n          \"limitHigh\": 4,\n          \"max\": 4.3,\n          \"min\": -5,\n          \"precision\": 2\n        }\n      },\n      \"modified\": 1657232414736,\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1657232414736\n    },\n    \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\": {\n      \"identifier\": {\n        \"key\": \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Needle Dial SWG Large\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"3984ca34-8512-4857-8a4e-56e9be074e00\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"dial-needle\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -1000000,\n          \"limitHigh\": 1000000,\n          \"max\": 2000000,\n          \"min\": -2000000,\n          \"precision\": 2\n        },\n        \"objectStyles\": {\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"__no_value\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"styles\": [],\n          \"selectedConditionId\": \"\"\n        }\n      },\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"modified\": 1658421115305,\n      \"persisted\": 1658421118049\n    },\n    \"4ae303e9-8edb-490b-93c3-cfaadfcdb64a\": {\n      \"identifier\": {\n        \"key\": \"4ae303e9-8edb-490b-93c3-cfaadfcdb64a\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Meter Vert SWG Large\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"6c530559-b285-40b3-8e76-52f08c112f34\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"meter-vertical\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -1000000,\n          \"limitHigh\": 1000000,\n          \"max\": 2000000,\n          \"min\": -2000000,\n          \"precision\": 2\n        }\n      },\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"modified\": 1657232414737,\n      \"persisted\": 1657232414737\n    },\n    \"2a84d91e-8612-43ed-9477-61b9cfbf5c2c\": {\n      \"identifier\": {\n        \"key\": \"2a84d91e-8612-43ed-9477-61b9cfbf5c2c\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Gauge Vertical Meter Inverted\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"691c802f-c53b-4938-8881-f421d02b2770\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"meter-vertical-inverted\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -9,\n          \"limitHigh\": 9,\n          \"max\": 10,\n          \"min\": -10,\n          \"precision\": 2\n        }\n      },\n      \"modified\": 1664993313837,\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1664993321002\n    },\n    \"c03cbe46-c5f0-40a8-a703-813ad663e900\": {\n      \"identifier\": {\n        \"key\": \"c03cbe46-c5f0-40a8-a703-813ad663e900\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100 Gauge\",\n      \"type\": \"gauge\",\n      \"composition\": [\n        {\n          \"key\": \"c1b9f83b-a7db-4047-8dc3-37a2186b9c43\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"gaugeController\": {\n          \"gaugeType\": \"dial-needle\",\n          \"isDisplayMinMax\": true,\n          \"isDisplayCurVal\": true,\n          \"isDisplayUnits\": true,\n          \"isUseTelemetryLimits\": false,\n          \"limitLow\": -20,\n          \"limitHigh\": 120,\n          \"max\": 200,\n          \"min\": -100,\n          \"precision\": 2\n        },\n        \"objectStyles\": {}\n      },\n      \"modified\": 1660006343021,\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1660006343021\n    },\n    \"e230c105-b961-4e01-a056-8f2e8630e185\": {\n      \"identifier\": {\n        \"key\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n        \"namespace\": \"\"\n      },\n      \"type\": \"telemetry.plot.stacked\",\n      \"composition\": [\n        {\n          \"key\": \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"e3131777-c0ef-44f3-9202-4c9c7b864dcc\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"fe903b87-0d52-475b-a858-624fa67c378f\",\n          \"namespace\": \"\"\n        },\n        {\n          \"key\": \"00326dab-02c5-4522-9f68-e9e73758a272\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"configuration\": {\n        \"series\": [\n          {\n            \"identifier\": {\n              \"key\": \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\",\n              \"namespace\": \"\"\n            },\n            \"series\": {\n              \"markers\": true\n            }\n          }\n        ],\n        \"yAxis\": {},\n        \"xAxis\": {}\n      },\n      \"name\": \"Merged Telemetry Views\",\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1658875540107,\n      \"modified\": 1658875512306\n    },\n    \"136523e4-ae71-4ebd-bd75-5394855d7708\": {\n      \"identifier\": {\n        \"key\": \"136523e4-ae71-4ebd-bd75-5394855d7708\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"Example Imagery\",\n      \"type\": \"example.imagery\",\n      \"configuration\": {\n        \"imageLocation\": \"\",\n        \"imageLoadDelayInMilliSeconds\": \"50000\",\n        \"imageSamples\": [],\n        \"layers\": [\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-16x9.png\",\n            \"name\": \"16:9\",\n            \"visible\": true\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-safe.png\",\n            \"name\": \"Safe\",\n            \"visible\": true\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-scale.png\",\n            \"name\": \"Scale\",\n            \"visible\": true\n          }\n        ]\n      },\n      \"telemetry\": {\n        \"values\": [\n          {\n            \"name\": \"Name\",\n            \"key\": \"name\",\n            \"source\": \"name\",\n            \"hints\": {\n              \"priority\": 0\n            }\n          },\n          {\n            \"name\": \"Time\",\n            \"key\": \"utc\",\n            \"format\": \"utc\",\n            \"hints\": {\n              \"domain\": 2,\n              \"priority\": 1\n            },\n            \"source\": \"utc\"\n          },\n          {\n            \"name\": \"Local Time\",\n            \"key\": \"local\",\n            \"format\": \"local-format\",\n            \"hints\": {\n              \"domain\": 1,\n              \"priority\": 2\n            },\n            \"source\": \"local\"\n          },\n          {\n            \"name\": \"Image\",\n            \"key\": \"url\",\n            \"format\": \"image\",\n            \"hints\": {\n              \"image\": 1,\n              \"priority\": 3\n            },\n            \"layers\": [\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-16x9.png\",\n                \"name\": \"16:9\",\n                \"visible\": true\n              },\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-safe.png\",\n                \"name\": \"Safe\",\n                \"visible\": true\n              },\n              {\n                \"source\": \"dist/imagery/example-imagery-layer-scale.png\",\n                \"name\": \"Scale\",\n                \"visible\": true\n              }\n            ],\n            \"source\": \"url\"\n          },\n          {\n            \"name\": \"Image Download Name\",\n            \"key\": \"imageDownloadName\",\n            \"format\": \"imageDownloadName\",\n            \"hints\": {\n              \"imageDownloadName\": 1,\n              \"priority\": 4\n            },\n            \"source\": \"imageDownloadName\"\n          }\n        ]\n      },\n      \"modified\": 1664993420994,\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1664993420997\n    },\n    \"d776581d-443c-47e4-9d5a-f04950842981\": {\n      \"identifier\": {\n        \"key\": \"d776581d-443c-47e4-9d5a-f04950842981\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"CW 210\",\n      \"type\": \"conditionWidget\",\n      \"configuration\": {\n        \"objectStyles\": {\n          \"styles\": [\n            {\n              \"conditionId\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#a61c00\",\n                \"border\": \"\",\n                \"color\": \"#ffffff\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#38761d\",\n                \"border\": \"\",\n                \"color\": \"#00ff00\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"14d676d6-5a38-4276-9d15-134f0afa6231\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#cc4125\",\n                \"border\": \"\",\n                \"color\": \"#f3f3f3\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"\",\n                \"border\": \"\",\n                \"color\": \"\",\n                \"output\": \"\"\n              }\n            }\n          ],\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"selectedConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"defaultConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"conditionSetIdentifier\": {\n            \"key\": \"0e4342e3-06db-4e13-ba48-b391879b37b6\",\n            \"namespace\": \"\"\n          }\n        },\n        \"useConditionSetOutputAsLabel\": false\n      },\n      \"label\": \"CW_210 BUS B\",\n      \"conditionalLabel\": \"\",\n      \"modified\": 1664992949199,\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1664992949199\n    },\n    \"a07a2210-5c85-4fa7-8f83-14979fbf0beb\": {\n      \"identifier\": {\n        \"key\": \"a07a2210-5c85-4fa7-8f83-14979fbf0beb\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"CW 210 2\",\n      \"type\": \"conditionWidget\",\n      \"configuration\": {\n        \"objectStyles\": {\n          \"styles\": [\n            {\n              \"conditionId\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#cc4125\",\n                \"border\": \"\",\n                \"color\": \"#f3f3f3\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"#38761d\",\n                \"border\": \"\",\n                \"color\": \"#00ff00\",\n                \"output\": \"\"\n              }\n            },\n            {\n              \"conditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n              \"style\": {\n                \"isStyleInvisible\": \"\",\n                \"backgroundColor\": \"\",\n                \"border\": \"\",\n                \"color\": \"\",\n                \"output\": \"\"\n              }\n            }\n          ],\n          \"staticStyle\": {\n            \"style\": {\n              \"backgroundColor\": \"\",\n              \"border\": \"\",\n              \"color\": \"\"\n            }\n          },\n          \"selectedConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"defaultConditionId\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n          \"conditionSetIdentifier\": {\n            \"key\": \"ee7ca7b3-04e2-4c91-b73f-01280a92d1cc\",\n            \"namespace\": \"\"\n          }\n        },\n        \"useConditionSetOutputAsLabel\": false,\n        \"fontStyle\": {\n          \"fontSize\": \"default\",\n          \"font\": \"default\"\n        }\n      },\n      \"label\": \"CW_210 BUS A\",\n      \"conditionalLabel\": \"\",\n      \"location\": \"5f6760b7-85c5-4f90-a8b1-c6ba8382a684\",\n      \"persisted\": 1664992949203,\n      \"modified\": 1664992949203\n    },\n    \"056bbcb6-1a45-4429-97e6-a8ae74cf55e3\": {\n      \"identifier\": {\n        \"key\": \"056bbcb6-1a45-4429-97e6-a8ae74cf55e3\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"210 Condition Set\",\n      \"type\": \"conditionSet\",\n      \"configuration\": {\n        \"conditionTestData\": [],\n        \"conditionCollection\": [\n          {\n            \"id\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n            \"configuration\": {\n              \"name\": \"High\",\n              \"output\": \"HIGH\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"64e49fe7-5b36-43db-8347-4550b910de4c\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"greaterThan\",\n                  \"input\": [\"120\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  > 120 \"\n          },\n          {\n            \"id\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n            \"configuration\": {\n              \"name\": \"Nom\",\n              \"output\": \"NOMINAL\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"59f1c4bf-5d36-450c-9668-6546955fc066\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"between\",\n                  \"input\": [\"120\", \"-20\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  is between 120 and -20 \"\n          },\n          {\n            \"id\": \"14d676d6-5a38-4276-9d15-134f0afa6231\",\n            \"configuration\": {\n              \"name\": \"Low\",\n              \"output\": \"LOW\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"6707be12-6a6e-4535-bb97-ab5c86f99934\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"lessThan\",\n                  \"input\": [\"-20\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  < -20 \"\n          },\n          {\n            \"isDefault\": true,\n            \"id\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n            \"configuration\": {\n              \"name\": \"Default\",\n              \"output\": \"Default\",\n              \"trigger\": \"all\",\n              \"criteria\": []\n            },\n            \"summary\": \"\"\n          }\n        ]\n      },\n      \"composition\": [\n        {\n          \"key\": \"10987a9e-74ed-4e31-b820-806deadc9425\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"telemetry\": {},\n      \"modified\": 1660006307115,\n      \"location\": null,\n      \"persisted\": 1660006307759\n    },\n    \"0e4342e3-06db-4e13-ba48-b391879b37b6\": {\n      \"identifier\": {\n        \"key\": \"0e4342e3-06db-4e13-ba48-b391879b37b6\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"210 Condition Set\",\n      \"type\": \"conditionSet\",\n      \"configuration\": {\n        \"conditionTestData\": [],\n        \"conditionCollection\": [\n          {\n            \"id\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n            \"configuration\": {\n              \"name\": \"High\",\n              \"output\": \"HIGH\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"64e49fe7-5b36-43db-8347-4550b910de4c\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"greaterThan\",\n                  \"input\": [\"120\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  > 120 \"\n          },\n          {\n            \"id\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n            \"configuration\": {\n              \"name\": \"Nom\",\n              \"output\": \"NOMINAL\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"59f1c4bf-5d36-450c-9668-6546955fc066\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"between\",\n                  \"input\": [\"120\", \"-20\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  is between 120 and -20 \"\n          },\n          {\n            \"id\": \"14d676d6-5a38-4276-9d15-134f0afa6231\",\n            \"configuration\": {\n              \"name\": \"Low\",\n              \"output\": \"LOW\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"6707be12-6a6e-4535-bb97-ab5c86f99934\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"lessThan\",\n                  \"input\": [\"-20\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  < -20 \"\n          },\n          {\n            \"isDefault\": true,\n            \"id\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n            \"configuration\": {\n              \"name\": \"Default\",\n              \"output\": \"Default\",\n              \"trigger\": \"all\",\n              \"criteria\": []\n            },\n            \"summary\": \"\"\n          }\n        ]\n      },\n      \"composition\": [\n        {\n          \"key\": \"1a15f3e4-99a9-4afd-a6fa-dcc822b39cbc\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"telemetry\": {},\n      \"modified\": 1660006307115,\n      \"location\": null,\n      \"persisted\": 1660006307759\n    },\n    \"a62b89ea-0fa0-46c5-beac-53d1009ac48c\": {\n      \"identifier\": {\n        \"key\": \"a62b89ea-0fa0-46c5-beac-53d1009ac48c\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"210 Condition Set 2\",\n      \"type\": \"conditionSet\",\n      \"configuration\": {\n        \"conditionTestData\": [],\n        \"conditionCollection\": [\n          {\n            \"id\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n            \"configuration\": {\n              \"name\": \"High\",\n              \"output\": \"HIGH\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"64e49fe7-5b36-43db-8347-4550b910de4c\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"greaterThan\",\n                  \"input\": [\"150\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  > 150 \"\n          },\n          {\n            \"id\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n            \"configuration\": {\n              \"name\": \"Nom\",\n              \"output\": \"NOMINAL\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"59f1c4bf-5d36-450c-9668-6546955fc066\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"between\",\n                  \"input\": [\"50\", \"-50\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  is between 50 and -50 \"\n          },\n          {\n            \"isDefault\": true,\n            \"id\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n            \"configuration\": {\n              \"name\": \"Default\",\n              \"output\": \"Default\",\n              \"trigger\": \"all\",\n              \"criteria\": []\n            },\n            \"summary\": \"\"\n          }\n        ]\n      },\n      \"composition\": [\n        {\n          \"key\": \"b8b2d336-cdc2-42de-bb6b-c2033e691b35\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"telemetry\": {},\n      \"location\": null,\n      \"persisted\": 1664992259247,\n      \"modified\": 1664992250791\n    },\n    \"ee7ca7b3-04e2-4c91-b73f-01280a92d1cc\": {\n      \"identifier\": {\n        \"key\": \"ee7ca7b3-04e2-4c91-b73f-01280a92d1cc\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"210 Condition Set 2\",\n      \"type\": \"conditionSet\",\n      \"configuration\": {\n        \"conditionTestData\": [],\n        \"conditionCollection\": [\n          {\n            \"id\": \"6248211a-5164-4657-8c86-3484601d5e3c\",\n            \"configuration\": {\n              \"name\": \"High\",\n              \"output\": \"HIGH\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"64e49fe7-5b36-43db-8347-4550b910de4c\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"greaterThan\",\n                  \"input\": [\"150\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  > 150 \"\n          },\n          {\n            \"id\": \"80708a88-b282-4524-8160-4e73546bff6a\",\n            \"configuration\": {\n              \"name\": \"Nom\",\n              \"output\": \"NOMINAL\",\n              \"trigger\": \"any\",\n              \"criteria\": [\n                {\n                  \"id\": \"59f1c4bf-5d36-450c-9668-6546955fc066\",\n                  \"telemetry\": \"any\",\n                  \"operation\": \"between\",\n                  \"input\": [\"50\", \"-50\"],\n                  \"metadata\": \"sin\"\n                }\n              ]\n            },\n            \"summary\": \"Match if any criteria are met:  any telemetry Sine  is between 50 and -50 \"\n          },\n          {\n            \"isDefault\": true,\n            \"id\": \"7af9f52f-e402-4782-ab28-01d450d506f0\",\n            \"configuration\": {\n              \"name\": \"Default\",\n              \"output\": \"Default\",\n              \"trigger\": \"all\",\n              \"criteria\": []\n            },\n            \"summary\": \"\"\n          }\n        ]\n      },\n      \"composition\": [\n        {\n          \"key\": \"17e8e842-c4f0-4c11-b4c6-e729f4018120\",\n          \"namespace\": \"\"\n        }\n      ],\n      \"telemetry\": {},\n      \"location\": null,\n      \"persisted\": 1664992259247,\n      \"modified\": 1664992250791\n    },\n    \"7a87b0a0-8a37-4c59-ac29-4d2861a55110\": {\n      \"identifier\": {\n        \"key\": \"7a87b0a0-8a37-4c59-ac29-4d2861a55110\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"1c96da17-bce6-4ddf-bde0-70a5031f9399\",\n      \"persisted\": 1657232414746\n    },\n    \"6c530559-b285-40b3-8e76-52f08c112f34\": {\n      \"identifier\": {\n        \"key\": \"6c530559-b285-40b3-8e76-52f08c112f34\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"location\": \"4ae303e9-8edb-490b-93c3-cfaadfcdb64a\",\n      \"modified\": 1657232414748,\n      \"persisted\": 1657232414748\n    },\n    \"4625b5f5-415b-42c6-9d97-4f86d5d7a4ef\": {\n      \"identifier\": {\n        \"key\": \"4625b5f5-415b-42c6-9d97-4f86d5d7a4ef\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 10\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"10\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0,\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657236492163,\n      \"location\": \"152d2a9f-f0f3-408e-bf55-af007840cdef\",\n      \"persisted\": 1657236492163\n    },\n    \"017ddbe2-1bec-46d2-acc9-5c2ad8fad79d\": {\n      \"identifier\": {\n        \"key\": \"017ddbe2-1bec-46d2-acc9-5c2ad8fad79d\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n      \"persisted\": 1657232414746\n    },\n    \"b5c72c1c-03fc-42d5-97c4-a34c7dacf872\": {\n      \"identifier\": {\n        \"key\": \"b5c72c1c-03fc-42d5-97c4-a34c7dacf872\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"location\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n      \"modified\": 1657232414748,\n      \"persisted\": 1657232414748\n    },\n    \"b95ad884-7c92-4789-b6e0-e8e90155482e\": {\n      \"identifier\": {\n        \"key\": \"b95ad884-7c92-4789-b6e0-e8e90155482e\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 10\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"10\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0,\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657236492163,\n      \"location\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n      \"persisted\": 1657236492163\n    },\n    \"10532581-1b49-438f-a2fe-843d412bf201\": {\n      \"identifier\": {\n        \"key\": \"10532581-1b49-438f-a2fe-843d412bf201\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657149026977,\n      \"location\": \"d78ec1fd-2cea-4b73-8bb6-ba44b09dc3b2\",\n      \"persisted\": 1657149026977\n    },\n    \"c1b9f83b-a7db-4047-8dc3-37a2186b9c43\": {\n      \"identifier\": {\n        \"key\": \"c1b9f83b-a7db-4047-8dc3-37a2186b9c43\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657149026977,\n      \"location\": \"c03cbe46-c5f0-40a8-a703-813ad663e900\",\n      \"persisted\": 1657149026977\n    },\n    \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\": {\n      \"identifier\": {\n        \"key\": \"6f0c1a8f-cefa-4783-a79e-9f123e8718ba\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n      \"persisted\": 1657232414746\n    },\n    \"e3131777-c0ef-44f3-9202-4c9c7b864dcc\": {\n      \"identifier\": {\n        \"key\": \"e3131777-c0ef-44f3-9202-4c9c7b864dcc\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"location\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n      \"modified\": 1657232414748,\n      \"persisted\": 1657232414748\n    },\n    \"fe903b87-0d52-475b-a858-624fa67c378f\": {\n      \"identifier\": {\n        \"key\": \"fe903b87-0d52-475b-a858-624fa67c378f\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 10\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"10\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0,\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657236492163,\n      \"location\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n      \"persisted\": 1657236492163\n    },\n    \"00326dab-02c5-4522-9f68-e9e73758a272\": {\n      \"identifier\": {\n        \"key\": \"00326dab-02c5-4522-9f68-e9e73758a272\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657149026977,\n      \"location\": \"e230c105-b961-4e01-a056-8f2e8630e185\",\n      \"persisted\": 1657149026977\n    },\n    \"135df6d8-080a-40b8-89b0-301c96174e18\": {\n      \"identifier\": {\n        \"key\": \"135df6d8-080a-40b8-89b0-301c96174e18\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414747,\n      \"location\": \"c68ff290-5891-41bd-bfc1-84734fb86a19\",\n      \"persisted\": 1657232414747\n    },\n    \"3984ca34-8512-4857-8a4e-56e9be074e00\": {\n      \"identifier\": {\n        \"key\": \"3984ca34-8512-4857-8a4e-56e9be074e00\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Large\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"1000000\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414747,\n      \"location\": \"8ae9193d-2f79-43bc-a39f-4f89a52da5a3\",\n      \"persisted\": 1657232414747\n    },\n    \"f4da3027-8296-4faf-b8c1-30b8919c53f4\": {\n      \"identifier\": {\n        \"key\": \"f4da3027-8296-4faf-b8c1-30b8919c53f4\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"8f72d5f7-7074-4745-9a0b-c5854d31790b\",\n      \"persisted\": 1657232414746\n    },\n    \"7ef88d73-8a2d-4b8f-92de-6abb76dd02e9\": {\n      \"identifier\": {\n        \"key\": \"7ef88d73-8a2d-4b8f-92de-6abb76dd02e9\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"caf64735-99c2-4906-a2fa-bb47912b2a79\",\n      \"persisted\": 1657232414746\n    },\n    \"ec3b4a3b-97f0-405e-be5a-d0ac15313df9\": {\n      \"identifier\": {\n        \"key\": \"ec3b4a3b-97f0-405e-be5a-d0ac15313df9\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"cdbbaf4f-ddaf-42c3-8e5e-51aef81d665d\",\n      \"persisted\": 1657232414746\n    },\n    \"691c802f-c53b-4938-8881-f421d02b2770\": {\n      \"identifier\": {\n        \"key\": \"691c802f-c53b-4938-8881-f421d02b2770\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG Small\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"5\",\n        \"amplitude\": \"5\",\n        \"offset\": 0,\n        \"dataRateInHz\": \"5\",\n        \"phase\": 0,\n        \"randomness\": 0\n      },\n      \"modified\": 1657232414746,\n      \"location\": \"2a84d91e-8612-43ed-9477-61b9cfbf5c2c\",\n      \"persisted\": 1657232414746\n    },\n    \"10987a9e-74ed-4e31-b820-806deadc9425\": {\n      \"identifier\": {\n        \"key\": \"10987a9e-74ed-4e31-b820-806deadc9425\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657149026977,\n      \"location\": \"056bbcb6-1a45-4429-97e6-a8ae74cf55e3\",\n      \"persisted\": 1657149026977\n    },\n    \"1a15f3e4-99a9-4afd-a6fa-dcc822b39cbc\": {\n      \"identifier\": {\n        \"key\": \"1a15f3e4-99a9-4afd-a6fa-dcc822b39cbc\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"modified\": 1657149026977,\n      \"location\": \"0e4342e3-06db-4e13-ba48-b391879b37b6\",\n      \"persisted\": 1657149026977\n    },\n    \"b8b2d336-cdc2-42de-bb6b-c2033e691b35\": {\n      \"identifier\": {\n        \"key\": \"b8b2d336-cdc2-42de-bb6b-c2033e691b35\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"location\": \"a62b89ea-0fa0-46c5-beac-53d1009ac48c\",\n      \"persisted\": 1664992205660,\n      \"modified\": 1664992205660\n    },\n    \"17e8e842-c4f0-4c11-b4c6-e729f4018120\": {\n      \"identifier\": {\n        \"key\": \"17e8e842-c4f0-4c11-b4c6-e729f4018120\",\n        \"namespace\": \"\"\n      },\n      \"name\": \"SWG 100\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": \"100\",\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": \"1\",\n        \"loadDelay\": 0\n      },\n      \"location\": \"ee7ca7b3-04e2-4c91-b73f-01280a92d1cc\",\n      \"persisted\": 1664992205660,\n      \"modified\": 1664992205660\n    }\n  },\n  \"rootId\": \"45b24009-dfed-4023-a30b-d31f5e3a2d87\"\n}\n"
  },
  {
    "path": "e2e/test-data/PerformanceDisplayLayout.json",
    "content": "{\n  \"openmct\": {\n    \"b3cee102-86dd-4c0a-8eec-4d5d276f8691\": {\n      \"identifier\": { \"key\": \"b3cee102-86dd-4c0a-8eec-4d5d276f8691\", \"namespace\": \"\" },\n      \"name\": \"Performance Display Layout\",\n      \"type\": \"layout\",\n      \"composition\": [{ \"key\": \"9666e7b4-be0c-47a5-94b8-99accad7155e\", \"namespace\": \"\" }],\n      \"configuration\": {\n        \"items\": [\n          {\n            \"width\": 32,\n            \"height\": 18,\n            \"x\": 12,\n            \"y\": 9,\n            \"identifier\": { \"key\": \"9666e7b4-be0c-47a5-94b8-99accad7155e\", \"namespace\": \"\" },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"23ca351d-a67d-46aa-a762-290eb742d2f1\"\n          }\n        ],\n        \"layoutGrid\": [10, 10]\n      },\n      \"modified\": 1654299875432,\n      \"location\": \"mine\",\n      \"persisted\": 1654299878751\n    },\n    \"9666e7b4-be0c-47a5-94b8-99accad7155e\": {\n      \"identifier\": { \"key\": \"9666e7b4-be0c-47a5-94b8-99accad7155e\", \"namespace\": \"\" },\n      \"name\": \"Performance Example Imagery\",\n      \"type\": \"example.imagery\",\n      \"configuration\": {\n        \"imageLocation\": \"\",\n        \"imageLoadDelayInMilliSeconds\": 20000,\n        \"imageSamples\": [],\n        \"layers\": [\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-16x9.png\",\n            \"name\": \"16:9\",\n            \"visible\": false\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-safe.png\",\n            \"name\": \"Safe\",\n            \"visible\": false\n          },\n          {\n            \"source\": \"dist/imagery/example-imagery-layer-scale.png\",\n            \"name\": \"Scale\",\n            \"visible\": false\n          }\n        ]\n      },\n      \"telemetry\": {\n        \"values\": [\n          { \"name\": \"Name\", \"key\": \"name\" },\n          { \"name\": \"Time\", \"key\": \"utc\", \"format\": \"utc\", \"hints\": { \"domain\": 2 } },\n          {\n            \"name\": \"Local Time\",\n            \"key\": \"local\",\n            \"format\": \"local-format\",\n            \"hints\": { \"domain\": 1 }\n          },\n          {\n            \"name\": \"Image\",\n            \"key\": \"url\",\n            \"format\": \"image\",\n            \"hints\": { \"image\": 1 },\n            \"layers\": [\n              { \"source\": \"dist/imagery/example-imagery-layer-16x9.png\", \"name\": \"16:9\" },\n              { \"source\": \"dist/imagery/example-imagery-layer-safe.png\", \"name\": \"Safe\" },\n              { \"source\": \"dist/imagery/example-imagery-layer-scale.png\", \"name\": \"Scale\" }\n            ]\n          },\n          {\n            \"name\": \"Image Download Name\",\n            \"key\": \"imageDownloadName\",\n            \"format\": \"imageDownloadName\",\n            \"hints\": { \"imageDownloadName\": 1 }\n          }\n        ]\n      },\n      \"modified\": 1654299840077,\n      \"location\": \"b3cee102-86dd-4c0a-8eec-4d5d276f8691\",\n      \"persisted\": 1654299840078\n    }\n  },\n  \"rootId\": \"b3cee102-86dd-4c0a-8eec-4d5d276f8691\"\n}\n"
  },
  {
    "path": "e2e/test-data/PerformanceNotebook.json",
    "content": "{\n  \"openmct\": {\n    \"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d\": {\n      \"identifier\": { \"key\": \"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d\", \"namespace\": \"\" },\n      \"name\": \"Performance Notebook\",\n      \"type\": \"notebook\",\n      \"configuration\": {\n        \"defaultSort\": \"oldest\",\n        \"entries\": {\n          \"3e31c412-33ba-4757-8ade-e9821f6ba321\": {\n            \"8c8f6035-631c-45af-8c24-786c60295335\": [\n              {\n                \"id\": \"entry-1652815305457\",\n                \"createdOn\": 1652815305457,\n                \"createdBy\": \"\",\n                \"text\": \"Existing Entry 1\",\n                \"embeds\": []\n              },\n              {\n                \"id\": \"entry-1652815313465\",\n                \"createdOn\": 1652815313465,\n                \"createdBy\": \"\",\n                \"text\": \"Existing Entry 2\",\n                \"embeds\": []\n              },\n              {\n                \"id\": \"entry-1652815399955\",\n                \"createdOn\": 1652815399955,\n                \"createdBy\": \"\",\n                \"text\": \"Existing Entry 3\",\n                \"embeds\": []\n              }\n            ]\n          }\n        },\n        \"imageMigrationVer\": \"v1\",\n        \"pageTitle\": \"Page\",\n        \"sections\": [\n          {\n            \"id\": \"3e31c412-33ba-4757-8ade-e9821f6ba321\",\n            \"isDefault\": false,\n            \"isSelected\": false,\n            \"name\": \"Section1\",\n            \"pages\": [\n              {\n                \"id\": \"8c8f6035-631c-45af-8c24-786c60295335\",\n                \"isDefault\": false,\n                \"isSelected\": false,\n                \"name\": \"Page1\",\n                \"pageTitle\": \"Page\"\n              },\n              {\n                \"id\": \"36555942-c9aa-439c-bbdb-0aaf50db50f5\",\n                \"isDefault\": false,\n                \"isSelected\": false,\n                \"name\": \"Page2\",\n                \"pageTitle\": \"Page\"\n              }\n            ],\n            \"sectionTitle\": \"Section\"\n          },\n          {\n            \"id\": \"dab0bd1d-2c5a-405c-987f-107123d6189a\",\n            \"isDefault\": false,\n            \"isSelected\": true,\n            \"name\": \"Section2\",\n            \"pages\": [\n              {\n                \"id\": \"f625a86a-cb99-4898-8082-80543c8de534\",\n                \"isDefault\": false,\n                \"isSelected\": false,\n                \"name\": \"Page1\",\n                \"pageTitle\": \"Page\"\n              },\n              {\n                \"id\": \"e77ef810-f785-42a7-942e-07e999b79c59\",\n                \"isDefault\": false,\n                \"isSelected\": true,\n                \"name\": \"Page2\",\n                \"pageTitle\": \"Page\"\n              }\n            ],\n            \"sectionTitle\": \"Section\"\n          }\n        ],\n        \"sectionTitle\": \"Section\",\n        \"type\": \"General\",\n        \"showTime\": \"0\"\n      },\n      \"modified\": 1652815915219,\n      \"location\": \"mine\",\n      \"persisted\": 1652815915222\n    }\n  },\n  \"rootId\": \"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d\"\n}\n"
  },
  {
    "path": "e2e/test-data/blank.html",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Blank Page</title>\n</head>\n<body>\n</body>\n</html>"
  },
  {
    "path": "e2e/test-data/condition_set_storage.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1725480977300,\\\"created\\\":1725480975674,\\\"persisted\\\":1725480977301},\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Test Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"id\\\":\\\"1f4b8d87-297b-4a2a-a2d2-46c42eb41b39\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Test Condition\\\",\\\"output\\\":\\\"Test Condition Met\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[{\\\"id\\\":\\\"034b4dfe-b17e-43f0-9787-93e4666d2690\\\",\\\"telemetry\\\":{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"},\\\"operation\\\":\\\"greaterThan\\\",\\\"input\\\":[0],\\\"metadata\\\":\\\"sin\\\"}]},\\\"summary\\\":\\\"Match if all criteria are met:  VIPER Rover Heading Sine  > 0 \\\"},{\\\"isDefault\\\":true,\\\"id\\\":\\\"c56ff651-547e-4704-a8b7-4f01247e2fa7\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Test Condition Unmet\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"\\\"}]},\\\"composition\\\":[{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"}],\\\"telemetry\\\":{},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Conditional Styling Data @localStorage @generatedata\\\\nGenerate basic condition set\\\\nchrome\\\",\\\"modified\\\":1725480978924,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1725480977299,\\\"persisted\\\":1725480978924},\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":5,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1725480978545,\\\"location\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"created\\\":1725480977993,\\\"persisted\\\":1725480978545}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Test Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"c56ff651-547e-4704-a8b7-4f01247e2fa7\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"}],\\\"telemetry\\\":{},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Conditional Styling Data @localStorage @generatedata\\\\nGenerate basic condition set\\\\nchrome\\\",\\\"modified\\\":1725480977994,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1725480977299,\\\"persisted\\\":1725480977994},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1725480977300,\\\"created\\\":1725480975674,\\\"persisted\\\":1725480977301},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Test Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"c56ff651-547e-4704-a8b7-4f01247e2fa7\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"}],\\\"telemetry\\\":{},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Conditional Styling Data @localStorage @generatedata\\\\nGenerate basic condition set\\\\nchrome\\\",\\\"modified\\\":1725480977994,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1725480977299,\\\"persisted\\\":1725480977994}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":5,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1725480978545,\\\"location\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"created\\\":1725480977993,\\\"persisted\\\":1725480978545},{\\\"identifier\\\":{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Test Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"c56ff651-547e-4704-a8b7-4f01247e2fa7\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"}],\\\"telemetry\\\":{},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Conditional Styling Data @localStorage @generatedata\\\\nGenerate basic condition set\\\\nchrome\\\",\\\"modified\\\":1725480977994,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1725480977299,\\\"persisted\\\":1725480977994},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1725480977300,\\\"created\\\":1725480975674,\\\"persisted\\\":1725480977301},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/954af939-eaf8-4977-8cee-57f36b58aae3/1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1725480978542,\\\"location\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"created\\\":1725480977993,\\\"persisted\\\":1725480977993}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1725480977300,\\\"created\\\":1725480975674,\\\"persisted\\\":1725480977301},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"954af939-eaf8-4977-8cee-57f36b58aae3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1725480977300,\\\"created\\\":1725480975674,\\\"persisted\\\":1725480977301}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/display_layout_with_child_layouts.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666},\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":30,\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"94b55aa2-8f61-431a-9453-4bbfda9119fb\\\"},{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":30,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"97d028f7-ffa6-494b-96d6-d8526e399766\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607743,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604666,\\\"persisted\\\":1732413607743},\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\":{\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413605564,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413605564,\\\"persisted\\\":1732413605564},\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\":{\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"identifier\\\":{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"},\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413606944,\\\"persisted\\\":1732413606944}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"94b55aa2-8f61-431a-9453-4bbfda9119fb\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604666,\\\"persisted\\\":1732413606944},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"94b55aa2-8f61-431a-9453-4bbfda9119fb\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604666,\\\"persisted\\\":1732413606944}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413606944,\\\"persisted\\\":1732413606944},{\\\"identifier\\\":{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"94b55aa2-8f61-431a-9453-4bbfda9119fb\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604666,\\\"persisted\\\":1732413606944},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad/ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413606944,\\\"persisted\\\":1732413606944}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413605564,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413605564,\\\"persisted\\\":1732413605564},{\\\"identifier\\\":{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ea2f53e0-3376-4ba7-8df7-349339e41d64\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"94b55aa2-8f61-431a-9453-4bbfda9119fb\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606944,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604666,\\\"persisted\\\":1732413606944},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad/034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"034e9aab-8b24-493b-876f-80ed474b61fb\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413605564,\\\"location\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"created\\\":1732413605564,\\\"persisted\\\":1732413605564}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f11f93ab-8918-4732-b20c-617b7b2e16ad\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604666,\\\"created\\\":1732413603039,\\\"persisted\\\":1732413604666}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/display_layout_with_child_overlay_plot.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993},\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[{\\\"width\\\":32,\\\"height\\\":18,\\\"x\\\":1,\\\"y\\\":1,\\\"identifier\\\":{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"},\\\"hasFrame\\\":true,\\\"fontSize\\\":\\\"default\\\",\\\"font\\\":\\\"default\\\",\\\"type\\\":\\\"subobject-view\\\",\\\"id\\\":\\\"7aea18ca-1537-4f4f-98e4-a57b4cd8f9a7\\\"}],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413608088.7,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604993,\\\"persisted\\\":1732413608088.7},\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Overlay Plot 1\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"}}],\\\"useIndependentTime\\\":true,\\\"timeOptions\\\":{\\\"clockOffsets\\\":{\\\"start\\\":-1800000,\\\"end\\\":30000},\\\"fixedOffsets\\\":{\\\"start\\\":1731438671000,\\\"end\\\":1731442271000},\\\"clock\\\":\\\"local\\\",\\\"mode\\\":\\\"fixed\\\"}},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413608703.7,\\\"location\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"created\\\":1732413606392.7,\\\"persisted\\\":1732413608703.7},\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\":{\\\"name\\\":\\\"Child SWG 1\\\",\\\"type\\\":\\\"generator\\\",\\\"identifier\\\":{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"},\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"created\\\":1732413607788,\\\"persisted\\\":1732413607788}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"tcHistory\",\n          \"value\": \"{\\\"utc\\\":[{\\\"start\\\":1731352271000,\\\"end\\\":1731355871000}]}\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413606392.7,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604993,\\\"persisted\\\":1732413606392.7},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413606392.7,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604993,\\\"persisted\\\":1732413606392.7}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child SWG 1\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"created\\\":1732413607788,\\\"persisted\\\":1732413607788},{\\\"identifier\\\":{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Overlay Plot 1\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"created\\\":1732413606392.7,\\\"persisted\\\":1732413607788},{\\\"identifier\\\":{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413606392.7,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604993,\\\"persisted\\\":1732413606392.7},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7/6731f693-b09e-46de-a0cb-9331f1fb2e6d/fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child SWG 1\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"created\\\":1732413607788,\\\"persisted\\\":1732413607788}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Overlay Plot 1\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"created\\\":1732413606392.7,\\\"persisted\\\":1732413607788},{\\\"identifier\\\":{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Display Layout\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413606392.7,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413604993,\\\"persisted\\\":1732413606392.7},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7/6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Overlay Plot 1\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"fa050882-fcec-49c0-aa78-236a595accaf\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate display layout with 1 child overlay plot\\\\nchrome\\\",\\\"modified\\\":1732413607788,\\\"location\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"created\\\":1732413606392.7,\\\"persisted\\\":1732413607788}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"712d07f1-3585-465a-a6db-3c40a9edcde7\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413604993,\\\"created\\\":1732413603983,\\\"persisted\\\":1732413604993}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/examplePlans/ExamplePlanWithOrderedLanes.json",
    "content": "{\n  \"Groups\": [\n    {\n      \"name\": \"Group 1\"\n    },\n    {\n      \"name\": \"Group 2\"\n    }\n  ],\n  \"Group 2\": [\n    {\n      \"name\": \"Past event 3\",\n      \"start\": 1660493208000,\n      \"end\": 1660503981000,\n      \"type\": \"Group 2\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    },\n    {\n      \"name\": \"Past event 4\",\n      \"start\": 1660579608000,\n      \"end\": 1660624108000,\n      \"type\": \"Group 2\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    },\n    {\n      \"name\": \"Past event 5\",\n      \"start\": 1660666008000,\n      \"end\": 1660681529000,\n      \"type\": \"Group 2\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    }\n  ],\n  \"Group 1\": [\n    {\n      \"name\": \"Past event 1\",\n      \"start\": 1660320408000,\n      \"end\": 1660343797000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    },\n    {\n      \"name\": \"Past event 2\",\n      \"start\": 1660406808000,\n      \"end\": 1660429160000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    }\n  ]\n}\n"
  },
  {
    "path": "e2e/test-data/examplePlans/ExamplePlan_Large.json",
    "content": "{\n  \"Lorem\": [\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829001\",\n      \"name\": \"Lorem ipsum dolor 2023-03-15 21:00\",\n      \"start\": 1678914000000,\n      \"end\": 1678920203000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829002\",\n      \"name\": \"Nam vehicula magna 2023-03-15 23:08\",\n      \"start\": 1678921680000,\n      \"end\": 1678927886000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829003\",\n      \"name\": \"Praesent pharetra risus 2023-03-15 23:28\",\n      \"start\": 1678922880000,\n      \"end\": 1678927341000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829004\",\n      \"name\": \"Duis eget arcu 2023-03-16 01:30\",\n      \"start\": 1678930200000,\n      \"end\": 1678933775000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829005\",\n      \"name\": \"Maecenas elementum purus 2023-03-16 02:00\",\n      \"start\": 1678932000000,\n      \"end\": 1678936537000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829006\",\n      \"name\": \"Praesent in mauris 2023-03-16 03:27\",\n      \"start\": 1678937220000,\n      \"end\": 1678943380000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829007\",\n      \"name\": \"Donec id nunc 2023-03-16 04:08\",\n      \"start\": 1678939680000,\n      \"end\": 1678942180000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829008\",\n      \"name\": \"Donec quis nunc 2023-03-16 04:45\",\n      \"start\": 1678941900000,\n      \"end\": 1678942146000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829009\",\n      \"name\": \"Maecenas in purus 2023-03-16 06:45\",\n      \"start\": 1678949100000,\n      \"end\": 1678954538000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829010\",\n      \"name\": \"Proin vehicula elit 2023-03-16 07:01\",\n      \"start\": 1678950060000,\n      \"end\": 1678955494000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829011\",\n      \"name\": \"Nunc ac lorem 2023-03-16 07:04\",\n      \"start\": 1678950240000,\n      \"end\": 1678957383000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829012\",\n      \"name\": \"Aenean mollis lorem 2023-03-16 07:55\",\n      \"start\": 1678953300000,\n      \"end\": 1678954984000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829013\",\n      \"name\": \"Nunc hendrerit ipsum 2023-03-16 08:53\",\n      \"start\": 1678956780000,\n      \"end\": 1678962831000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829014\",\n      \"name\": \"Vestibulum placerat velit 2023-03-16 10:35\",\n      \"start\": 1678962900000,\n      \"end\": 1678968570000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829015\",\n      \"name\": \"Praesent eu augue 2023-03-16 11:02\",\n      \"start\": 1678964520000,\n      \"end\": 1678966583000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829016\",\n      \"name\": \"Quisque mattis ligula 2023-03-16 11:53\",\n      \"start\": 1678967580000,\n      \"end\": 1678974730000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829017\",\n      \"name\": \"Quisque a lectus 2023-03-16 13:33\",\n      \"start\": 1678973580000,\n      \"end\": 1678977996000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829018\",\n      \"name\": \"Nam mattis risus 2023-03-16 15:02\",\n      \"start\": 1678978920000,\n      \"end\": 1678982935000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829019\",\n      \"name\": \"Aenean rutrum libero 2023-03-16 16:26\",\n      \"start\": 1678983960000,\n      \"end\": 1678990712000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829020\",\n      \"name\": \"Fusce tincidunt lorem 2023-03-16 17:45\",\n      \"start\": 1678988700000,\n      \"end\": 1678995915000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829021\",\n      \"name\": \"Donec et nibh 2023-03-16 19:09\",\n      \"start\": 1678993740000,\n      \"end\": 1678995474000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    }\n  ],\n  \"Ipsum\": [\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829022\",\n      \"name\": \"Praesent blandit urna 2023-03-16 20:10\",\n      \"start\": 1678997400000,\n      \"end\": 1679003749000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829023\",\n      \"name\": \"Nullam congue est 2023-03-16 21:57\",\n      \"start\": 1679003820000,\n      \"end\": 1679008250000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829024\",\n      \"name\": \"Sed sed enim 2023-03-16 22:34\",\n      \"start\": 1679006040000,\n      \"end\": 1679012412000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829025\",\n      \"name\": \"Donec tincidunt ante 2023-03-16 23:09\",\n      \"start\": 1679008140000,\n      \"end\": 1679011218000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829026\",\n      \"name\": \"Morbi bibendum tellus 2023-03-17 00:17\",\n      \"start\": 1679012220000,\n      \"end\": 1679014528000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829027\",\n      \"name\": \"Quisque id est 2023-03-17 01:59\",\n      \"start\": 1679018340000,\n      \"end\": 1679025535000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829028\",\n      \"name\": \"Vivamus imperdiet tellus 2023-03-17 04:01\",\n      \"start\": 1679025660000,\n      \"end\": 1679031500000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829029\",\n      \"name\": \"Pellentesque tempus augue 2023-03-17 04:10\",\n      \"start\": 1679026200000,\n      \"end\": 1679029090000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829030\",\n      \"name\": \"Donec vel mauris 2023-03-17 05:21\",\n      \"start\": 1679030460000,\n      \"end\": 1679033163000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829031\",\n      \"name\": \"Morbi ac ipsum 2023-03-17 06:54\",\n      \"start\": 1679036040000,\n      \"end\": 1679043127000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829032\",\n      \"name\": \"Sed elementum nisi 2023-03-17 07:26\",\n      \"start\": 1679037960000,\n      \"end\": 1679042424000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829033\",\n      \"name\": \"Proin at ipsum 2023-03-17 09:07\",\n      \"start\": 1679044020000,\n      \"end\": 1679044445000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829034\",\n      \"name\": \"Sed cursus ipsum 2023-03-17 11:12\",\n      \"start\": 1679051520000,\n      \"end\": 1679056773000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829035\",\n      \"name\": \"Nullam mollis purus 2023-03-17 11:42\",\n      \"start\": 1679053320000,\n      \"end\": 1679057845000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829036\",\n      \"name\": \"Aliquam vitae erat 2023-03-17 13:02\",\n      \"start\": 1679058120000,\n      \"end\": 1679059116000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829037\",\n      \"name\": \"Aenean congue orci 2023-03-17 13:48\",\n      \"start\": 1679060880000,\n      \"end\": 1679066284000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829038\",\n      \"name\": \"Praesent vel orci 2023-03-17 15:58\",\n      \"start\": 1679068680000,\n      \"end\": 1679070694000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829039\",\n      \"name\": \"In non diam 2023-03-17 16:19\",\n      \"start\": 1679069940000,\n      \"end\": 1679072641000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829040\",\n      \"name\": \"Suspendisse sit amet 2023-03-17 17:51\",\n      \"start\": 1679075460000,\n      \"end\": 1679078232000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829041\",\n      \"name\": \"Sed sit amet 2023-03-17 18:33\",\n      \"start\": 1679077980000,\n      \"end\": 1679083509000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829042\",\n      \"name\": \"Donec dictum justo 2023-03-17 19:17\",\n      \"start\": 1679080620000,\n      \"end\": 1679086107000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829043\",\n      \"name\": \"Etiam vestibulum justo 2023-03-17 19:39\",\n      \"start\": 1679081940000,\n      \"end\": 1679086413000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829044\",\n      \"name\": \"Sed ullamcorper diam 2023-03-17 20:18\",\n      \"start\": 1679084280000,\n      \"end\": 1679088693000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829045\",\n      \"name\": \"Cras aliquet dolor 2023-03-17 20:27\",\n      \"start\": 1679084820000,\n      \"end\": 1679086883000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829046\",\n      \"name\": \"Duis et orci 2023-03-17 22:29\",\n      \"start\": 1679092140000,\n      \"end\": 1679092436000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829047\",\n      \"name\": \"Pellentesque quis augue 2023-03-17 23:01\",\n      \"start\": 1679094060000,\n      \"end\": 1679099511000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829048\",\n      \"name\": \"Mauris sed ligula 2023-03-17 23:46\",\n      \"start\": 1679096760000,\n      \"end\": 1679100276000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829049\",\n      \"name\": \"Vestibulum non quam 2023-03-17 23:52\",\n      \"start\": 1679097120000,\n      \"end\": 1679099465000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829050\",\n      \"name\": \"Nulla ut ante 2023-03-18 01:00\",\n      \"start\": 1679101200000,\n      \"end\": 1679105368000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829051\",\n      \"name\": \"Vivamus scelerisque est 2023-03-18 02:26\",\n      \"start\": 1679106360000,\n      \"end\": 1679107303000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829052\",\n      \"name\": \"Pellentesque viverra lectus 2023-03-18 04:28\",\n      \"start\": 1679113680000,\n      \"end\": 1679120663000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829053\",\n      \"name\": \"Vivamus placerat metus 2023-03-18 05:54\",\n      \"start\": 1679118840000,\n      \"end\": 1679120052000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829054\",\n      \"name\": \"Pellentesque nec quam 2023-03-18 06:20\",\n      \"start\": 1679120400000,\n      \"end\": 1679124820000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829055\",\n      \"name\": \"Ut semper dui 2023-03-18 08:11\",\n      \"start\": 1679127060000,\n      \"end\": 1679131793000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829056\",\n      \"name\": \"Praesent condimentum purus 2023-03-18 09:42\",\n      \"start\": 1679132520000,\n      \"end\": 1679136293000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829057\",\n      \"name\": \"Morbi varius leo 2023-03-18 10:18\",\n      \"start\": 1679134680000,\n      \"end\": 1679135462000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829058\",\n      \"name\": \"Etiam non nulla 2023-03-18 11:14\",\n      \"start\": 1679138040000,\n      \"end\": 1679144793000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829059\",\n      \"name\": \"Integer vitae quam 2023-03-18 11:27\",\n      \"start\": 1679138820000,\n      \"end\": 1679145512000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829060\",\n      \"name\": \"Ut bibendum sapien 2023-03-18 13:26\",\n      \"start\": 1679145960000,\n      \"end\": 1679151196000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829061\",\n      \"name\": \"Praesent in nibh 2023-03-18 14:09\",\n      \"start\": 1679148540000,\n      \"end\": 1679152592000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829062\",\n      \"name\": \"Mauris tempus risus 2023-03-18 15:53\",\n      \"start\": 1679154780000,\n      \"end\": 1679157952000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829063\",\n      \"name\": \"Vivamus eu massa 2023-03-18 16:10\",\n      \"start\": 1679155800000,\n      \"end\": 1679157039000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829064\",\n      \"name\": \"Aenean pharetra nulla 2023-03-18 17:08\",\n      \"start\": 1679159280000,\n      \"end\": 1679163693000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829065\",\n      \"name\": \"Vivamus elementum mauris 2023-03-18 18:54\",\n      \"start\": 1679165640000,\n      \"end\": 1679171765000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829066\",\n      \"name\": \"Curabitur vel nunc 2023-03-18 20:27\",\n      \"start\": 1679171220000,\n      \"end\": 1679176508000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829067\",\n      \"name\": \"Donec condimentum quam 2023-03-18 22:06\",\n      \"start\": 1679177160000,\n      \"end\": 1679179341000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829068\",\n      \"name\": \"Etiam tristique velit 2023-03-18 23:45\",\n      \"start\": 1679183100000,\n      \"end\": 1679183457000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    }\n  ],\n  \"Nonsectetur\": [\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829069\",\n      \"name\": \"Praesent at purus 2023-03-19 00:32\",\n      \"start\": 1679185920000,\n      \"end\": 1679188046000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829070\",\n      \"name\": \"Morbi convallis nunc 2023-03-19 01:34\",\n      \"start\": 1679189640000,\n      \"end\": 1679194168000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829071\",\n      \"name\": \"Vestibulum at augue 2023-03-19 03:04\",\n      \"start\": 1679195040000,\n      \"end\": 1679201691000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829072\",\n      \"name\": \"Morbi eget ante 2023-03-19 04:29\",\n      \"start\": 1679200140000,\n      \"end\": 1679200843000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829073\",\n      \"name\": \"Aenean mattis ante 2023-03-19 04:40\",\n      \"start\": 1679200800000,\n      \"end\": 1679203030000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829074\",\n      \"name\": \"Nam vitae ante 2023-03-19 04:43\",\n      \"start\": 1679200980000,\n      \"end\": 1679201798000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829075\",\n      \"name\": \"Suspendisse accumsan lacus 2023-03-19 06:24\",\n      \"start\": 1679207040000,\n      \"end\": 1679207797000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829076\",\n      \"name\": \"In rutrum mauris 2023-03-19 08:27\",\n      \"start\": 1679214420000,\n      \"end\": 1679216767000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829077\",\n      \"name\": \"Vivamus posuere tellus 2023-03-19 10:20\",\n      \"start\": 1679221200000,\n      \"end\": 1679226475000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829078\",\n      \"name\": \"Ut non turpis 2023-03-19 12:10\",\n      \"start\": 1679227800000,\n      \"end\": 1679229012000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829079\",\n      \"name\": \"Nunc tincidunt leo 2023-03-19 12:12\",\n      \"start\": 1679227920000,\n      \"end\": 1679229913000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829080\",\n      \"name\": \"Nunc egestas eros 2023-03-19 13:51\",\n      \"start\": 1679233860000,\n      \"end\": 1679235101000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829081\",\n      \"name\": \"Sed feugiat tortor 2023-03-19 14:25\",\n      \"start\": 1679235900000,\n      \"end\": 1679239630000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829082\",\n      \"name\": \"Pellentesque non dolor 2023-03-19 15:38\",\n      \"start\": 1679240280000,\n      \"end\": 1679245396000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829083\",\n      \"name\": \"Nullam ut lorem 2023-03-19 15:50\",\n      \"start\": 1679241000000,\n      \"end\": 1679244205000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829084\",\n      \"name\": \"Nam faucibus risus 2023-03-19 16:51\",\n      \"start\": 1679244660000,\n      \"end\": 1679245213000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829085\",\n      \"name\": \"Proin euismod ligula 2023-03-19 17:14\",\n      \"start\": 1679246040000,\n      \"end\": 1679249781000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829086\",\n      \"name\": \"Aenean congue orci 2023-03-19 17:23\",\n      \"start\": 1679246580000,\n      \"end\": 1679248687000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829087\",\n      \"name\": \"Praesent vel orci 2023-03-19 18:55\",\n      \"start\": 1679252100000,\n      \"end\": 1679256957000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829088\",\n      \"name\": \"In non diam 2023-03-19 19:06\",\n      \"start\": 1679252760000,\n      \"end\": 1679252878000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829089\",\n      \"name\": \"Suspendisse sit amet 2023-03-19 20:42\",\n      \"start\": 1679258520000,\n      \"end\": 1679259158000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829090\",\n      \"name\": \"Sed sit amet 2023-03-19 22:37\",\n      \"start\": 1679265420000,\n      \"end\": 1679268745000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829091\",\n      \"name\": \"Donec dictum justo 2023-03-19 23:19\",\n      \"start\": 1679267940000,\n      \"end\": 1679272128000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829092\",\n      \"name\": \"Etiam vestibulum justo 2023-03-19 23:53\",\n      \"start\": 1679269980000,\n      \"end\": 1679273096000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829093\",\n      \"name\": \"Sed ullamcorper diam 2023-03-20 00:15\",\n      \"start\": 1679271300000,\n      \"end\": 1679275297000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829094\",\n      \"name\": \"Cras aliquet dolor 2023-03-20 00:30\",\n      \"start\": 1679272200000,\n      \"end\": 1679278448000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829095\",\n      \"name\": \"Duis et orci 2023-03-20 02:00\",\n      \"start\": 1679277600000,\n      \"end\": 1679278981000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829096\",\n      \"name\": \"Pellentesque quis augue 2023-03-20 03:58\",\n      \"start\": 1679284680000,\n      \"end\": 1679291126000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829097\",\n      \"name\": \"Mauris sed ligula 2023-03-20 04:35\",\n      \"start\": 1679286900000,\n      \"end\": 1679294138000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829098\",\n      \"name\": \"Vestibulum non quam 2023-03-20 04:52\",\n      \"start\": 1679287920000,\n      \"end\": 1679291835000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829099\",\n      \"name\": \"Nulla ut ante 2023-03-20 06:10\",\n      \"start\": 1679292600000,\n      \"end\": 1679295800000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829100\",\n      \"name\": \"Vivamus scelerisque est 2023-03-20 07:38\",\n      \"start\": 1679297880000,\n      \"end\": 1679304545000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829101\",\n      \"name\": \"Pellentesque viverra lectus 2023-03-20 08:14\",\n      \"start\": 1679300040000,\n      \"end\": 1679305543000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829102\",\n      \"name\": \"Vivamus placerat metus 2023-03-20 09:58\",\n      \"start\": 1679306280000,\n      \"end\": 1679313528000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829103\",\n      \"name\": \"Pellentesque nec quam 2023-03-20 11:08\",\n      \"start\": 1679310480000,\n      \"end\": 1679311567000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829104\",\n      \"name\": \"Ut semper dui 2023-03-20 12:45\",\n      \"start\": 1679316300000,\n      \"end\": 1679321703000,\n      \"color\": \"#cc9900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829105\",\n      \"name\": \"Praesent condimentum purus 2023-03-20 14:32\",\n      \"start\": 1679322720000,\n      \"end\": 1679326451000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829106\",\n      \"name\": \"Morbi varius leo 2023-03-20 14:42\",\n      \"start\": 1679323320000,\n      \"end\": 1679329739000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829107\",\n      \"name\": \"Etiam non nulla 2023-03-20 16:10\",\n      \"start\": 1679328600000,\n      \"end\": 1679330910000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829108\",\n      \"name\": \"Integer vitae quam 2023-03-20 17:22\",\n      \"start\": 1679332920000,\n      \"end\": 1679333055000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829109\",\n      \"name\": \"Ut bibendum sapien 2023-03-20 17:28\",\n      \"start\": 1679333280000,\n      \"end\": 1679334377000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829110\",\n      \"name\": \"Praesent in nibh 2023-03-20 18:09\",\n      \"start\": 1679335740000,\n      \"end\": 1679339834000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829111\",\n      \"name\": \"Mauris tempus risus 2023-03-20 18:45\",\n      \"start\": 1679337900000,\n      \"end\": 1679338054000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829112\",\n      \"name\": \"Vivamus eu massa 2023-03-20 19:07\",\n      \"start\": 1679339220000,\n      \"end\": 1679344356000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829113\",\n      \"name\": \"Aenean pharetra nulla 2023-03-20 21:00\",\n      \"start\": 1679346000000,\n      \"end\": 1679349652000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829114\",\n      \"name\": \"Vivamus elementum mauris 2023-03-20 22:48\",\n      \"start\": 1679352480000,\n      \"end\": 1679358089000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829115\",\n      \"name\": \"Curabitur vel nunc 2023-03-21 00:54\",\n      \"start\": 1679360040000,\n      \"end\": 1679361673000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829116\",\n      \"name\": \"Donec condimentum quam 2023-03-21 02:14\",\n      \"start\": 1679364840000,\n      \"end\": 1679370470000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829117\",\n      \"name\": \"Etiam tristique velit 2023-03-21 03:12\",\n      \"start\": 1679368320000,\n      \"end\": 1679373263000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829118\",\n      \"name\": \"Praesent at purus 2023-03-21 05:19\",\n      \"start\": 1679375940000,\n      \"end\": 1679376500000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829119\",\n      \"name\": \"Morbi convallis nunc 2023-03-21 06:39\",\n      \"start\": 1679380740000,\n      \"end\": 1679383240000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829120\",\n      \"name\": \"Vestibulum at augue 2023-03-21 07:12\",\n      \"start\": 1679382720000,\n      \"end\": 1679383807000,\n      \"color\": \"#009900\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829121\",\n      \"name\": \"Morbi eget ante 2023-03-21 08:52\",\n      \"start\": 1679388720000,\n      \"end\": 1679394965000,\n      \"color\": \"#1A6398\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829122\",\n      \"name\": \"Aenean mattis ante 2023-03-21 10:52\",\n      \"start\": 1679395920000,\n      \"end\": 1679396480000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829123\",\n      \"name\": \"Nam vitae ante 2023-03-21 11:40\",\n      \"start\": 1679398800000,\n      \"end\": 1679401571000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829124\",\n      \"name\": \"Suspendisse accumsan lacus 2023-03-21 12:08\",\n      \"start\": 1679400480000,\n      \"end\": 1679402991000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829125\",\n      \"name\": \"In rutrum mauris 2023-03-21 12:44\",\n      \"start\": 1679402640000,\n      \"end\": 1679405235000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829126\",\n      \"name\": \"Vivamus posuere tellus 2023-03-21 13:27\",\n      \"start\": 1679405220000,\n      \"end\": 1679407622000,\n      \"color\": \"#28172\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829127\",\n      \"name\": \"Ut non turpis 2023-03-21 14:16\",\n      \"start\": 1679408160000,\n      \"end\": 1679415007000,\n      \"color\": \"#807C19\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829128\",\n      \"name\": \"Nunc tincidunt leo 2023-03-21 15:44\",\n      \"start\": 1679413440000,\n      \"end\": 1679418071000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829129\",\n      \"name\": \"Nunc egestas eros 2023-03-21 16:42\",\n      \"start\": 1679416920000,\n      \"end\": 1679421025000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829130\",\n      \"name\": \"Sed feugiat tortor 2023-03-21 17:04\",\n      \"start\": 1679418240000,\n      \"end\": 1679422217000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829131\",\n      \"name\": \"Pellentesque non dolor 2023-03-21 19:03\",\n      \"start\": 1679425380000,\n      \"end\": 1679428591000,\n      \"color\": \"#6700CD\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829132\",\n      \"name\": \"Nullam ut lorem 2023-03-21 20:25\",\n      \"start\": 1679430300000,\n      \"end\": 1679434622000,\n      \"color\": \"#CD6300\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829133\",\n      \"name\": \"Nam faucibus risus 2023-03-21 21:16\",\n      \"start\": 1679433360000,\n      \"end\": 1679434647000,\n      \"color\": \"#62640B\",\n      \"textColor\": \"#ffffff\"\n    },\n    {\n      \"uuid\": \"ef59547d-50ed-45c6-a5eb-a49629829134\",\n      \"name\": \"Proin euismod ligula 2023-03-21 23:00\",\n      \"start\": 1679439600000,\n      \"end\": 1679443378000,\n      \"color\": \"#398129A\",\n      \"textColor\": \"#ffffff\"\n    }\n  ]\n}\n"
  },
  {
    "path": "e2e/test-data/examplePlans/ExamplePlan_Small1.json",
    "content": "{\n  \"Group 1\": [\n    {\n      \"name\": \"Past event 1\",\n      \"start\": 1660320408000,\n      \"end\": 1660343797000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 1\n    },\n    {\n      \"name\": \"Past event 2\",\n      \"start\": 1660406808000,\n      \"end\": 1660429160000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 2\n    },\n    {\n      \"name\": \"Past event 3\",\n      \"start\": 1660493208000,\n      \"end\": 1660503981000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 3\n    },\n    {\n      \"name\": \"Past event 4\",\n      \"start\": 1660579608000,\n      \"end\": 1660624108000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 4\n    },\n    {\n      \"name\": \"Past event 5\",\n      \"start\": 1660666008000,\n      \"end\": 1660681529000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 5\n    }\n  ]\n}\n"
  },
  {
    "path": "e2e/test-data/examplePlans/ExamplePlan_Small2.json",
    "content": "{\n  \"Group 1\": [\n    {\n      \"name\": \"Group 1 event 1\",\n      \"start\": 1650320408000,\n      \"end\": 1660343797000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\"\n    },\n    {\n      \"name\": \"Group 1 event 2\",\n      \"start\": 1660005808000,\n      \"end\": 1660429160000,\n      \"type\": \"Group 1\",\n      \"color\": \"yellow\",\n      \"textColor\": \"white\"\n    }\n  ],\n  \"Group 2\": [\n    {\n      \"name\": \"Group 2 event 1\",\n      \"start\": 1660320408000,\n      \"end\": 1660420408000,\n      \"type\": \"Group 2\",\n      \"color\": \"green\",\n      \"textColor\": \"white\"\n    },\n    {\n      \"name\": \"Group 2 event 2\",\n      \"start\": 1660406808000,\n      \"end\": 1690429160000,\n      \"type\": \"Group 2\",\n      \"color\": \"blue\",\n      \"textColor\": \"white\"\n    }\n  ]\n}\n"
  },
  {
    "path": "e2e/test-data/examplePlans/ExamplePlan_Small3.json",
    "content": "{\n  \"Group 1\": [\n    {\n      \"name\": \"Time until birthday\",\n      \"start\": 1650320402000,\n      \"end\": 1660343797000,\n      \"type\": \"Group 1\",\n      \"color\": \"orange\",\n      \"textColor\": \"white\",\n      \"id\": 1\n    },\n    {\n      \"name\": \"Time until supper\",\n      \"start\": 1650320402000,\n      \"end\": 1650420410000,\n      \"type\": \"Group 2\",\n      \"color\": \"blue\",\n      \"textColor\": \"white\",\n      \"id\": 2\n    }\n  ],\n  \"Group 2\": [\n    {\n      \"name\": \"Time since the last time I ate\",\n      \"start\": 1650320102001,\n      \"end\": 1650320102001,\n      \"type\": \"Group 2\",\n      \"color\": \"green\",\n      \"textColor\": \"white\",\n      \"id\": 3\n    },\n    {\n      \"name\": \"Time since last accident\",\n      \"start\": 1650320102002,\n      \"end\": 1650320102002,\n      \"type\": \"Group 1\",\n      \"color\": \"yellow\",\n      \"textColor\": \"white\",\n      \"id\": 4\n    }\n  ]\n}\n"
  },
  {
    "path": "e2e/test-data/flexible_layout_with_child_layouts.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7},\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Flexible Layout\\\",\\\"type\\\":\\\"flexible-layout\\\",\\\"configuration\\\":{\\\"containers\\\":[{\\\"id\\\":\\\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\\\",\\\"frames\\\":[{\\\"id\\\":\\\"928f4a48-d080-4983-b3b0-8089b925c702\\\",\\\"domainObjectIdentifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"size\\\":100,\\\"noFrame\\\":false}],\\\"size\\\":50},{\\\"id\\\":\\\"de887070-1474-4ce6-8863-8002926d745e\\\",\\\"frames\\\":[],\\\"size\\\":50}],\\\"rowsLayout\\\":false},\\\"composition\\\":[{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"}],\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605725.7,\\\"persisted\\\":1732413607918},\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\":{\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"identifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606530,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413606530,\\\"persisted\\\":1732413606530},\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\":{\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"identifier\\\":{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"},\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413607918,\\\"persisted\\\":1732413607918}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Flexible Layout\\\",\\\"type\\\":\\\"flexible-layout\\\",\\\"configuration\\\":{\\\"containers\\\":[{\\\"id\\\":\\\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\\\",\\\"frames\\\":[{\\\"id\\\":\\\"928f4a48-d080-4983-b3b0-8089b925c702\\\",\\\"domainObjectIdentifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"size\\\":100,\\\"noFrame\\\":false}],\\\"size\\\":50},{\\\"id\\\":\\\"de887070-1474-4ce6-8863-8002926d745e\\\",\\\"frames\\\":[],\\\"size\\\":50}],\\\"rowsLayout\\\":false},\\\"composition\\\":[{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"}],\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605725.7,\\\"persisted\\\":1732413607918},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Flexible Layout\\\",\\\"type\\\":\\\"flexible-layout\\\",\\\"configuration\\\":{\\\"containers\\\":[{\\\"id\\\":\\\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\\\",\\\"frames\\\":[{\\\"id\\\":\\\"928f4a48-d080-4983-b3b0-8089b925c702\\\",\\\"domainObjectIdentifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"size\\\":100,\\\"noFrame\\\":false}],\\\"size\\\":50},{\\\"id\\\":\\\"de887070-1474-4ce6-8863-8002926d745e\\\",\\\"frames\\\":[],\\\"size\\\":50}],\\\"rowsLayout\\\":false},\\\"composition\\\":[{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"}],\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605725.7,\\\"persisted\\\":1732413607918}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413607918,\\\"persisted\\\":1732413607918},{\\\"identifier\\\":{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Flexible Layout\\\",\\\"type\\\":\\\"flexible-layout\\\",\\\"configuration\\\":{\\\"containers\\\":[{\\\"id\\\":\\\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\\\",\\\"frames\\\":[{\\\"id\\\":\\\"928f4a48-d080-4983-b3b0-8089b925c702\\\",\\\"domainObjectIdentifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"size\\\":100,\\\"noFrame\\\":false}],\\\"size\\\":50},{\\\"id\\\":\\\"de887070-1474-4ce6-8863-8002926d745e\\\",\\\"frames\\\":[],\\\"size\\\":50}],\\\"rowsLayout\\\":false},\\\"composition\\\":[{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"}],\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605725.7,\\\"persisted\\\":1732413607918},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35/20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 2\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413607918,\\\"persisted\\\":1732413607918}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606530,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413606530,\\\"persisted\\\":1732413606530},{\\\"identifier\\\":{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Parent Flexible Layout\\\",\\\"type\\\":\\\"flexible-layout\\\",\\\"configuration\\\":{\\\"containers\\\":[{\\\"id\\\":\\\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\\\",\\\"frames\\\":[{\\\"id\\\":\\\"928f4a48-d080-4983-b3b0-8089b925c702\\\",\\\"domainObjectIdentifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"size\\\":100,\\\"noFrame\\\":false}],\\\"size\\\":50},{\\\"id\\\":\\\"de887070-1474-4ce6-8863-8002926d745e\\\",\\\"frames\\\":[],\\\"size\\\":50}],\\\"rowsLayout\\\":false},\\\"composition\\\":[{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"20cad719-b961-457c-abcf-262944bdd1ce\\\",\\\"namespace\\\":\\\"\\\"}],\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413607918,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605725.7,\\\"persisted\\\":1732413607918},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35/a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"a6141275-b3a0-4768-8e4a-197f1cb809dd\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Child Layout 1\\\",\\\"type\\\":\\\"layout\\\",\\\"composition\\\":[],\\\"configuration\\\":{\\\"items\\\":[],\\\"layoutGrid\\\":[10,10]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate flexible layout with 2 child display layouts\\\\nchrome\\\",\\\"modified\\\":1732413606530,\\\"location\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"created\\\":1732413606530,\\\"persisted\\\":1732413606530}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"3b2f155b-ec69-48d5-ac58-76af10187a35\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605725.7,\\\"created\\\":1732413604014,\\\"persisted\\\":1732413605725.7}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/memory-leak-detection.json",
    "content": "{\"openmct\":{\"9224ac93-50af-4bb9-ac72-89a42b33f031\":{\"identifier\":{\"key\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"namespace\":\"\"},\"name\":\"Memory Leak detection\",\"type\":\"folder\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"},{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"},{\"key\":\"f0a154f6-c660-4f38-a386-044d09ccde1f\",\"namespace\":\"\"},{\"key\":\"7902af9e-2e7f-4f7f-96a6-33e01235dab6\",\"namespace\":\"\"},{\"key\":\"75769431-b069-44c6-b053-3d8488a6d259\",\"namespace\":\"\"},{\"key\":\"7de93d31-63b7-48d1-8cbc-052c6df393c0\",\"namespace\":\"\"},{\"key\":\"acb3f054-93f1-4464-958d-fb4ab1192bba\",\"namespace\":\"\"},{\"key\":\"91849735-bd56-4219-8ee3-e860ba5e1f52\",\"namespace\":\"\"},{\"key\":\"256c0f7c-fe8d-4bf3-a854-11f1ba7a8812\",\"namespace\":\"\"},{\"key\":\"84e63225-8fad-4e7b-94b9-30ab27d4e5b9\",\"namespace\":\"\"},{\"key\":\"7e1f5274-c7a6-45ac-bac4-48750206ab7f\",\"namespace\":\"\"},{\"key\":\"450bca86-457b-43b7-8940-8fb671a9a66b\",\"namespace\":\"\"},{\"key\":\"4c38e733-c3d8-4b9d-80e0-c1389ddf04f0\",\"namespace\":\"\"},{\"key\":\"a91e78a2-4c23-40e1-a68e-ccf0ae3afd03\",\"namespace\":\"\"},{\"key\":\"c7287d57-2ee2-42e3-825d-972979419865\",\"namespace\":\"\"},{\"key\":\"0eff8b35-2f80-4b33-a663-5e2728d5b164\",\"namespace\":\"\"},{\"key\":\"c2993869-bfdd-41e3-9715-05a5f197606d\",\"namespace\":\"\"},{\"key\":\"76e675bb-8bbf-4ad3-aa44-5863a631f21f\",\"namespace\":\"\"},{\"key\":\"4cd610a5-5afd-4c0d-a2b1-e5a18a1702ac\",\"namespace\":\"\"},{\"key\":\"c32e44b1-f6fc-46c8-a542-707c426515ca\",\"namespace\":\"\"},{\"key\":\"6de01ba3-b11f-4cc0-995c-f2d5c4b0cd4e\",\"namespace\":\"\"}],\"modified\":1702544029983,\"location\":\"mine\",\"created\":1702542980577,\"persisted\":1702544029984},\"4d65d346-898a-49fc-af04-7327eb58fa9b\":{\"identifier\":{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"},\"name\":\"1hz-swg\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1702542980781,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980578,\"persisted\":1702542980781},\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\":{\"identifier\":{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"},\"name\":\"overlay-plot-single-1hz-swg\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}}]},\"modified\":1702542980578,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980578,\"persisted\":1702542980578},\"f0a154f6-c660-4f38-a386-044d09ccde1f\":{\"identifier\":{\"key\":\"f0a154f6-c660-4f38-a386-044d09ccde1f\",\"namespace\":\"\"},\"name\":\"stacked-plot-single-1hz-swg\",\"type\":\"telemetry.plot.stacked\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[],\"yAxis\":{},\"xAxis\":{}},\"modified\":1702542980579,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980579,\"persisted\":1702542980579},\"7902af9e-2e7f-4f7f-96a6-33e01235dab6\":{\"identifier\":{\"key\":\"7902af9e-2e7f-4f7f-96a6-33e01235dab6\",\"namespace\":\"\"},\"name\":\"lad-table-single-1hz-swg\",\"type\":\"LadTable\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"modified\":1702542980581,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980580,\"persisted\":1702542980581},\"75769431-b069-44c6-b053-3d8488a6d259\":{\"identifier\":{\"key\":\"75769431-b069-44c6-b053-3d8488a6d259\",\"namespace\":\"\"},\"name\":\"telemetry-table-single-1hz-swg\",\"type\":\"table\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"columnWidths\":{},\"hiddenColumns\":{},\"columnOrder\":[],\"cellFormat\":{},\"autosize\":true},\"modified\":1702542980582,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980582,\"persisted\":1702542980582},\"7de93d31-63b7-48d1-8cbc-052c6df393c0\":{\"identifier\":{\"key\":\"7de93d31-63b7-48d1-8cbc-052c6df393c0\",\"namespace\":\"\"},\"name\":\"lad-table-set-single-1hz-swg\",\"type\":\"LadTableSet\",\"composition\":[{\"key\":\"7902af9e-2e7f-4f7f-96a6-33e01235dab6\",\"namespace\":\"\"}],\"modified\":1702542980583,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980583,\"persisted\":1702542980583},\"acb3f054-93f1-4464-958d-fb4ab1192bba\":{\"identifier\":{\"key\":\"acb3f054-93f1-4464-958d-fb4ab1192bba\",\"namespace\":\"\"},\"name\":\"notebook-memory-leak-detection-test\",\"type\":\"notebook\",\"configuration\":{\"defaultSort\":\"oldest\",\"entries\":{\"551e1ce9-0263-416d-90b0-41beebc9d50d\":{\"f536da89-c2e8-4b7f-b25d-5ee7c8ac8df3\":[{\"id\":\"entry-5e9bda4c-93cd-4fe7-9c0a-5ce419187fa9\",\"createdOn\":1686176929016,\"createdBy\":null,\"text\":\"First entry\",\"embeds\":[],\"modifiedBy\":\"Unknown\",\"modified\":1686176934867},{\"id\":\"entry-dc466597-2584-4ecd-9a78-038a60c6a2dd\",\"createdOn\":1686176935883,\"createdBy\":null,\"text\":\"Second entry\",\"embeds\":[],\"modifiedBy\":\"Unknown\",\"modified\":1686176942618}]},\"f298ac79-ed27-467e-ba9d-042094c8f8fa\":{\"156aabf8-db82-4b5a-b362-883d46b738cb\":[{\"id\":\"entry-70b81129-f42a-4747-955e-0c803f360deb\",\"createdOn\":1686176971570,\"createdBy\":null,\"text\":\"First entry of First Page of Second Section\",\"embeds\":[],\"modifiedBy\":\"Unknown\",\"modified\":1686176983771},{\"id\":\"entry-b0dc1556-c5ab-4b04-85c1-ef0d4a744306\",\"createdOn\":1686176992402,\"createdBy\":null,\"text\":\"Second entry of first page of second section with embedded object\",\"embeds\":[{\"bounds\":{\"start\":1670516888271,\"end\":1670518688271},\"createdOn\":1686176992389,\"createdBy\":null,\"cssClass\":\"icon-plot-stacked\",\"domainObject\":{\"identifier\":{\"key\":\"f0a154f6-c660-4f38-a386-044d09ccde1f\",\"namespace\":\"\"},\"name\":\"stacked-plot-single-1hz-swg\",\"type\":\"telemetry.plot.stacked\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[],\"yAxis\":{},\"xAxis\":{}},\"modified\":1686175150618,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1686175142052,\"persisted\":1686175150618},\"historicLink\":\"#/browse/mine/9224ac93-50af-4bb9-ac72-89a42b33f031/f0a154f6-c660-4f38-a386-044d09ccde1f?tc.mode=fixed&tc.startBound=1670516888271&tc.endBound=1670518688271&tc.timeSystem=utc&view=plot-stacked\",\"id\":\"embed-1686176992389\",\"name\":\"stacked-plot-single-1hz-swg\",\"snapshot\":{\"fullSizeImageObjectIdentifier\":{\"key\":\"ea82c76b-c720-4935-a26d-474be113538f\",\"namespace\":\"\"},\"thumbnailImage\":{\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAi1JREFUSEvt171vUlEYx/HvLS0UpIBBLViNotG46KaTJo0Dswt/QNOZMPMPMGNnwuhA0j+AODgZ20VNjE3aBBJfImCQl1reWugxj08hRkthKQx4xhvO+Zznd57LvdcyxhimMKz/8KRSt7rdrrHZbJPyBo5VKBRMIBCYPDx7zZXP500oFBpELbe1ZVnnHr2VyWRMJBI5d+hvYHp/IIlEwsTj8RmqePZup6lVnEwmTSwWm3xzTQ2eWtSpVMqsr69PPupwOGyy2ezk4eebr8zT1dXf8NEx5GrwvQ07FcjXQd4E5fqfY84CeYz4F+GWB6664Y4Pgi6QV0d5xrS6Ou9bA94U4UcbKi1oyVrym42NDRONRv+pWCYWGvChAtUObOag04PwdXDY4NGyoped44UlsM2CF3tQaMLQh8Rp8LIL7vkUvuaGx0GFe0YXPWsI/GkftkpDYIlW1hBY4v7a0Irfl+HgCJ4EFbrogIdXdAPjwHs1ODhU+F35lIr7sCC7VYVrHdip6jkJXGrqmQrsnAefY3TFffhtGbZLQ+B2V6sQWHboXlB45QJ47Qq7FuBZSOElO9jnzo5aYNn4x8oJvLa2ZtLp9GDWsdEmEnirCLs1heVauwd3fZA76XZpsNue8WE5rv3DIRX3YdnJ64LCUqVspH6o8HYR/E7tbO8ChLyjK5bkPv9UuHl0StQCSzO55hV++QXu+xW+uQStnsIPLsGKW+EbHlgc8U3Qh6Vp6x34BWH+QY6XeEwMAAAAAElFTkSuQmCC\"}},\"type\":\"f0a154f6-c660-4f38-a386-044d09ccde1f\"}],\"modifiedBy\":\"Unknown\",\"modified\":1686177011706}]}},\"imageMigrationVer\":\"v1\",\"pageTitle\":\"Page\",\"sections\":[{\"id\":\"551e1ce9-0263-416d-90b0-41beebc9d50d\",\"isDefault\":false,\"isSelected\":false,\"name\":\"First Section\",\"pages\":[{\"id\":\"f536da89-c2e8-4b7f-b25d-5ee7c8ac8df3\",\"isDefault\":false,\"isSelected\":false,\"name\":\"First Page\",\"pageTitle\":\"Page\"},{\"id\":\"96a12ca2-7903-4417-a7cd-1e6bd3140af2\",\"isDefault\":false,\"isSelected\":true,\"name\":\"Second Page\",\"pageTitle\":\"Page\"}],\"sectionTitle\":\"Section\"},{\"id\":\"f298ac79-ed27-467e-ba9d-042094c8f8fa\",\"isDefault\":false,\"isSelected\":true,\"name\":\"Second Section\",\"pages\":[{\"id\":\"156aabf8-db82-4b5a-b362-883d46b738cb\",\"isDefault\":false,\"isSelected\":true,\"name\":\"First Page Page\",\"pageTitle\":\"Page\"}],\"sectionTitle\":\"Section\"}],\"sectionTitle\":\"Section\",\"type\":\"General\"},\"modified\":1702542980584,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980584,\"persisted\":1702542980584},\"91849735-bd56-4219-8ee3-e860ba5e1f52\":{\"identifier\":{\"key\":\"91849735-bd56-4219-8ee3-e860ba5e1f52\",\"namespace\":\"\"},\"name\":\"tabbed-display-memory-leak-test\",\"type\":\"folder\",\"composition\":[\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"03c1700a-2845-4476-903e-b6d637216c97\"],\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"modified\":1702542980585,\"created\":1702542980585,\"persisted\":1702542980585},\"256c0f7c-fe8d-4bf3-a854-11f1ba7a8812\":{\"identifier\":{\"key\":\"256c0f7c-fe8d-4bf3-a854-11f1ba7a8812\",\"namespace\":\"\"},\"name\":\"display-layout-single-1hz-swg\",\"type\":\"layout\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"identifier\":{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"},\"x\":19,\"y\":6,\"width\":20,\"height\":5,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"a20b9455-504a-4e66-bcb0-9b3ec95cfe2f\"}],\"layoutGrid\":[10,10]},\"modified\":1702542980777,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980777,\"persisted\":1702542980777},\"84e63225-8fad-4e7b-94b9-30ab27d4e5b9\":{\"identifier\":{\"key\":\"84e63225-8fad-4e7b-94b9-30ab27d4e5b9\",\"namespace\":\"\"},\"name\":\"display-layout-single-overlay-plot\",\"type\":\"layout\",\"composition\":[{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":56,\"height\":29,\"x\":23,\"y\":13,\"identifier\":{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"b36dd532-166f-418a-9bb5-0f5962a8db51\"}],\"layoutGrid\":[10,10]},\"modified\":1702542980785,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702542980785,\"persisted\":1702542980785},\"7e1f5274-c7a6-45ac-bac4-48750206ab7f\":{\"identifier\":{\"key\":\"7e1f5274-c7a6-45ac-bac4-48750206ab7f\",\"namespace\":\"\"},\"name\":\"gauge-single-1hz-swg\",\"type\":\"gauge\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"gaugeController\":{\"gaugeType\":\"dial-filled\",\"isDisplayMinMax\":true,\"isDisplayCurVal\":true,\"isDisplayUnits\":true,\"isUseTelemetryLimits\":true,\"limitLow\":10,\"limitHigh\":90,\"max\":100,\"min\":0,\"precision\":2}},\"modified\":1702543146976,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543141431,\"persisted\":1702543146977},\"450bca86-457b-43b7-8940-8fb671a9a66b\":{\"identifier\":{\"key\":\"450bca86-457b-43b7-8940-8fb671a9a66b\",\"namespace\":\"\"},\"name\":\"timer-far-future\",\"type\":\"timer\",\"configuration\":{\"timerFormat\":\"long\",\"timestamp\":\"2050-05-28T08:09:02.000Z\",\"timezone\":\"UTC\",\"timerState\":\"started\"},\"modified\":1702543246644,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543246575,\"persisted\":1702543246644},\"4c38e733-c3d8-4b9d-80e0-c1389ddf04f0\":{\"identifier\":{\"key\":\"4c38e733-c3d8-4b9d-80e0-c1389ddf04f0\",\"namespace\":\"\"},\"name\":\"clock\",\"type\":\"clock\",\"configuration\":{\"baseFormat\":\"YYYY/MM/DD hh:mm:ss\",\"use24\":\"clock12\",\"timezone\":\"UTC\"},\"modified\":1702543265409,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543265409,\"persisted\":1702543265409},\"a91e78a2-4c23-40e1-a68e-ccf0ae3afd03\":{\"identifier\":{\"key\":\"a91e78a2-4c23-40e1-a68e-ccf0ae3afd03\",\"namespace\":\"\"},\"name\":\"plan-generated\",\"type\":\"plan\",\"configuration\":{\"clipActivityNames\":false,\"swimlaneVisibility\":{\"send\":true,\"fort shells\":true,\"knowledge wear these\":true,\"popular dead\":true,\"master\":true,\"zulu through\":true,\"she\":true,\"smallest vessels\":true}},\"selectFile\":{\"name\":\"plan.json\",\"body\":\"{\\n    \\\"send\\\": [\\n        {\\n            \\\"name\\\": \\\"anywhere copy friend pack\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702543908537,\\n            \\\"end\\\": 1702547448537,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"angle to chance largest effort\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702547448537,\\n            \\\"end\\\": 1702547475537,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"map mad carefully\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702547475537,\\n            \\\"end\\\": 1702547547537,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"over forget cell flight cave origin mind liquid\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702547547537,\\n            \\\"end\\\": 1702549287537,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"level sand leave rather wrote notice teeth piece\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702549287537,\\n            \\\"end\\\": 1702549351537,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"change unit away atom spell characteristic rock\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702549351537,\\n            \\\"end\\\": 1702554811537,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"ordinary series right fought discovery information\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702554811537,\\n            \\\"end\\\": 1702554861537,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"put sharp proud when thus food help\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702554861537,\\n            \\\"end\\\": 1702556661537,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"loss burst\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702556661537,\\n            \\\"end\\\": 1702556781537,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"went finally president little smell sunlight sheet race\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702556781537,\\n            \\\"end\\\": 1702558041537,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"charge\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558041537,\\n            \\\"end\\\": 1702558048537,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"year this itself\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558048537,\\n            \\\"end\\\": 1702558127537,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"circus several\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558127537,\\n            \\\"end\\\": 1702558187537,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"peace remain fill\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558187537,\\n            \\\"end\\\": 1702558221537,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"government dropped night court sign wrote dirty\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558221537,\\n            \\\"end\\\": 1702558761537,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"coach push program touch\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558761537,\\n            \\\"end\\\": 1702558830537,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"laugh solve western southern\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558830537,\\n            \\\"end\\\": 1702558856537,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pound\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702558856537,\\n            \\\"end\\\": 1702564436537,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fuel judge shall hard arrow stared\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702564436537,\\n            \\\"end\\\": 1702568756537,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"receive require hello highway reason appearance\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702568756537,\\n            \\\"end\\\": 1702568824537,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"broke his poor which habit describe darkness\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702568824537,\\n            \\\"end\\\": 1702573324537,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"answer over lift have ice cast\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702573324537,\\n            \\\"end\\\": 1702573360537,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cattle news along graph dull\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702573360537,\\n            \\\"end\\\": 1702577920537,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"plane\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702577920537,\\n            \\\"end\\\": 1702583620537,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"duty captured storm frame list of strip\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702583620537,\\n            \\\"end\\\": 1702583708537,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"little oil learn president everyone uncle question\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702583708537,\\n            \\\"end\\\": 1702586768537,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"putting\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702586768537,\\n            \\\"end\\\": 1702591148537,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"noun also\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702591148537,\\n            \\\"end\\\": 1702594748537,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"map correct flew lesson organization note earth alive\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702594748537,\\n            \\\"end\\\": 1702599608537,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"story\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599608537,\\n            \\\"end\\\": 1702599659537,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"branch court this asleep sight strange\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599659537,\\n            \\\"end\\\": 1702599731537,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"own letter pride plate arrange use hair active\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599731537,\\n            \\\"end\\\": 1702599733537,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"religious discover among consider radio sweet\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599733537,\\n            \\\"end\\\": 1702599788537,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tape copy imagine breath\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599788537,\\n            \\\"end\\\": 1702599798537,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wind\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702599798537,\\n            \\\"end\\\": 1702603218537,\\n            \\\"color\\\": \\\"#FF6633\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"deer colony pleasure city loose sing magnet fifteen\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702603218537,\\n            \\\"end\\\": 1702604358537,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"whatever\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702604358537,\\n            \\\"end\\\": 1702604658537,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"page dirt call upon\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702604658537,\\n            \\\"end\\\": 1702604675537,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"stems only tall growth\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702604675537,\\n            \\\"end\\\": 1702604751537,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"right basis fox\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702604751537,\\n            \\\"end\\\": 1702604771537,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shall\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702604771537,\\n            \\\"end\\\": 1702609451537,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"damage will alive follow traffic beauty sold thou\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702609451537,\\n            \\\"end\\\": 1702609479537,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"area forgotten birth key stairs\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702609479537,\\n            \\\"end\\\": 1702609899537,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"nearer add area giant been gulf vegetable\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702609899537,\\n            \\\"end\\\": 1702613199537,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pass meat after sight\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702613199537,\\n            \\\"end\\\": 1702618479537,\\n            \\\"color\\\": \\\"#FF6633\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"heat grain pig coach understanding lie\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702618479537,\\n            \\\"end\\\": 1702621539537,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"southern promised glass\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702621539537,\\n            \\\"end\\\": 1702623879537,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dance leave manner mission pilot\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702623879537,\\n            \\\"end\\\": 1702623999537,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sitting ride example\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702623999537,\\n            \\\"end\\\": 1702624095537,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rain rate rope give moving mysterious again\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702624095537,\\n            \\\"end\\\": 1702629375537,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"act program jump bank bread satellites snow\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702629375537,\\n            \\\"end\\\": 1702629472537,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"people rest flew bad bright green importance force\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702629472537,\\n            \\\"end\\\": 1702630492537,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"complete worth pour\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702630492537,\\n            \\\"end\\\": 1702630530537,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"smoke skill positive skin\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702630530537,\\n            \\\"end\\\": 1702630625537,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"search\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702630625537,\\n            \\\"end\\\": 1702630659537,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pay plate newspaper arrow arrive diameter\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702630659537,\\n            \\\"end\\\": 1702630685537,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fly act difficult oldest swimming serve\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702630685537,\\n            \\\"end\\\": 1702632785537,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"lift share\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702632785537,\\n            \\\"end\\\": 1702638785537,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mighty whole region massage\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702638785537,\\n            \\\"end\\\": 1702638813537,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"touch solution captured\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702638813537,\\n            \\\"end\\\": 1702638824537,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shelter mountain hole bite\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702638824537,\\n            \\\"end\\\": 1702642604537,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"stock\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702642604537,\\n            \\\"end\\\": 1702643684537,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trail family studying impossible dead\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702643684537,\\n            \\\"end\\\": 1702649504537,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"if control thread helpful dawn driven island wonder\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702649504537,\\n            \\\"end\\\": 1702649590537,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"older all leave using close ability jack\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702649590537,\\n            \\\"end\\\": 1702651810537,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"poem them liquid\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702651810537,\\n            \\\"end\\\": 1702651879537,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"final sweet raw hold likely\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702651879537,\\n            \\\"end\\\": 1702652899537,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hardly setting came\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702652899537,\\n            \\\"end\\\": 1702652912537,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"picture store helpful\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702652912537,\\n            \\\"end\\\": 1702657052537,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"plenty system break atmosphere\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702657052537,\\n            \\\"end\\\": 1702657115537,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"excitement apartment understanding gas stiff\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702657115537,\\n            \\\"end\\\": 1702657196537,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"long\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702657196537,\\n            \\\"end\\\": 1702659596537,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"girl bowl provide newspaper between month within\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702659596537,\\n            \\\"end\\\": 1702663256537,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"disappear against distant suppose onto\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702663256537,\\n            \\\"end\\\": 1702666976537,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"entire possible worth explore bicycle gray\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702666976537,\\n            \\\"end\\\": 1702666980537,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rising stand\\\",\\n            \\\"type\\\": \\\"send\\\",\\n            \\\"start\\\": 1702666980537,\\n            \\\"end\\\": 1702667063537,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ],\\n    \\\"fort shells\\\": [\\n        {\\n            \\\"name\\\": \\\"chain me\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702543308543,\\n            \\\"end\\\": 1702543342543,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wool\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702543342543,\\n            \\\"end\\\": 1702547482543,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"source took\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702547482543,\\n            \\\"end\\\": 1702547526543,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"storm large we tired cook rocky\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702547526543,\\n            \\\"end\\\": 1702550166543,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"piece hunter boy camp close\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550166543,\\n            \\\"end\\\": 1702550247543,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tired constantly experiment structure ordinary pretty stock\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550247543,\\n            \\\"end\\\": 1702550347543,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bet\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550347543,\\n            \\\"end\\\": 1702550415543,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"recent\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550415543,\\n            \\\"end\\\": 1702550443543,\\n            \\\"color\\\": \\\"#B33300\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"addition frog pleasure pot satisfied several\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550443543,\\n            \\\"end\\\": 1702550457543,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mill office silent direction branch\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550457543,\\n            \\\"end\\\": 1702550474543,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"social solar pine\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550474543,\\n            \\\"end\\\": 1702550502543,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dollar smell negative program seat society observe hard\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702550502543,\\n            \\\"end\\\": 1702552422543,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"facing\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702552422543,\\n            \\\"end\\\": 1702556922543,\\n            \\\"color\\\": \\\"#FF6633\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"vowel would especially love both\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702556922543,\\n            \\\"end\\\": 1702556977543,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"make stove nearly way order\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702556977543,\\n            \\\"end\\\": 1702559017543,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pen stranger mine individual\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702559017543,\\n            \\\"end\\\": 1702560157543,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"clothes instance hat\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702560157543,\\n            \\\"end\\\": 1702563757543,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"seeing tank expect camp never partly those symbol\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702563757543,\\n            \\\"end\\\": 1702563806543,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"soft\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702563806543,\\n            \\\"end\\\": 1702568066543,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sale\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702568066543,\\n            \\\"end\\\": 1702568068543,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"studying actually limited minerals rise\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702568068543,\\n            \\\"end\\\": 1702568105543,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"forest behavior either dig\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702568105543,\\n            \\\"end\\\": 1702573085543,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"ahead steam science jar signal farther shout grade\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702573085543,\\n            \\\"end\\\": 1702574225543,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"topic throat\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702574225543,\\n            \\\"end\\\": 1702575305543,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"flower village guess property\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702575305543,\\n            \\\"end\\\": 1702579685543,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fairly might union\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702579685543,\\n            \\\"end\\\": 1702580825543,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cowboy that\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702580825543,\\n            \\\"end\\\": 1702580849543,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rich apple effect hot army follow as\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702580849543,\\n            \\\"end\\\": 1702580925543,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hunt\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702580925543,\\n            \\\"end\\\": 1702580943543,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"current did till congress shelf meat\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702580943543,\\n            \\\"end\\\": 1702585743543,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"city ate neighbor\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702585743543,\\n            \\\"end\\\": 1702585747543,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cross lost\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702585747543,\\n            \\\"end\\\": 1702585779543,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fox finish they\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702585779543,\\n            \\\"end\\\": 1702586679543,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bad sort whether lot function steady\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702586679543,\\n            \\\"end\\\": 1702586753543,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"possibly dig\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702586753543,\\n            \\\"end\\\": 1702586784543,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"ill blank riding year article entirely shape\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702586784543,\\n            \\\"end\\\": 1702586807543,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"map silly perhaps pie oxygen\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702586807543,\\n            \\\"end\\\": 1702588967543,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"general ago\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702588967543,\\n            \\\"end\\\": 1702588969543,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"at putting actually speed\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702588969543,\\n            \\\"end\\\": 1702589029543,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"given attention whole importance paper produce ready fence\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702589029543,\\n            \\\"end\\\": 1702589068543,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"kill elephant fire boat class river include\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702589068543,\\n            \\\"end\\\": 1702589105543,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"per tip explain shade captured lost\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702589105543,\\n            \\\"end\\\": 1702593665543,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"gone\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702593665543,\\n            \\\"end\\\": 1702593719543,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"say\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702593719543,\\n            \\\"end\\\": 1702593761543,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"horn planning surprise central stepped\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702593761543,\\n            \\\"end\\\": 1702596101543,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"chain heart second line\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702596101543,\\n            \\\"end\\\": 1702601261543,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"feathers hunt plan\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702601261543,\\n            \\\"end\\\": 1702601296543,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"expression but valuable entire extra\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702601296543,\\n            \\\"end\\\": 1702604476543,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"please power greatly root\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702604476543,\\n            \\\"end\\\": 1702609816543,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"thick\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702609816543,\\n            \\\"end\\\": 1702609819543,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"herself\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702609819543,\\n            \\\"end\\\": 1702609846543,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"seldom car\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702609846543,\\n            \\\"end\\\": 1702612486543,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"putting unit\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702612486543,\\n            \\\"end\\\": 1702612578543,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"along strength review rough fence tired\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702612578543,\\n            \\\"end\\\": 1702616418543,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"spring while pass express color us leaf year\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702616418543,\\n            \\\"end\\\": 1702616506543,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rice southern spell slipped double held then pure\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702616506543,\\n            \\\"end\\\": 1702616603543,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"leaving stronger teacher\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702616603543,\\n            \\\"end\\\": 1702619063543,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"least comfortable pick care\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702619063543,\\n            \\\"end\\\": 1702619136543,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fence sat\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702619136543,\\n            \\\"end\\\": 1702619165543,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"push joy\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702619165543,\\n            \\\"end\\\": 1702619194543,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"inside fill death decide\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702619194543,\\n            \\\"end\\\": 1702619254543,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"with cold foot you log whose ball tape\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702619254543,\\n            \\\"end\\\": 1702624774543,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"one bag\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702624774543,\\n            \\\"end\\\": 1702625674543,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"classroom\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702625674543,\\n            \\\"end\\\": 1702630714543,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"copper stopped information shorter lower fought thick cutting\\\",\\n            \\\"type\\\": \\\"fort shells\\\",\\n            \\\"start\\\": 1702630714543,\\n            \\\"end\\\": 1702630797543,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ],\\n    \\\"knowledge wear these\\\": [\\n        {\\n            \\\"name\\\": \\\"sunlight large\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702543428544,\\n            \\\"end\\\": 1702543513544,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"atomic solve wagon stomach twenty\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702543513544,\\n            \\\"end\\\": 1702549453544,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cheese changing courage lying step deep\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702549453544,\\n            \\\"end\\\": 1702549483544,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"jet height immediately most post\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702549483544,\\n            \\\"end\\\": 1702549489544,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"for lamp everybody\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702549489544,\\n            \\\"end\\\": 1702552969544,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"per express\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702552969544,\\n            \\\"end\\\": 1702557709544,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pay shine return yesterday\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702557709544,\\n            \\\"end\\\": 1702557805544,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"goose level brass\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702557805544,\\n            \\\"end\\\": 1702557897544,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"motor piano\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702557897544,\\n            \\\"end\\\": 1702560957544,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"notice alive give mother other deer\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702560957544,\\n            \\\"end\\\": 1702560990544,\\n            \\\"color\\\": \\\"#B33300\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pony vowel gain furniture\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702560990544,\\n            \\\"end\\\": 1702565010544,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"noun meat fully be remember look\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702565010544,\\n            \\\"end\\\": 1702565101544,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rush yard pink acres hall daughter middle shine\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702565101544,\\n            \\\"end\\\": 1702569121544,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"jack human average current seeing\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702569121544,\\n            \\\"end\\\": 1702572001544,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mistake hurried value either\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702572001544,\\n            \\\"end\\\": 1702576441544,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"interest can piano instrument produce\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702576441544,\\n            \\\"end\\\": 1702579321544,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fifteen same nine\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702579321544,\\n            \\\"end\\\": 1702579334544,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"blind lay top unless coach rocket shallow dirt\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702579334544,\\n            \\\"end\\\": 1702579419544,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hay chair own solution ahead percent success\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702579419544,\\n            \\\"end\\\": 1702580499544,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"method court push fallen political anywhere guide\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702580499544,\\n            \\\"end\\\": 1702585539544,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"clock impossible few catch those leg wolf\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702585539544,\\n            \\\"end\\\": 1702585571544,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"feet fellow so since ball prepare\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702585571544,\\n            \\\"end\\\": 1702587131544,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wire\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702587131544,\\n            \\\"end\\\": 1702587189544,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fire fall ship earth\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702587189544,\\n            \\\"end\\\": 1702590429544,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fell\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702590429544,\\n            \\\"end\\\": 1702594689544,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wood quite graph week do official\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702594689544,\\n            \\\"end\\\": 1702597509544,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wheat sleep under front when force dawn series\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702597509544,\\n            \\\"end\\\": 1702597521544,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"him officer milk chamber pressure loss\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702597521544,\\n            \\\"end\\\": 1702597597544,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"principle home bar experiment\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702597597544,\\n            \\\"end\\\": 1702597638544,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"different\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702597638544,\\n            \\\"end\\\": 1702598058544,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"castle valuable everyone mail\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702598058544,\\n            \\\"end\\\": 1702599498544,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pen\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702599498544,\\n            \\\"end\\\": 1702603098544,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"slight term\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702603098544,\\n            \\\"end\\\": 1702603154544,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"thin younger join sheet bone camp television\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702603154544,\\n            \\\"end\\\": 1702605134544,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tribe\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702605134544,\\n            \\\"end\\\": 1702605206544,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"industry vast traffic\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702605206544,\\n            \\\"end\\\": 1702605227544,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"graph sheep large spread fastened species\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702605227544,\\n            \\\"end\\\": 1702609307544,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wash\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702609307544,\\n            \\\"end\\\": 1702609329544,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"protection recently slight drawn join short\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702609329544,\\n            \\\"end\\\": 1702609356544,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"to went atomic depend shout salmon underline sit\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702609356544,\\n            \\\"end\\\": 1702609405544,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"collect\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702609405544,\\n            \\\"end\\\": 1702609467544,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"college\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702609467544,\\n            \\\"end\\\": 1702610007544,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rod fairly\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702610007544,\\n            \\\"end\\\": 1702615167544,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"invented provide disease bound seldom running industry gain\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702615167544,\\n            \\\"end\\\": 1702618587544,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"frog silver\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702618587544,\\n            \\\"end\\\": 1702618664544,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"opportunity jet war ball coffee\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702618664544,\\n            \\\"end\\\": 1702621664544,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wealth sea tree brick\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621664544,\\n            \\\"end\\\": 1702621716544,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"ourselves back visitor ordinary pull\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621716544,\\n            \\\"end\\\": 1702621792544,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cause\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621792544,\\n            \\\"end\\\": 1702621796544,\\n            \\\"color\\\": \\\"#B33300\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"clothes fierce definition my neighborhood zebra writer studied\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621796544,\\n            \\\"end\\\": 1702621796544,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"elephant mass paper dead\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621796544,\\n            \\\"end\\\": 1702621852544,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"struck dropped situation cabin\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621852544,\\n            \\\"end\\\": 1702621852544,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"led chapter school vapor bound different\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621852544,\\n            \\\"end\\\": 1702621942544,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"birth city till construction\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702621942544,\\n            \\\"end\\\": 1702624582544,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"burst vote\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624582544,\\n            \\\"end\\\": 1702624601544,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"touch\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624601544,\\n            \\\"end\\\": 1702624632544,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"son quickly evidence this shine mass activity\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624632544,\\n            \\\"end\\\": 1702624689544,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trade cover\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624689544,\\n            \\\"end\\\": 1702624710544,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rhythm\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624710544,\\n            \\\"end\\\": 1702624748544,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"foreign comfortable provide canal atom made standard\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624748544,\\n            \\\"end\\\": 1702624827544,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"young\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702624827544,\\n            \\\"end\\\": 1702627347544,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tank pull rapidly raise consider four\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702627347544,\\n            \\\"end\\\": 1702632387544,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"constantly clean former planning talk mixture\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702632387544,\\n            \\\"end\\\": 1702632447544,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trunk mouth lovely observe friendly each city camera\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702632447544,\\n            \\\"end\\\": 1702635147544,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"them far exclaimed made research circle\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702635147544,\\n            \\\"end\\\": 1702635228544,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"faster calm chest\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702635228544,\\n            \\\"end\\\": 1702635408544,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"train law sun press mine salt dig breakfast\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702635408544,\\n            \\\"end\\\": 1702638528544,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shinning\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702638528544,\\n            \\\"end\\\": 1702638621544,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"after\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702638621544,\\n            \\\"end\\\": 1702643541544,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wore hold sat ice bus detail\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702643541544,\\n            \\\"end\\\": 1702648041544,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"growth heading flat here globe suggest lonely tired\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702648041544,\\n            \\\"end\\\": 1702651881544,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"city circle book jet block divide wide indicate\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702651881544,\\n            \\\"end\\\": 1702652001544,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"thin left according also her regular clock salmon\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702652001544,\\n            \\\"end\\\": 1702655121544,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"complete still mark sheet\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702655121544,\\n            \\\"end\\\": 1702655177544,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"plus spell thumb\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702655177544,\\n            \\\"end\\\": 1702655181544,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rest adjective rate girl time watch situation silent\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702655181544,\\n            \\\"end\\\": 1702655233544,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"everywhere row beauty recent union\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702655233544,\\n            \\\"end\\\": 1702655319544,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"having pack\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702655319544,\\n            \\\"end\\\": 1702657239544,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mother remain official effect fed bush specific\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702657239544,\\n            \\\"end\\\": 1702657310544,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"protection careful\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702657310544,\\n            \\\"end\\\": 1702657383544,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"next die\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702657383544,\\n            \\\"end\\\": 1702657442544,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"please please either month gray\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702657442544,\\n            \\\"end\\\": 1702660082544,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"importance now car wall breeze hour evening\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702660082544,\\n            \\\"end\\\": 1702660202544,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"accident spend round against\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702660202544,\\n            \\\"end\\\": 1702664222544,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"husband soil these rays\\\",\\n            \\\"type\\\": \\\"knowledge wear these\\\",\\n            \\\"start\\\": 1702664222544,\\n            \\\"end\\\": 1702664281544,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        }\\n    ],\\n    \\\"popular dead\\\": [\\n        {\\n            \\\"name\\\": \\\"triangle\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702543313545,\\n            \\\"end\\\": 1702543413545,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"firm pure impossible office baby toy sit\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702543413545,\\n            \\\"end\\\": 1702546473545,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"were else bat\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702546473545,\\n            \\\"end\\\": 1702546507545,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pick cookies card brother fair\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702546507545,\\n            \\\"end\\\": 1702546555545,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"turn sets depend flame cap for\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702546555545,\\n            \\\"end\\\": 1702546651545,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"like noted laid market available experience huge physical\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702546651545,\\n            \\\"end\\\": 1702550551545,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"heavy kitchen date sun trap\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702550551545,\\n            \\\"end\\\": 1702550586545,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"easily ranch ice almost\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702550586545,\\n            \\\"end\\\": 1702550826545,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"egg paid reach from however thick bat\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702550826545,\\n            \\\"end\\\": 1702552386545,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"maybe elephant evidence familiar adjective sleep six nervous\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702552386545,\\n            \\\"end\\\": 1702555566545,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"won highest putting\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702555566545,\\n            \\\"end\\\": 1702556226545,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"history saved know flag\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702556226545,\\n            \\\"end\\\": 1702560486545,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"slope bicycle\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702560486545,\\n            \\\"end\\\": 1702560556545,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"obtain troops realize pressure contain\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702560556545,\\n            \\\"end\\\": 1702563796545,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"foreign personal follow\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702563796545,\\n            \\\"end\\\": 1702563868545,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bare\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702563868545,\\n            \\\"end\\\": 1702563960545,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"soft welcome foot want village\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702563960545,\\n            \\\"end\\\": 1702567860545,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"became pitch breakfast\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702567860545,\\n            \\\"end\\\": 1702567911545,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"title chart position log slope ordinary pilot\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702567911545,\\n            \\\"end\\\": 1702567983545,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wind had detail check\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702567983545,\\n            \\\"end\\\": 1702568054545,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"both as simple gate tea mine golden sell\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702568054545,\\n            \\\"end\\\": 1702569914545,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"telephone bet\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702569914545,\\n            \\\"end\\\": 1702569945545,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"voyage effect prize soldier importance surprise\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702569945545,\\n            \\\"end\\\": 1702575645545,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"would offer map meet\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575645545,\\n            \\\"end\\\": 1702575687545,\\n            \\\"color\\\": \\\"#B33300\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bus drew seed forest\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575687545,\\n            \\\"end\\\": 1702575687545,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pet consider spider fifth careful\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575687545,\\n            \\\"end\\\": 1702575709545,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"win protection brave\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575709545,\\n            \\\"end\\\": 1702575785545,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pipe improve silk piano satisfied\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575785545,\\n            \\\"end\\\": 1702575803545,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wire necessary quite\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702575803545,\\n            \\\"end\\\": 1702580183545,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"drawn kitchen\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702580183545,\\n            \\\"end\\\": 1702582943545,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fact wealth slabs weak highway maybe whole border\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702582943545,\\n            \\\"end\\\": 1702582997545,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hard scared die complex seed careful rhythm\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702582997545,\\n            \\\"end\\\": 1702583050545,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"opportunity there write recently rubbed\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702583050545,\\n            \\\"end\\\": 1702585090545,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"afraid enter stronger needle try range waste he\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702585090545,\\n            \\\"end\\\": 1702585182545,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pony day load dig\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702585182545,\\n            \\\"end\\\": 1702587102545,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trade made provide meant earth certain\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702587102545,\\n            \\\"end\\\": 1702587121545,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"former fence spoken art disappear man glad\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702587121545,\\n            \\\"end\\\": 1702590541545,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"duty growth rising waste threw its slowly\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702590541545,\\n            \\\"end\\\": 1702590568545,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trap vapor primitive handsome entire continued beat\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702590568545,\\n            \\\"end\\\": 1702590592545,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"she till waste thin valley her wire\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702590592545,\\n            \\\"end\\\": 1702594492545,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"source west strike basic over tribe\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702594492545,\\n            \\\"end\\\": 1702594529545,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"task television oldest\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702594529545,\\n            \\\"end\\\": 1702597109545,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"similar\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702597109545,\\n            \\\"end\\\": 1702597185545,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shout slip grass similar wave fighting older mind\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702597185545,\\n            \\\"end\\\": 1702600065545,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sink all noted any\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702600065545,\\n            \\\"end\\\": 1702600134545,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"successful very opportunity pilot\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702600134545,\\n            \\\"end\\\": 1702604154545,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wonderful appropriate daily\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702604154545,\\n            \\\"end\\\": 1702604192545,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tell environment measure kill foreign money cent meal\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702604192545,\\n            \\\"end\\\": 1702604312545,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"asleep pound exactly\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702604312545,\\n            \\\"end\\\": 1702608632545,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"up loose pool composed twice master composition yourself\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702608632545,\\n            \\\"end\\\": 1702613672545,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dangerous final\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702613672545,\\n            \\\"end\\\": 1702617272545,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"truth frighten increase moon fast share either stretch\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702617272545,\\n            \\\"end\\\": 1702617325545,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"main carry\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702617325545,\\n            \\\"end\\\": 1702623145545,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"nearest few town eventually distant this\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702623145545,\\n            \\\"end\\\": 1702623242545,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sometime\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702623242545,\\n            \\\"end\\\": 1702623271545,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"buried hit step hearing till\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702623271545,\\n            \\\"end\\\": 1702628251545,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dozen good shot between\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702628251545,\\n            \\\"end\\\": 1702628337545,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bright tie although lying expect variety brick\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702628337545,\\n            \\\"end\\\": 1702628403545,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"vote came note talk wood\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702628403545,\\n            \\\"end\\\": 1702628463545,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"while private are property ancient rather greatly\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702628463545,\\n            \\\"end\\\": 1702629303545,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"green by writing glass sets tube sold warn\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702629303545,\\n            \\\"end\\\": 1702634463545,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"physical herd gave voice eventually lose\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702634463545,\\n            \\\"end\\\": 1702638303545,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"basis flight\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702638303545,\\n            \\\"end\\\": 1702644123545,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mass can electricity from size can\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702644123545,\\n            \\\"end\\\": 1702644173545,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"arrangement failed\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702644173545,\\n            \\\"end\\\": 1702649333545,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"combination various lead honor comfortable\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702649333545,\\n            \\\"end\\\": 1702649385545,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"silly determine direction three common strong\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702649385545,\\n            \\\"end\\\": 1702655025545,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"transportation skin\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655025545,\\n            \\\"end\\\": 1702655038545,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shallow fact willing\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655038545,\\n            \\\"end\\\": 1702655105545,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wherever happily frozen\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655105545,\\n            \\\"end\\\": 1702655132545,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"saved rope\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655132545,\\n            \\\"end\\\": 1702655208545,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"skin pool degree boy police image article darkness\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655208545,\\n            \\\"end\\\": 1702655293545,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"citizen applied salt soft bank\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655293545,\\n            \\\"end\\\": 1702655294545,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mail\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655294545,\\n            \\\"end\\\": 1702655375545,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"swam minute interest\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655375545,\\n            \\\"end\\\": 1702655396545,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"refer suppose attention fed steam\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702655396545,\\n            \\\"end\\\": 1702657016545,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"society matter promised\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702657016545,\\n            \\\"end\\\": 1702662296545,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trade slave careful band guide poor stock cent\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702662296545,\\n            \\\"end\\\": 1702662319545,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"willing fat south observe\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702662319545,\\n            \\\"end\\\": 1702662376545,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"had elephant length further rubber chosen partly every\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702662376545,\\n            \\\"end\\\": 1702662461545,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cost it\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702662461545,\\n            \\\"end\\\": 1702662495545,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"beneath walk taken rubber pocket location\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702662495545,\\n            \\\"end\\\": 1702668495545,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"careful seems combine cabin serve tales memory\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702668495545,\\n            \\\"end\\\": 1702668511545,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"song\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702668511545,\\n            \\\"end\\\": 1702672711545,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trip is food log stems price\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702672711545,\\n            \\\"end\\\": 1702672771545,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"made\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702672771545,\\n            \\\"end\\\": 1702676911545,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"clothes park die\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702676911545,\\n            \\\"end\\\": 1702676973545,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"clay known regular rock gray\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702676973545,\\n            \\\"end\\\": 1702676989545,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hurried\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702676989545,\\n            \\\"end\\\": 1702678309545,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fierce laid yet born me palace school\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702678309545,\\n            \\\"end\\\": 1702678325545,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"experience example horn race development\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702678325545,\\n            \\\"end\\\": 1702682105545,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"kids\\\",\\n            \\\"type\\\": \\\"popular dead\\\",\\n            \\\"start\\\": 1702682105545,\\n            \\\"end\\\": 1702687145545,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ],\\n    \\\"master\\\": [\\n        {\\n            \\\"name\\\": \\\"swept birth\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702543428548,\\n            \\\"end\\\": 1702543428548,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"told dropped silent surface herd throw\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702543428548,\\n            \\\"end\\\": 1702543511548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"six electric feature own\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702543511548,\\n            \\\"end\\\": 1702543563548,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"television gray\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702543563548,\\n            \\\"end\\\": 1702548963548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"gun consist he thick being several comfortable mirror\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702548963548,\\n            \\\"end\\\": 1702548996548,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"finally help straw zoo road\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702548996548,\\n            \\\"end\\\": 1702549023548,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"joy light independent\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702549023548,\\n            \\\"end\\\": 1702551183548,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"break west about farther measure south\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702551183548,\\n            \\\"end\\\": 1702551274548,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"last per taken equal\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702551274548,\\n            \\\"end\\\": 1702551340548,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"unless love rubbed fell\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702551340548,\\n            \\\"end\\\": 1702556380548,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"common\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702556380548,\\n            \\\"end\\\": 1702556392548,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"door facing eat test crowd\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702556392548,\\n            \\\"end\\\": 1702556425548,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"upward balloon burn public couple\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702556425548,\\n            \\\"end\\\": 1702559245548,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"excited troops government dream began plus\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702559245548,\\n            \\\"end\\\": 1702563325548,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"nails done dawn suppose river replace island\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702563325548,\\n            \\\"end\\\": 1702563362548,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shake speak examine\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702563362548,\\n            \\\"end\\\": 1702563415548,\\n            \\\"color\\\": \\\"#B33300\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"split ought area\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702563415548,\\n            \\\"end\\\": 1702568095548,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dull theory choose six five facing fact\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702568095548,\\n            \\\"end\\\": 1702571635548,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"lying\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702571635548,\\n            \\\"end\\\": 1702571645548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"was\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702571645548,\\n            \\\"end\\\": 1702571727548,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"proud explain fill time obtain represent kept truth\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702571727548,\\n            \\\"end\\\": 1702574427548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"condition ago\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702574427548,\\n            \\\"end\\\": 1702580067548,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"iron pain spite setting everyone\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702580067548,\\n            \\\"end\\\": 1702580116548,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"death city deeply hello practice important rich build\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702580116548,\\n            \\\"end\\\": 1702580179548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rise question for term although symbol tight\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702580179548,\\n            \\\"end\\\": 1702580232548,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"behind drink out deer dollar bend\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702580232548,\\n            \\\"end\\\": 1702581072548,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"history\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581072548,\\n            \\\"end\\\": 1702581162548,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"slowly wide task habit electricity\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581162548,\\n            \\\"end\\\": 1702581202548,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"answer\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581202548,\\n            \\\"end\\\": 1702581248548,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"castle fact deeply\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581248548,\\n            \\\"end\\\": 1702581251548,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"chief iron space forgot\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581251548,\\n            \\\"end\\\": 1702581289548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"vessels\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581289548,\\n            \\\"end\\\": 1702581358548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"by poet eager managed social answer sum\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581358548,\\n            \\\"end\\\": 1702581417548,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"aboard substance\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581417548,\\n            \\\"end\\\": 1702581499548,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sentence job\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702581499548,\\n            \\\"end\\\": 1702583959548,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fast enjoy vast sheep command entirely choose shoot\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702583959548,\\n            \\\"end\\\": 1702585519548,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pony\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702585519548,\\n            \\\"end\\\": 1702585617548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"smell partly trouble parallel service rays\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702585617548,\\n            \\\"end\\\": 1702587537548,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"character old usual slept\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587537548,\\n            \\\"end\\\": 1702587546548,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"lie dropped hand orange built raw cent\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587546548,\\n            \\\"end\\\": 1702587577548,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"closely human fog row partly massage\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587577548,\\n            \\\"end\\\": 1702587630548,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"time needle pupil outside across individual remember\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587630548,\\n            \\\"end\\\": 1702587679548,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"love any\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587679548,\\n            \\\"end\\\": 1702587741548,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"willing\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587741548,\\n            \\\"end\\\": 1702587759548,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"home said heart trade beautiful happily\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702587759548,\\n            \\\"end\\\": 1702591239548,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"evening\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702591239548,\\n            \\\"end\\\": 1702591250548,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"liquid bottle stems loss wish\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702591250548,\\n            \\\"end\\\": 1702591344548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"myself state tide\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702591344548,\\n            \\\"end\\\": 1702596504548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"local answer smile\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702596504548,\\n            \\\"end\\\": 1702599384548,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"flow bread trunk\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702599384548,\\n            \\\"end\\\": 1702599419548,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"element watch\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702599419548,\\n            \\\"end\\\": 1702599446548,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"your earlier dig pair\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702599446548,\\n            \\\"end\\\": 1702604126548,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"date\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702604126548,\\n            \\\"end\\\": 1702604666548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"captain escape\\\",\\n            \\\"type\\\": \\\"master\\\",\\n            \\\"start\\\": 1702604666548,\\n            \\\"end\\\": 1702604733548,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ],\\n    \\\"zulu through\\\": [\\n        {\\n            \\\"name\\\": \\\"money replace single clean never\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702543668548,\\n            \\\"end\\\": 1702543740548,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"this safety early previous line interior depth eager\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702543740548,\\n            \\\"end\\\": 1702546380548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"duty just perhaps\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702546380548,\\n            \\\"end\\\": 1702546419548,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"written lose combination swung birds spite\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702546419548,\\n            \\\"end\\\": 1702548099548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"sick supply merely fill whale friendly\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702548099548,\\n            \\\"end\\\": 1702552539548,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"gulf related add spend tried this felt\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702552539548,\\n            \\\"end\\\": 1702552572548,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"judge\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702552572548,\\n            \\\"end\\\": 1702554792548,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"remove storm choose his\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702554792548,\\n            \\\"end\\\": 1702555992548,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"smallest\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702555992548,\\n            \\\"end\\\": 1702556017548,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"symbol\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702556017548,\\n            \\\"end\\\": 1702557577548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hair offer\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702557577548,\\n            \\\"end\\\": 1702562737548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"this ball rhyme shut everywhere flow danger\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702562737548,\\n            \\\"end\\\": 1702563037548,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"written ants sitting\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702563037548,\\n            \\\"end\\\": 1702566937548,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"compound proud closer sink ran settle\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702566937548,\\n            \\\"end\\\": 1702567037548,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fighting gravity parent wonderful engine\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702567037548,\\n            \\\"end\\\": 1702567068548,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"deer please only threw mile clock\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702567068548,\\n            \\\"end\\\": 1702567094548,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"completely classroom\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702567094548,\\n            \\\"end\\\": 1702570394548,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bus grandfather able\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702570394548,\\n            \\\"end\\\": 1702575194548,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"liquid thousand object young physical before fifth somewhere\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702575194548,\\n            \\\"end\\\": 1702575217548,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bow more war\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702575217548,\\n            \\\"end\\\": 1702580977548,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"conversation stared country tight yard\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702580977548,\\n            \\\"end\\\": 1702586257548,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"law adventure crowd\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702586257548,\\n            \\\"end\\\": 1702587577548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"chart root seen plastic mixture\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587577548,\\n            \\\"end\\\": 1702587608548,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"against strength pole simply\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587608548,\\n            \\\"end\\\": 1702587663548,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"trace powerful\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587663548,\\n            \\\"end\\\": 1702587752548,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"zulu nose finest quickly maybe important greater\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587752548,\\n            \\\"end\\\": 1702587812548,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"growth ten farther situation\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587812548,\\n            \\\"end\\\": 1702587908548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"many\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587908548,\\n            \\\"end\\\": 1702587995548,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"came exchange burn dropped scared\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702587995548,\\n            \\\"end\\\": 1702589195548,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"teach\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702589195548,\\n            \\\"end\\\": 1702589243548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"industrial got prepare film ill fly prove\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702589243548,\\n            \\\"end\\\": 1702589303548,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"month driving friendly cup\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702589303548,\\n            \\\"end\\\": 1702589336548,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"report gradually scale wild saw getting across\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702589336548,\\n            \\\"end\\\": 1702592936548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"shall piano everyone tip\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702592936548,\\n            \\\"end\\\": 1702592945548,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"driving slave\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702592945548,\\n            \\\"end\\\": 1702596845548,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"carefully home cool\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702596845548,\\n            \\\"end\\\": 1702600685548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"provide hurt\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702600685548,\\n            \\\"end\\\": 1702600719548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"factor hidden crack gone\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702600719548,\\n            \\\"end\\\": 1702601079548,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"adventure square poem settle lead against\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702601079548,\\n            \\\"end\\\": 1702601118548,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"together eight corner threw card\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702601118548,\\n            \\\"end\\\": 1702605318548,\\n            \\\"color\\\": \\\"#99E6E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rope private daily born\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702605318548,\\n            \\\"end\\\": 1702607598548,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"draw nothing block were tried\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702607598548,\\n            \\\"end\\\": 1702611198548,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"planning lion scientist bush butter corn gift wrapped\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702611198548,\\n            \\\"end\\\": 1702611226548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"steel phrase plural older habit here tune search\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702611226548,\\n            \\\"end\\\": 1702611318548,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"learn broke atom frog grandfather\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702611318548,\\n            \\\"end\\\": 1702613898548,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"corner\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702613898548,\\n            \\\"end\\\": 1702617018548,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"leg silly loose sleep daily\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702617018548,\\n            \\\"end\\\": 1702617098548,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cook highway spell cave studying function\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702617098548,\\n            \\\"end\\\": 1702617131548,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"put\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702617131548,\\n            \\\"end\\\": 1702617133548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hello avoid angry\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702617133548,\\n            \\\"end\\\": 1702620193548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"driven rock voice never\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702620193548,\\n            \\\"end\\\": 1702620216548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pain throat labor\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702620216548,\\n            \\\"end\\\": 1702620273548,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"liquid solar blood attached vote\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702620273548,\\n            \\\"end\\\": 1702622673548,\\n            \\\"color\\\": \\\"#33991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"regular ill stay depth taught furniture trip\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702622673548,\\n            \\\"end\\\": 1702622696548,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"teach lady spoken giant greatest diameter spend\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702622696548,\\n            \\\"end\\\": 1702627796548,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cow talk\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702627796548,\\n            \\\"end\\\": 1702627857548,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"origin nation anyone division capital round pupil radio\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702627857548,\\n            \\\"end\\\": 1702632177548,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tales\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702632177548,\\n            \\\"end\\\": 1702632220548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fun chart hospital thank structure additional\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702632220548,\\n            \\\"end\\\": 1702634260548,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"market upon mile during over\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702634260548,\\n            \\\"end\\\": 1702634270548,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"beneath team does upward dish mighty place as\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702634270548,\\n            \\\"end\\\": 1702634289548,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"airplane information kind planned shape\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702634289548,\\n            \\\"end\\\": 1702634589548,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pilot fort vowel these discussion lot strong\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702634589548,\\n            \\\"end\\\": 1702634656548,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wild\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702634656548,\\n            \\\"end\\\": 1702636156548,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"settle needs play grabbed compound\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702636156548,\\n            \\\"end\\\": 1702641676548,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mighty\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702641676548,\\n            \\\"end\\\": 1702641685548,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"lunch wonderful eat pipe huge\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702641685548,\\n            \\\"end\\\": 1702641716548,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wonder drove numeral throughout dead property\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702641716548,\\n            \\\"end\\\": 1702646396548,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"if seldom task\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702646396548,\\n            \\\"end\\\": 1702646459548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"split especially hall sentence principal soldier\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702646459548,\\n            \\\"end\\\": 1702647119548,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"congress secret tide tin different believed\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702647119548,\\n            \\\"end\\\": 1702647196548,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"did how\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702647196548,\\n            \\\"end\\\": 1702647201548,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"letter news wide highway shoot at\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702647201548,\\n            \\\"end\\\": 1702651581548,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"event\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702651581548,\\n            \\\"end\\\": 1702655181548,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"direct burst division nine drink than neck\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702655181548,\\n            \\\"end\\\": 1702655183548,\\n            \\\"color\\\": \\\"#4D8000\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"careful sweet deep mile\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702655183548,\\n            \\\"end\\\": 1702655199548,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"union why soil truck famous amount species engineer\\\",\\n            \\\"type\\\": \\\"zulu through\\\",\\n            \\\"start\\\": 1702655199548,\\n            \\\"end\\\": 1702655281548,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ],\\n    \\\"she\\\": [\\n        {\\n            \\\"name\\\": \\\"tell last pale easily canal line principle fox\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702543308550,\\n            \\\"end\\\": 1702547868550,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"widely camp lot plan\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702547868550,\\n            \\\"end\\\": 1702553148550,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hay distant ground reader slightly environment\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702553148550,\\n            \\\"end\\\": 1702553202550,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"married supply\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702553202550,\\n            \\\"end\\\": 1702553225550,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"example winter needs blow compound rice\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702553225550,\\n            \\\"end\\\": 1702559045550,\\n            \\\"color\\\": \\\"#B366CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"wrong poet\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702559045550,\\n            \\\"end\\\": 1702562525550,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"old am fallen happened duty fighting\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702562525550,\\n            \\\"end\\\": 1702562567550,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"am instead whale taste wise\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702562567550,\\n            \\\"end\\\": 1702568207550,\\n            \\\"color\\\": \\\"#4DB3FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"disappear mission rich\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702568207550,\\n            \\\"end\\\": 1702568227550,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"although anyone\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702568227550,\\n            \\\"end\\\": 1702570447550,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"dig citizen lack week regular key\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702570447550,\\n            \\\"end\\\": 1702570477550,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"limited lift nearby past\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702570477550,\\n            \\\"end\\\": 1702571017550,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bottle native complete\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702571017550,\\n            \\\"end\\\": 1702573897550,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"chose spite easy slipped\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702573897550,\\n            \\\"end\\\": 1702577677550,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mouse vowel gradually\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702577677550,\\n            \\\"end\\\": 1702577754550,\\n            \\\"color\\\": \\\"#6680B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"aboard higher answer dot garden wrote fire\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702577754550,\\n            \\\"end\\\": 1702577769550,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"machinery attention explanation mud aboard herd feathers\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702577769550,\\n            \\\"end\\\": 1702581129550,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"close dear term customs leather plenty spider\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702581129550,\\n            \\\"end\\\": 1702584969550,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"picture loud past highway group exciting\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702584969550,\\n            \\\"end\\\": 1702585329550,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"steady fort remain troops wall\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702585329550,\\n            \\\"end\\\": 1702590129550,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tales fun rhythm identity development sugar\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702590129550,\\n            \\\"end\\\": 1702590221550,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"becoming\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702590221550,\\n            \\\"end\\\": 1702590234550,\\n            \\\"color\\\": \\\"#FF4D4D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"please energy exist apart follow mouth each most\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702590234550,\\n            \\\"end\\\": 1702593354550,\\n            \\\"color\\\": \\\"#FF33FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rain southern enemy\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702593354550,\\n            \\\"end\\\": 1702593434550,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"broad rocket number understanding common swing honor children\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702593434550,\\n            \\\"end\\\": 1702595894550,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"best thousand mostly bigger\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702595894550,\\n            \\\"end\\\": 1702598354550,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bowl\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702598354550,\\n            \\\"end\\\": 1702598386550,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"copy gasoline modern noun drive family\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702598386550,\\n            \\\"end\\\": 1702602346550,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bicycle young cast unhappy\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702602346550,\\n            \\\"end\\\": 1702602426550,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mouth coat\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702602426550,\\n            \\\"end\\\": 1702603746550,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"strip lead back if respect material rose\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702603746550,\\n            \\\"end\\\": 1702606446550,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mix theory\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702606446550,\\n            \\\"end\\\": 1702612266550,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"without\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702612266550,\\n            \\\"end\\\": 1702616526550,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"vast deep usually\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702616526550,\\n            \\\"end\\\": 1702616544550,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"to beauty\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702616544550,\\n            \\\"end\\\": 1702616544550,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"this steam\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702616544550,\\n            \\\"end\\\": 1702617984550,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"band outer sky lonely smell\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702617984550,\\n            \\\"end\\\": 1702622064550,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"stock jet\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702622064550,\\n            \\\"end\\\": 1702622096550,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"flag\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702622096550,\\n            \\\"end\\\": 1702626116550,\\n            \\\"color\\\": \\\"#FF99E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"lonely sentence chamber tropical arrange contain damage monkey\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626116550,\\n            \\\"end\\\": 1702626169550,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"diagram park baby cage does elephant\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626169550,\\n            \\\"end\\\": 1702626234550,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"can\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626234550,\\n            \\\"end\\\": 1702626265550,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"inside occur condition animal danger\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626265550,\\n            \\\"end\\\": 1702626278550,\\n            \\\"color\\\": \\\"#33FFCC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"breeze entire some cup direction series\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626278550,\\n            \\\"end\\\": 1702626311550,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rise why stream pool wash ring\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702626311550,\\n            \\\"end\\\": 1702627751550,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"east slipped aware nor gasoline beautiful circus\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702627751550,\\n            \\\"end\\\": 1702629731550,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"national strike willing least compound exchange whenever\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702629731550,\\n            \\\"end\\\": 1702630931550,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"police television fresh\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702630931550,\\n            \\\"end\\\": 1702631591550,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"store organized\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702631591550,\\n            \\\"end\\\": 1702635431550,\\n            \\\"color\\\": \\\"#CCFF1A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"industry remarkable page\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702635431550,\\n            \\\"end\\\": 1702635440550,\\n            \\\"color\\\": \\\"#3366E6\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"everywhere butter serve sheet chest metal machinery cannot\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702635440550,\\n            \\\"end\\\": 1702636820550,\\n            \\\"color\\\": \\\"#00B3E6\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"melted\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702636820550,\\n            \\\"end\\\": 1702637060550,\\n            \\\"color\\\": \\\"#FF3380\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"week visitor copy shelter now\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702637060550,\\n            \\\"end\\\": 1702637103550,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"cowboy raw cheese idea yourself block image lungs\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702637103550,\\n            \\\"end\\\": 1702639623550,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"flat not nearly certain adjective fog front has\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702639623550,\\n            \\\"end\\\": 1702641603550,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"piece instance slipped track be see center against\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702641603550,\\n            \\\"end\\\": 1702644603550,\\n            \\\"color\\\": \\\"#FFFF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"end worth visitor be atmosphere\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702644603550,\\n            \\\"end\\\": 1702644608550,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"firm\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702644608550,\\n            \\\"end\\\": 1702645688550,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"straight warm thus religious throw separate sense church\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702645688550,\\n            \\\"end\\\": 1702645731550,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"freedom across cut\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702645731550,\\n            \\\"end\\\": 1702645731550,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"consonant habit rocky locate\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702645731550,\\n            \\\"end\\\": 1702645787550,\\n            \\\"color\\\": \\\"#E6B333\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"rose summer inch\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702645787550,\\n            \\\"end\\\": 1702649267550,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"mission meant glass anybody cell situation peace indicate\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702649267550,\\n            \\\"end\\\": 1702651787550,\\n            \\\"color\\\": \\\"#E6FF80\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"satisfied music excellent better\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702651787550,\\n            \\\"end\\\": 1702651837550,\\n            \\\"color\\\": \\\"#66664D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"nine lovely disappear sunlight pay\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702651837550,\\n            \\\"end\\\": 1702652977550,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"flow dry\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702652977550,\\n            \\\"end\\\": 1702655977550,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"ground\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702655977550,\\n            \\\"end\\\": 1702659997550,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"effort changing provide hollow met recall apart\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702659997550,\\n            \\\"end\\\": 1702660007550,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"been cabin transportation pan period rather lift\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702660007550,\\n            \\\"end\\\": 1702660025550,\\n            \\\"color\\\": \\\"#999933\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"weak if best face\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702660025550,\\n            \\\"end\\\": 1702660094550,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"liquid dress club muscle enter\\\",\\n            \\\"type\\\": \\\"she\\\",\\n            \\\"start\\\": 1702660094550,\\n            \\\"end\\\": 1702660168550,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        }\\n    ],\\n    \\\"smallest vessels\\\": [\\n        {\\n            \\\"name\\\": \\\"importance copper magic led largest soldier\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702543316551,\\n            \\\"end\\\": 1702543359551,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"park seed let religious wet hurried line shinning\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702543359551,\\n            \\\"end\\\": 1702543659551,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"interest firm wonderful led bowl rear\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702543659551,\\n            \\\"end\\\": 1702543678551,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"while dish continent column lose green volume\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702543678551,\\n            \\\"end\\\": 1702547758551,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"fix feel cake\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702547758551,\\n            \\\"end\\\": 1702553758551,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"happily follow white hollow\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702553758551,\\n            \\\"end\\\": 1702557058551,\\n            \\\"color\\\": \\\"#E666B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bowl anybody spent wealth master tax stairs\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702557058551,\\n            \\\"end\\\": 1702557064551,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"within pictured layers old\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702557064551,\\n            \\\"end\\\": 1702557097551,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"magnet dozen\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702557097551,\\n            \\\"end\\\": 1702557142551,\\n            \\\"color\\\": \\\"#B3B31A\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"surface pride without duty outer\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702557142551,\\n            \\\"end\\\": 1702557143551,\\n            \\\"color\\\": \\\"#00E680\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"able still vowel\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702557143551,\\n            \\\"end\\\": 1702560263551,\\n            \\\"color\\\": \\\"#B34D4D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"onto local where\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702560263551,\\n            \\\"end\\\": 1702560328551,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"bat national ship little horse tears\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702560328551,\\n            \\\"end\\\": 1702560368551,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"throat hope trouble face duck hearing temperature\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702560368551,\\n            \\\"end\\\": 1702560437551,\\n            \\\"color\\\": \\\"#FF6633\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"increase jump\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702560437551,\\n            \\\"end\\\": 1702564517551,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"struggle dish massage pressure\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702564517551,\\n            \\\"end\\\": 1702565237551,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"floor massage yet stage correctly social bite\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702565237551,\\n            \\\"end\\\": 1702568237551,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tell express\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702568237551,\\n            \\\"end\\\": 1702568254551,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"actually relationship definition rod get win also\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702568254551,\\n            \\\"end\\\": 1702573894551,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"egg tonight exactly addition title express bigger\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702573894551,\\n            \\\"end\\\": 1702573914551,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"burst real belong percent fierce hope throughout cause\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702573914551,\\n            \\\"end\\\": 1702577214551,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"made inside different tight prize\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702577214551,\\n            \\\"end\\\": 1702578414551,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"hungry\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702578414551,\\n            \\\"end\\\": 1702580574551,\\n            \\\"color\\\": \\\"#CCCC00\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"daily there inch clock\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702580574551,\\n            \\\"end\\\": 1702580673551,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"was iron near divide north clothing provide telephone\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702580673551,\\n            \\\"end\\\": 1702582473551,\\n            \\\"color\\\": \\\"#80B300\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pleasant before way case twenty body appearance\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702582473551,\\n            \\\"end\\\": 1702585053551,\\n            \\\"color\\\": \\\"#E666FF\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"key dropped explore luck say roll southern\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702585053551,\\n            \\\"end\\\": 1702590573551,\\n            \\\"color\\\": \\\"#991AFF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"well creature fallen\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702590573551,\\n            \\\"end\\\": 1702590622551,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"tax nice determine paragraph usual act song party\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702590622551,\\n            \\\"end\\\": 1702591762551,\\n            \\\"color\\\": \\\"#4DB380\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"term castle\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702591762551,\\n            \\\"end\\\": 1702591830551,\\n            \\\"color\\\": \\\"#1AB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"result obtain have does mother\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702591830551,\\n            \\\"end\\\": 1702591847551,\\n            \\\"color\\\": \\\"#CC80CC\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"no human owner burst oil neck\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702591847551,\\n            \\\"end\\\": 1702591921551,\\n            \\\"color\\\": \\\"#66991A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"village wing couple are been spend out settle\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702591921551,\\n            \\\"end\\\": 1702592019551,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"point bar zero busy\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702592019551,\\n            \\\"end\\\": 1702592056551,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"advice leaving speak partly dear draw\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702592056551,\\n            \\\"end\\\": 1702592236551,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"teacher hope soil factor\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702592236551,\\n            \\\"end\\\": 1702596376551,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"managed related leaving\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702596376551,\\n            \\\"end\\\": 1702599676551,\\n            \\\"color\\\": \\\"#9900B3\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"eye pink refused\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702599676551,\\n            \\\"end\\\": 1702599726551,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"poor kids\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702599726551,\\n            \\\"end\\\": 1702599792551,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"main official also perfect party claws hung\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702599792551,\\n            \\\"end\\\": 1702599859551,\\n            \\\"color\\\": \\\"#FF1A66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"threw design simply design kitchen\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702599859551,\\n            \\\"end\\\": 1702605859551,\\n            \\\"color\\\": \\\"#CC9999\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"burn cross\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702605859551,\\n            \\\"end\\\": 1702605870551,\\n            \\\"color\\\": \\\"#E6B3B3\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"basket school however native look\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702605870551,\\n            \\\"end\\\": 1702605898551,\\n            \\\"color\\\": \\\"#4D80CC\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"slope dark my wood health all\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702605898551,\\n            \\\"end\\\": 1702605927551,\\n            \\\"color\\\": \\\"#E64D66\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"vowel division possible will giving join felt without\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702605927551,\\n            \\\"end\\\": 1702611627551,\\n            \\\"color\\\": \\\"#4D8066\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"identity\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702611627551,\\n            \\\"end\\\": 1702611685551,\\n            \\\"color\\\": \\\"#809900\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"congress ring porch thee\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702611685551,\\n            \\\"end\\\": 1702617565551,\\n            \\\"color\\\": \\\"#1AFF33\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"floor purple chain\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702617565551,\\n            \\\"end\\\": 1702617621551,\\n            \\\"color\\\": \\\"#809980\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"deeply people wagon be\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702617621551,\\n            \\\"end\\\": 1702617683551,\\n            \\\"color\\\": \\\"#999966\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"seen adventure purple page age\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702617683551,\\n            \\\"end\\\": 1702621583551,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"impossible\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702621583551,\\n            \\\"end\\\": 1702621659551,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"nearly himself waste store duck prepare golden tent\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702621659551,\\n            \\\"end\\\": 1702622139551,\\n            \\\"color\\\": \\\"#E6331A\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"locate alive sun serve might ability again\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702622139551,\\n            \\\"end\\\": 1702622231551,\\n            \\\"color\\\": \\\"#6666FF\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"develop dried wheel actual exercise market both whose\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702622231551,\\n            \\\"end\\\": 1702622831551,\\n            \\\"color\\\": \\\"#66E64D\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"discover fire\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702622831551,\\n            \\\"end\\\": 1702622893551,\\n            \\\"color\\\": \\\"#66994D\\\",\\n            \\\"textColor\\\": \\\"#ffffff\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"pack\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702622893551,\\n            \\\"end\\\": 1702626193551,\\n            \\\"color\\\": \\\"#FFB399\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        },\\n        {\\n            \\\"name\\\": \\\"white\\\",\\n            \\\"type\\\": \\\"smallest vessels\\\",\\n            \\\"start\\\": 1702626193551,\\n            \\\"end\\\": 1702626231551,\\n            \\\"color\\\": \\\"#99FF99\\\",\\n            \\\"textColor\\\": \\\"#000000\\\"\\n        }\\n    ]\\n}\\n\"},\"modified\":1702543347229,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543347106,\"persisted\":1702543347237},\"c7287d57-2ee2-42e3-825d-972979419865\":{\"identifier\":{\"key\":\"c7287d57-2ee2-42e3-825d-972979419865\",\"namespace\":\"\"},\"name\":\"gantt-chart\",\"type\":\"gantt-chart\",\"configuration\":{\"clipActivityNames\":true,\"swimlaneVisibility\":{\"send\":true,\"fort shells\":true,\"knowledge wear these\":true,\"popular dead\":true,\"master\":true,\"zulu through\":true,\"she\":true,\"smallest vessels\":true}},\"composition\":[{\"key\":\"a91e78a2-4c23-40e1-a68e-ccf0ae3afd03\",\"namespace\":\"\"}],\"modified\":1702543398882,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543388951,\"persisted\":1702543398883},\"0eff8b35-2f80-4b33-a663-5e2728d5b164\":{\"identifier\":{\"key\":\"0eff8b35-2f80-4b33-a663-5e2728d5b164\",\"namespace\":\"\"},\"name\":\"time-list\",\"type\":\"timelist\",\"configuration\":{\"sortOrderIndex\":0,\"futureEventsIndex\":1,\"futureEventsDurationIndex\":0,\"futureEventsDuration\":20,\"currentEventsIndex\":1,\"currentEventsDurationIndex\":0,\"currentEventsDuration\":20,\"pastEventsIndex\":1,\"pastEventsDurationIndex\":0,\"pastEventsDuration\":20,\"filter\":\"\"},\"composition\":[{\"key\":\"a91e78a2-4c23-40e1-a68e-ccf0ae3afd03\",\"namespace\":\"\"}],\"modified\":1702543429667,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543423050,\"persisted\":1702543429667},\"c2993869-bfdd-41e3-9715-05a5f197606d\":{\"identifier\":{\"key\":\"c2993869-bfdd-41e3-9715-05a5f197606d\",\"namespace\":\"\"},\"name\":\"graph-single-1hz-swg\",\"type\":\"telemetry.plot.bar-graph\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"barStyles\":{\"series\":{\"4d65d346-898a-49fc-af04-7327eb58fa9b\":{\"name\":\"1hz-swg\",\"type\":\"generator\",\"isAlias\":true,\"color\":\"#43b0ff\"}}},\"axes\":{\"xKey\":\"cos\",\"yKey\":\"wavelengths\"},\"useInterpolation\":\"linear\",\"useBar\":true},\"modified\":1702543763413,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543457649,\"persisted\":1702543763414},\"76e675bb-8bbf-4ad3-aa44-5863a631f21f\":{\"identifier\":{\"key\":\"76e675bb-8bbf-4ad3-aa44-5863a631f21f\",\"namespace\":\"\"},\"name\":\"scatter-plot-single-1hz-swg\",\"type\":\"telemetry.plot.scatter-plot\",\"composition\":[{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"styles\":{\"color\":\"#43b0ff\"},\"axes\":{\"xKey\":\"sin\",\"yKey\":\"cos\"},\"ranges\":{}},\"modified\":1702543833753,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543823658,\"persisted\":1702543833753},\"4cd610a5-5afd-4c0d-a2b1-e5a18a1702ac\":{\"identifier\":{\"key\":\"4cd610a5-5afd-4c0d-a2b1-e5a18a1702ac\",\"namespace\":\"\"},\"name\":\"web-page\",\"type\":\"webPage\",\"url\":\"http://www.nasa.gov\",\"modified\":1702543869323,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543869323,\"persisted\":1702543869323},\"c32e44b1-f6fc-46c8-a542-707c426515ca\":{\"identifier\":{\"key\":\"c32e44b1-f6fc-46c8-a542-707c426515ca\",\"namespace\":\"\"},\"name\":\"complex-display-layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"4c38e733-c3d8-4b9d-80e0-c1389ddf04f0\",\"namespace\":\"\"},{\"key\":\"c7287d57-2ee2-42e3-825d-972979419865\",\"namespace\":\"\"},{\"key\":\"c2993869-bfdd-41e3-9715-05a5f197606d\",\"namespace\":\"\"},{\"key\":\"7e1f5274-c7a6-45ac-bac4-48750206ab7f\",\"namespace\":\"\"},{\"key\":\"6de01ba3-b11f-4cc0-995c-f2d5c4b0cd4e\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":18,\"height\":11,\"x\":1,\"y\":2,\"identifier\":{\"key\":\"4c38e733-c3d8-4b9d-80e0-c1389ddf04f0\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"e9eccd6d-2cc4-4e8b-8456-69a05fdbe731\"},{\"width\":43,\"height\":15,\"x\":1,\"y\":14,\"identifier\":{\"key\":\"c7287d57-2ee2-42e3-825d-972979419865\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"c6320790-6521-4ad8-867c-bde447578e38\"},{\"width\":32,\"height\":18,\"x\":1,\"y\":30,\"identifier\":{\"key\":\"c2993869-bfdd-41e3-9715-05a5f197606d\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"6364cea5-31ac-4710-b00b-0b13861ebaa6\"},{\"width\":28,\"height\":18,\"x\":46,\"y\":3,\"identifier\":{\"key\":\"7e1f5274-c7a6-45ac-bac4-48750206ab7f\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"7ea3620c-934f-49ec-804e-ea78c0b16c64\"},{\"fill\":\"#666666\",\"stroke\":\"\",\"x\":22,\"y\":2,\"width\":10,\"height\":5,\"type\":\"box-view\",\"id\":\"6f5f7cad-3028-460a-a43b-ff5be110229e\"},{\"fill\":\"\",\"stroke\":\"\",\"color\":\"\",\"x\":22,\"y\":8,\"width\":10,\"height\":5,\"text\":\"Hello\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"text-view\",\"id\":\"e5df6fc8-31ff-4f79-9c13-9b5e76598042\"},{\"width\":41,\"height\":26,\"x\":34,\"y\":30,\"identifier\":{\"key\":\"6de01ba3-b11f-4cc0-995c-f2d5c4b0cd4e\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a97eac92-c2df-47c4-9c3e-c8b432a51688\"}],\"layoutGrid\":[10,10]},\"modified\":1702544098395,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702543946070,\"persisted\":1702544098396},\"6de01ba3-b11f-4cc0-995c-f2d5c4b0cd4e\":{\"identifier\":{\"key\":\"6de01ba3-b11f-4cc0-995c-f2d5c4b0cd4e\",\"namespace\":\"\"},\"name\":\"child-display-layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"acb3f054-93f1-4464-958d-fb4ab1192bba\",\"namespace\":\"\"},{\"key\":\"0eff8b35-2f80-4b33-a663-5e2728d5b164\",\"namespace\":\"\"},{\"key\":\"450bca86-457b-43b7-8940-8fb671a9a66b\",\"namespace\":\"\"},{\"key\":\"4cd610a5-5afd-4c0d-a2b1-e5a18a1702ac\",\"namespace\":\"\"},{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"},{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":28,\"height\":36,\"x\":2,\"y\":3,\"identifier\":{\"key\":\"acb3f054-93f1-4464-958d-fb4ab1192bba\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"aa4ffe19-21d0-45fc-97b6-5fd7a00f5366\"},{\"width\":32,\"height\":18,\"x\":32,\"y\":3,\"identifier\":{\"key\":\"0eff8b35-2f80-4b33-a663-5e2728d5b164\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"ed9f862f-9b53-4f64-a8a7-a927c4c1e20f\"},{\"width\":15,\"height\":8,\"x\":32,\"y\":23,\"identifier\":{\"key\":\"450bca86-457b-43b7-8940-8fb671a9a66b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"b7b0ea51-905d-429c-82d7-6e9d3310ff33\"},{\"width\":32,\"height\":18,\"x\":32,\"y\":33,\"identifier\":{\"key\":\"4cd610a5-5afd-4c0d-a2b1-e5a18a1702ac\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"1f411695-0170-439d-b44e-6f5790e48191\"},{\"width\":26,\"height\":15,\"x\":3,\"y\":42,\"identifier\":{\"key\":\"fd7a7943-bca2-4866-9d49-51ff0b59cdc0\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"f03d8fdb-adfb-4517-a488-cb948a0a441f\"},{\"identifier\":{\"key\":\"4d65d346-898a-49fc-af04-7327eb58fa9b\",\"namespace\":\"\"},\"x\":51,\"y\":24,\"width\":10,\"height\":5,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"c090c218-bd9e-4882-b5a9-316c851cc717\"}],\"layoutGrid\":[10,10]},\"modified\":1702544083000,\"location\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\",\"created\":1702544029968,\"persisted\":1702544083001},\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\":{\"identifier\":{\"namespace\":\"\",\"key\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\"},\"composition\":[\"397ea03e-bc97-4558-9350-bb08e971b0f5\",\"004440ea-2706-457f-964c-786ed7825e02\",\"57a02a0e-d95a-4a57-9452-2e38e7e26548\",\"3f066b75-f807-407f-8e48-3bc30d99d883\",\"00e2856c-eacc-435f-881f-afb371712e26\",\"5918aeb8-a4bb-461d-beff-755b4493bd90\",\"b0156377-4747-40d5-9b81-0571bfeff9a0\",\"7db16413-1c08-42ff-bc74-eac9a22e6a83\"],\"name\":\"Mock Spacecraft Telemetry\",\"type\":\"folder\",\"location\":\"91849735-bd56-4219-8ee3-e860ba5e1f52\",\"modified\":1702542980586,\"created\":1702542980586,\"persisted\":1702542980586},\"03c1700a-2845-4476-903e-b6d637216c97\":{\"identifier\":{\"namespace\":\"\",\"key\":\"03c1700a-2845-4476-903e-b6d637216c97\"},\"type\":\"folder\",\"location\":\"91849735-bd56-4219-8ee3-e860ba5e1f52\",\"composition\":[\"bcc3df01-4f62-4ecc-b1c2-075622029199\",\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",{\"key\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"namespace\":\"\"},{\"key\":\"ba405614-b7d1-4e06-9f41-a38524087583\",\"namespace\":\"\"},{\"key\":\"e2f83356-8688-4166-8335-90c0a1cd9676\",\"namespace\":\"\"},{\"key\":\"a3eadc8d-fe33-4a1d-b39a-d53a00f7031b\",\"namespace\":\"\"},{\"key\":\"e1f998ce-8e4d-4aef-abcb-e77ae69cc7c1\",\"namespace\":\"\"},{\"key\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"namespace\":\"\"}],\"name\":\"Components\",\"modified\":1702542980595,\"conditionalLabel\":\"\",\"created\":1702542980595,\"persisted\":1702542980595},\"397ea03e-bc97-4558-9350-bb08e971b0f5\":{\"identifier\":{\"namespace\":\"\",\"key\":\"397ea03e-bc97-4558-9350-bb08e971b0f5\"},\"telemetry\":{\"period\":10000000,\"amplitude\":\"10\",\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0.001},\"name\":\"Navcam Pan\",\"type\":\"generator\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980618,\"id\":\"397ea03e-bc97-4558-9350-bb08e971b0f5\",\"created\":1702542980586,\"persisted\":1702542980618},\"004440ea-2706-457f-964c-786ed7825e02\":{\"identifier\":{\"namespace\":\"\",\"key\":\"004440ea-2706-457f-964c-786ed7825e02\"},\"telemetry\":{\"period\":10000000,\"amplitude\":\"10\",\"offset\":\"0\",\"dataRateInHz\":1,\"phase\":3.14,\"randomness\":\"0.1\"},\"name\":\"Joint Velocity\",\"type\":\"generator\",\"id\":\"004440ea-2706-457f-964c-786ed7825e02\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980649,\"created\":1702542980587,\"persisted\":1702542980649},\"57a02a0e-d95a-4a57-9452-2e38e7e26548\":{\"identifier\":{\"namespace\":\"\",\"key\":\"57a02a0e-d95a-4a57-9452-2e38e7e26548\"},\"telemetry\":{\"period\":10000000,\"amplitude\":10,\"offset\":20,\"dataRateInHz\":1,\"phase\":1,\"randomness\":0.001},\"name\":\"Navcam Tilt\",\"type\":\"generator\",\"id\":\"57a02a0e-d95a-4a57-9452-2e38e7e26548\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980621,\"created\":1702542980588,\"persisted\":1702542980621},\"3f066b75-f807-407f-8e48-3bc30d99d883\":{\"identifier\":{\"namespace\":\"\",\"key\":\"3f066b75-f807-407f-8e48-3bc30d99d883\"},\"telemetry\":{\"period\":10000000,\"amplitude\":20,\"offset\":0,\"dataRateInHz\":1,\"phase\":3.14,\"randomness\":0.001},\"name\":\"Joint Position\",\"type\":\"generator\",\"id\":\"3f066b75-f807-407f-8e48-3bc30d99d883\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980629,\"created\":1702542980589,\"persisted\":1702542980629},\"00e2856c-eacc-435f-881f-afb371712e26\":{\"identifier\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"telemetry\":{\"period\":10000000,\"amplitude\":100,\"offset\":100,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0.001},\"name\":\"Battery SOC\",\"type\":\"generator\",\"id\":\"00e2856c-eacc-435f-881f-afb371712e26\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980634,\"created\":1702542980590,\"persisted\":1702542980634},\"5918aeb8-a4bb-461d-beff-755b4493bd90\":{\"identifier\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"telemetry\":{\"period\":\"1000\",\"amplitude\":\"25\",\"offset\":0,\"dataRateInHz\":1,\"phase\":0.5,\"randomness\":0},\"name\":\"Rover Roll\",\"type\":\"generator\",\"id\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980642,\"created\":1702542980591,\"persisted\":1702542980642},\"b0156377-4747-40d5-9b81-0571bfeff9a0\":{\"identifier\":{\"namespace\":\"\",\"key\":\"b0156377-4747-40d5-9b81-0571bfeff9a0\"},\"telemetry\":{\"period\":10000000,\"amplitude\":45,\"offset\":0,\"dataRateInHz\":1,\"phase\":1,\"randomness\":0.001},\"name\":\"Rover Pitch\",\"type\":\"generator\",\"id\":\"b0156377-4747-40d5-9b81-0571bfeff9a0\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980614,\"created\":1702542980592,\"persisted\":1702542980614},\"7db16413-1c08-42ff-bc74-eac9a22e6a83\":{\"identifier\":{\"namespace\":\"\",\"key\":\"7db16413-1c08-42ff-bc74-eac9a22e6a83\"},\"telemetry\":{\"period\":10000000,\"amplitude\":45,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0.001},\"name\":\"Rover Yaw\",\"type\":\"generator\",\"id\":\"7db16413-1c08-42ff-bc74-eac9a22e6a83\",\"location\":\"304e4b7e-9c2e-4b07-b6ea-a5f589046169\",\"modified\":1702542980607,\"created\":1702542980593,\"persisted\":1702542980607},\"bcc3df01-4f62-4ecc-b1c2-075622029199\":{\"identifier\":{\"namespace\":\"\",\"key\":\"bcc3df01-4f62-4ecc-b1c2-075622029199\"},\"composition\":[\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",{\"key\":\"e2f83356-8688-4166-8335-90c0a1cd9676\",\"namespace\":\"\"},{\"key\":\"a3eadc8d-fe33-4a1d-b39a-d53a00f7031b\",\"namespace\":\"\"}],\"keep_alive\":true,\"name\":\"tab-view-simple-memory-leak-test\",\"type\":\"tabs\",\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"modified\":1702542980597,\"configuration\":{\"objectStyles\":{}},\"conditionalLabel\":\"\",\"created\":1702542980597,\"currentTabIndex\":0,\"persisted\":1702542980597},\"c3215dc9-0cb1-4989-b57c-37450885fcc6\":{\"identifier\":{\"namespace\":\"\",\"key\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\"},\"composition\":[\"397ea03e-bc97-4558-9350-bb08e971b0f5\",\"004440ea-2706-457f-964c-786ed7825e02\",\"57a02a0e-d95a-4a57-9452-2e38e7e26548\",\"3f066b75-f807-407f-8e48-3bc30d99d883\",\"2cfa3b6f-2807-4a29-8185-d81e399ca177\",\"b5bdbdfd-e42a-461f-8a21-3719641b7919\",\"e589e5dc-df04-4dc9-afc1-8e632dee6293\",\"5403b1d6-3d34-4982-a9d2-32dc678d2597\",\"2e17648f-3657-489d-b377-86477c57aa1b\",\"fb26ebc1-9d64-4902-a47c-f33f57d4500f\",\"02027f64-40cc-4966-bbf4-5a7a868dcafb\",{\"key\":\"dfc3dea9-a220-4cd2-a363-00401b357ce0\",\"namespace\":\"\"},{\"key\":\"01a348fc-e0dd-41c3-a487-e6d4af0ccda2\",\"namespace\":\"\"},{\"key\":\"e5bb3185-952b-4b73-9891-fee2050256f3\",\"namespace\":\"\"},{\"key\":\"48816cba-16c9-48ad-9717-ec4edf1df8cd\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"stroke\":\"transparent\",\"x\":44,\"y\":10,\"width\":28,\"height\":21,\"url\":\"https://i.imgur.com/i1zz1we.png\",\"type\":\"image-view\",\"id\":\"3c59df18-90d2-4fe3-b617-1626af59b7ca\"},{\"x\":54,\"y\":29,\"x2\":54,\"y2\":24,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"6a23785c-11da-4d8b-bea5-314d48c21ce8\"},{\"identifier\":{\"namespace\":\"\",\"key\":\"397ea03e-bc97-4558-9350-bb08e971b0f5\"},\"x\":74,\"y\":13,\"width\":19,\"height\":3,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"fc4ce448-e1be-4d92-9f59-df23405793b4\"},{\"identifier\":{\"namespace\":\"\",\"key\":\"004440ea-2706-457f-964c-786ed7825e02\"},\"x\":74,\"y\":19,\"width\":19,\"height\":3,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"ac48013e-033f-4a8c-b671-5b2e4aaadd81\"},{\"identifier\":{\"namespace\":\"\",\"key\":\"57a02a0e-d95a-4a57-9452-2e38e7e26548\"},\"x\":74,\"y\":9,\"width\":19,\"height\":3,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"31789462-91b1-407a-9a76-1a83116b4f36\",\"showUnits\":false},{\"identifier\":{\"namespace\":\"\",\"key\":\"3f066b75-f807-407f-8e48-3bc30d99d883\"},\"x\":74,\"y\":23,\"width\":19,\"height\":3,\"displayMode\":\"all\",\"value\":\"sin\",\"stroke\":\"\",\"fill\":\"\",\"color\":\"\",\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"telemetry-view\",\"id\":\"155b2358-cb82-4baf-ba0d-d7c92f8f44ce\"},{\"x\":70,\"y\":29,\"x2\":54,\"y2\":29,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"a4146b13-056c-4849-9986-c69f24fb81d8\"},{\"x\":74,\"y\":10,\"x2\":71,\"y2\":10,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"63d6b2e5-653c-4a69-b72f-2e0524dcf428\"},{\"x\":67,\"y\":17,\"x2\":73,\"y2\":17,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"828ae816-54a7-4c7f-9ffe-2491f1a80347\"},{\"width\":47,\"height\":24,\"x\":0,\"y\":6,\"identifier\":{\"key\":\"2cfa3b6f-2807-4a29-8185-d81e399ca177\",\"namespace\":\"\"},\"hasFrame\":false,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"5e6da1c6-639d-4118-99a4-48d8bfa7a15a\"},{\"width\":47,\"height\":23,\"x\":0,\"y\":53,\"identifier\":{\"key\":\"b5bdbdfd-e42a-461f-8a21-3719641b7919\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"e538408d-2e87-40df-8c0f-ea852781e67a\"},{\"width\":47,\"height\":23,\"x\":0,\"y\":30,\"identifier\":{\"key\":\"e589e5dc-df04-4dc9-afc1-8e632dee6293\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"56699891-0144-44e6-a5fd-a0bf225109e8\"},{\"x\":74,\"y\":21,\"x2\":68,\"y2\":21,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"e05a1873-3c5d-4e68-8b62-1810230879bc\"},{\"x\":68,\"y\":27,\"x2\":68,\"y2\":21,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"c0e15084-350f-429b-8459-645d0fa337e9\"},{\"x\":71,\"y\":10,\"x2\":71,\"y2\":15,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"af41777d-3027-4626-baa6-2a09cd3983e0\"},{\"x\":67,\"y\":15,\"x2\":71,\"y2\":15,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"5a26d7a7-2905-43b0-bce2-b0b2b942c7bc\"},{\"x\":68,\"y\":27,\"x2\":56,\"y2\":27,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"a0c9206d-be9d-4c73-aeeb-fb48f530b0fe\"},{\"x\":56,\"y\":27,\"x2\":56,\"y2\":24,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"5e264631-1753-4960-9a94-1ee39774f04c\"},{\"x\":73,\"y\":14,\"x2\":73,\"y2\":17,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"c2b0b637-f29c-475d-8ec0-d82637e52a15\"},{\"x\":74,\"y\":14,\"x2\":73,\"y2\":14,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"5eba5795-58b1-4bb5-9dc3-a8dd9427fe98\"},{\"x\":70,\"y\":29,\"x2\":70,\"y2\":24,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"44e1d2dc-3bd4-4cfd-a78b-0df7557742ef\"},{\"x\":74,\"y\":24,\"x2\":70,\"y2\":24,\"stroke\":\"#666666\",\"type\":\"line-view\",\"id\":\"3696d4f1-1534-4494-848a-976fc85f478d\"},{\"width\":46,\"height\":23,\"x\":47,\"y\":53,\"identifier\":{\"key\":\"5403b1d6-3d34-4982-a9d2-32dc678d2597\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9a744b51-41cc-4fd6-bd14-94846a0a9ca3\"},{\"width\":47,\"height\":26,\"x\":0,\"y\":76,\"identifier\":{\"key\":\"2e17648f-3657-489d-b377-86477c57aa1b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"3cc086b3-c035-4fba-b71a-8f5e0af4f732\"},{\"width\":46,\"height\":23,\"x\":47,\"y\":30,\"identifier\":{\"key\":\"fb26ebc1-9d64-4902-a47c-f33f57d4500f\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"2b9dba11-5314-49b0-81fd-3758e467b1b0\"},{\"width\":46,\"height\":26,\"x\":47,\"y\":76,\"identifier\":{\"key\":\"02027f64-40cc-4966-bbf4-5a7a868dcafb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"172f6909-3efc-42a2-8568-933974d013a6\"},{\"width\":23,\"height\":5,\"x\":0,\"y\":0,\"identifier\":{\"key\":\"dfc3dea9-a220-4cd2-a363-00401b357ce0\",\"namespace\":\"\"},\"hasFrame\":false,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9f725dd0-44b8-43db-818e-1dce4cb1a376\"},{\"width\":23,\"height\":5,\"x\":24,\"y\":0,\"identifier\":{\"key\":\"01a348fc-e0dd-41c3-a487-e6d4af0ccda2\",\"namespace\":\"\"},\"hasFrame\":false,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"1dbfd63a-aa8b-4aae-9daa-1de1de8c1bcc\"},{\"width\":23,\"height\":5,\"x\":48,\"y\":0,\"identifier\":{\"key\":\"e5bb3185-952b-4b73-9891-fee2050256f3\",\"namespace\":\"\"},\"hasFrame\":false,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"74cb215d-851d-440a-8d94-2464e5f86547\"},{\"width\":22,\"height\":5,\"x\":72,\"y\":0,\"identifier\":{\"key\":\"48816cba-16c9-48ad-9717-ec4edf1df8cd\",\"namespace\":\"\"},\"hasFrame\":false,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"1a894143-a891-4430-9b78-5ece24886eab\"}],\"layoutGrid\":[10,10],\"layoutDimensions\":{},\"objectStyles\":{\"a4146b13-056c-4849-9986-c69f24fb81d8\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"6a23785c-11da-4d8b-bea5-314d48c21ce8\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"63d6b2e5-653c-4a69-b72f-2e0524dcf428\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"828ae816-54a7-4c7f-9ffe-2491f1a80347\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"e05a1873-3c5d-4e68-8b62-1810230879bc\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"c0e15084-350f-429b-8459-645d0fa337e9\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"af41777d-3027-4626-baa6-2a09cd3983e0\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"5a26d7a7-2905-43b0-bce2-b0b2b942c7bc\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"a0c9206d-be9d-4c73-aeeb-fb48f530b0fe\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"5e264631-1753-4960-9a94-1ee39774f04c\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"c2b0b637-f29c-475d-8ec0-d82637e52a15\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"5eba5795-58b1-4bb5-9dc3-a8dd9427fe98\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"44e1d2dc-3bd4-4cfd-a78b-0df7557742ef\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}},\"3696d4f1-1534-4494-848a-976fc85f478d\":{\"staticStyle\":{\"style\":{\"border\":\"1px solid #45818e\"}}}}},\"name\":\"display-layout-simple-telemetry\",\"type\":\"layout\",\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"modified\":1702542980598,\"id\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"conditionalLabel\":\"\",\"created\":1702542980598,\"persisted\":1702542980599},\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\":{\"identifier\":{\"namespace\":\"\",\"key\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\"},\"type\":\"folder\",\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"composition\":[\"80964387-1304-48d5-8b7f-3063e219fef6\",\"84b92e94-d5f2-4545-9bf9-775eeae700e1\",\"9cb58ae6-1c63-4576-aade-dd5ed3ac1637\",\"5f5a9e82-8e04-4cb4-8c46-37de6173ad5f\",\"a7f011ab-f324-4718-bbc7-9442d9fa8f06\",\"90a1ae05-f10d-48b2-b07c-74cdcca92cfc\"],\"name\":\"Imagery\",\"modified\":1702542980691,\"created\":1702542980691,\"persisted\":1702542980691},\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\":{\"identifier\":{\"key\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"namespace\":\"\"},\"name\":\"Imagery 2\",\"type\":\"folder\",\"composition\":[{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"},{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"}],\"modified\":1702542980717,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980717,\"persisted\":1702542980717},\"ba405614-b7d1-4e06-9f41-a38524087583\":{\"identifier\":{\"key\":\"ba405614-b7d1-4e06-9f41-a38524087583\",\"namespace\":\"\"},\"name\":\"flexible-layout-images-memory-leak-test\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"ca9e43d8-a5f4-4af8-bfb1-61d0382621e7\",\"frames\":[{\"id\":\"61f5dde1-b550-4b15-9191-41ebdfe11697\",\"domainObjectIdentifier\":{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},\"size\":33,\"noFrame\":false},{\"id\":\"23961564-8a92-4d5c-aecd-833d8d77884e\",\"domainObjectIdentifier\":{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},\"size\":34,\"noFrame\":false},{\"id\":\"674929bd-5217-43f1-b4a0-7c28ecca59ab\",\"domainObjectIdentifier\":{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},\"size\":33,\"noFrame\":false}],\"size\":50},{\"id\":\"8d6ecc4e-7f08-4252-8c53-5c6c00c90ff5\",\"frames\":[{\"id\":\"ba897a57-ddaa-428a-98dc-2139e8553896\",\"domainObjectIdentifier\":{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"},\"size\":33,\"noFrame\":false},{\"id\":\"bcbc2248-2853-4183-98a6-2e7e78f3da5d\",\"domainObjectIdentifier\":{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},\"size\":34,\"noFrame\":false},{\"id\":\"7cb521c7-d167-48fc-814a-857d37a15dd5\",\"domainObjectIdentifier\":{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"},\"size\":33,\"noFrame\":false}],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"},{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"}],\"modified\":1702542980742,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980742,\"persisted\":1702542980742},\"e2f83356-8688-4166-8335-90c0a1cd9676\":{\"identifier\":{\"key\":\"e2f83356-8688-4166-8335-90c0a1cd9676\",\"namespace\":\"\"},\"name\":\"display-layout-images-memory-leak-test\",\"type\":\"layout\",\"composition\":[{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"},{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":42,\"height\":28,\"x\":0,\"y\":0,\"identifier\":{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"36ec392c-241f-4b12-9d52-3a6cad878140\"},{\"width\":43,\"height\":28,\"x\":43,\"y\":0,\"identifier\":{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"63d36f95-8d41-46b7-8621-c34a4799d156\"},{\"width\":42,\"height\":31,\"x\":0,\"y\":29,\"identifier\":{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"d768d5d3-898d-4528-bbe0-1696b94016b5\"},{\"width\":42,\"height\":31,\"x\":43,\"y\":29,\"identifier\":{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"1d5acab7-f393-40a9-a27d-963de65f3742\"},{\"width\":42,\"height\":30,\"x\":0,\"y\":61,\"identifier\":{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"c030a252-75e7-42ca-b341-7a29f44eda3c\"},{\"width\":42,\"height\":30,\"x\":43,\"y\":61,\"identifier\":{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"b552f249-713a-46b4-8576-116a3b83c35a\"}],\"layoutGrid\":[10,10]},\"modified\":1702542980659,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980659,\"persisted\":1702542980659},\"a3eadc8d-fe33-4a1d-b39a-d53a00f7031b\":{\"identifier\":{\"key\":\"a3eadc8d-fe33-4a1d-b39a-d53a00f7031b\",\"namespace\":\"\"},\"name\":\"time-strip-telemetry-memory-leak-test\",\"type\":\"time-strip\",\"composition\":[{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"},{\"namespace\":\"\",\"key\":\"5403b1d6-3d34-4982-a9d2-32dc678d2597\"},{\"namespace\":\"\",\"key\":\"2e17648f-3657-489d-b377-86477c57aa1b\"},{\"namespace\":\"\",\"key\":\"e589e5dc-df04-4dc9-afc1-8e632dee6293\"},{\"namespace\":\"\",\"key\":\"2cfa3b6f-2807-4a29-8185-d81e399ca177\"}],\"configuration\":{\"useIndependentTime\":false,\"timeOptions\":{\"clockOffsets\":{\"start\":-900000,\"end\":30000},\"fixedOffsets\":{\"start\":1645902914886,\"end\":1645903844886},\"mode\":{\"key\":\"local\"}}},\"modified\":1702542980682,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980682,\"persisted\":1702542980682},\"e1f998ce-8e4d-4aef-abcb-e77ae69cc7c1\":{\"identifier\":{\"key\":\"e1f998ce-8e4d-4aef-abcb-e77ae69cc7c1\",\"namespace\":\"\"},\"name\":\"flexible-layout-plots-memory-leak-test\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"4ff28bc6-a480-4efa-82f5-4496e3a68455\",\"frames\":[{\"id\":\"a75001d9-9bd1-4508-aefc-296ca84fc028\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"5403b1d6-3d34-4982-a9d2-32dc678d2597\"},\"size\":26,\"noFrame\":false},{\"id\":\"3b146612-3c23-45c3-ad18-813b6641443a\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"e589e5dc-df04-4dc9-afc1-8e632dee6293\"},\"size\":25,\"noFrame\":false},{\"id\":\"9844318c-da0d-4839-83e2-ff42fd3f5581\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"2cfa3b6f-2807-4a29-8185-d81e399ca177\"},\"size\":24,\"noFrame\":false}],\"size\":50},{\"id\":\"6548300d-0ae2-44da-a5aa-d7c05685fd46\",\"frames\":[{\"id\":\"8757a01b-1fcd-465b-bbe0-9864cbdcabe0\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"2e17648f-3657-489d-b377-86477c57aa1b\"},\"size\":34,\"noFrame\":false},{\"id\":\"9e60f0d2-a783-469b-a757-a822eabb2db8\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"b5bdbdfd-e42a-461f-8a21-3719641b7919\"},\"size\":33,\"noFrame\":false},{\"id\":\"1cf2e92c-b991-410a-9ff9-92c5f5a63851\",\"domainObjectIdentifier\":{\"namespace\":\"\",\"key\":\"02027f64-40cc-4966-bbf4-5a7a868dcafb\"},\"size\":33,\"noFrame\":false}],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"namespace\":\"\",\"key\":\"5403b1d6-3d34-4982-a9d2-32dc678d2597\"},{\"namespace\":\"\",\"key\":\"2e17648f-3657-489d-b377-86477c57aa1b\"},{\"namespace\":\"\",\"key\":\"e589e5dc-df04-4dc9-afc1-8e632dee6293\"},{\"namespace\":\"\",\"key\":\"b5bdbdfd-e42a-461f-8a21-3719641b7919\"},{\"namespace\":\"\",\"key\":\"2cfa3b6f-2807-4a29-8185-d81e399ca177\"},{\"namespace\":\"\",\"key\":\"02027f64-40cc-4966-bbf4-5a7a868dcafb\"}],\"modified\":1702542980769,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980769,\"persisted\":1702542980769},\"57ca2a85-5488-426d-b994-060531e05cd9\":{\"identifier\":{\"key\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"namespace\":\"\"},\"name\":\"Demo Conditions\",\"type\":\"folder\",\"composition\":[{\"key\":\"fa8b9a91-57ed-47a8-971a-7b604a822801\",\"namespace\":\"\"},{\"key\":\"098a3834-5fd3-4ed2-b9e2-f3191fea5d56\",\"namespace\":\"\"},{\"key\":\"3d8e6617-4479-467d-8a90-7ab24e2e5514\",\"namespace\":\"\"},{\"key\":\"e5bb3185-952b-4b73-9891-fee2050256f3\",\"namespace\":\"\"},{\"key\":\"01a348fc-e0dd-41c3-a487-e6d4af0ccda2\",\"namespace\":\"\"},{\"key\":\"dfc3dea9-a220-4cd2-a363-00401b357ce0\",\"namespace\":\"\"},{\"key\":\"48816cba-16c9-48ad-9717-ec4edf1df8cd\",\"namespace\":\"\"},{\"key\":\"99040f3d-3a35-4e35-b2d6-ba76c1302fd4\",\"namespace\":\"\"}],\"modified\":1702542980773,\"location\":\"03c1700a-2845-4476-903e-b6d637216c97\",\"created\":1702542980773,\"persisted\":1702542980773},\"5403b1d6-3d34-4982-a9d2-32dc678d2597\":{\"identifier\":{\"namespace\":\"\",\"key\":\"5403b1d6-3d34-4982-a9d2-32dc678d2597\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"397ea03e-bc97-4558-9350-bb08e971b0f5\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"397ea03e-bc97-4558-9350-bb08e971b0f5\"},\"alarmMarkers\":false,\"color\":\"#43b0ff\"}],\"yAxis\":{},\"xAxis\":{}},\"name\":\"Navcam Pan\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980616,\"created\":1702542980616,\"persisted\":1702542980616},\"2e17648f-3657-489d-b377-86477c57aa1b\":{\"identifier\":{\"namespace\":\"\",\"key\":\"2e17648f-3657-489d-b377-86477c57aa1b\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"57a02a0e-d95a-4a57-9452-2e38e7e26548\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"57a02a0e-d95a-4a57-9452-2e38e7e26548\"},\"alarmMarkers\":false}],\"yAxis\":{},\"xAxis\":{}},\"name\":\"Navcam Tilt\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980620,\"created\":1702542980620,\"persisted\":1702542980620},\"e589e5dc-df04-4dc9-afc1-8e632dee6293\":{\"identifier\":{\"namespace\":\"\",\"key\":\"e589e5dc-df04-4dc9-afc1-8e632dee6293\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"b0156377-4747-40d5-9b81-0571bfeff9a0\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"b0156377-4747-40d5-9b81-0571bfeff9a0\"},\"color\":\"#43b0ff\",\"alarmMarkers\":false}],\"yAxis\":{},\"xAxis\":{}},\"name\":\"Rover Pitch\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980613,\"created\":1702542980613,\"persisted\":1702542980613},\"b5bdbdfd-e42a-461f-8a21-3719641b7919\":{\"identifier\":{\"namespace\":\"\",\"key\":\"b5bdbdfd-e42a-461f-8a21-3719641b7919\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"5918aeb8-a4bb-461d-beff-755b4493bd90\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"color\":\"#8cc9fd\",\"alarmMarkers\":false}],\"yAxis\":{},\"xAxis\":{}},\"name\":\"Rover Roll\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980609,\"created\":1702542980609,\"persisted\":1702542980609},\"2cfa3b6f-2807-4a29-8185-d81e399ca177\":{\"identifier\":{\"namespace\":\"\",\"key\":\"2cfa3b6f-2807-4a29-8185-d81e399ca177\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"7db16413-1c08-42ff-bc74-eac9a22e6a83\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"7db16413-1c08-42ff-bc74-eac9a22e6a83\"},\"color\":\"#43b0ff\",\"alarmMarkers\":false,\"limitLines\":true}],\"yAxis\":{},\"xAxis\":{},\"useIndependentTime\":false},\"name\":\"Rover Yaw\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980606,\"created\":1702542980606,\"persisted\":1702542980606},\"02027f64-40cc-4966-bbf4-5a7a868dcafb\":{\"identifier\":{\"namespace\":\"\",\"key\":\"02027f64-40cc-4966-bbf4-5a7a868dcafb\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"3f066b75-f807-407f-8e48-3bc30d99d883\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"3f066b75-f807-407f-8e48-3bc30d99d883\"},\"alarmMarkers\":false}],\"yAxis\":{},\"xAxis\":{}},\"name\":\"Joint Position\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980627,\"created\":1702542980627,\"persisted\":1702542980627},\"fb26ebc1-9d64-4902-a47c-f33f57d4500f\":{\"identifier\":{\"namespace\":\"\",\"key\":\"fb26ebc1-9d64-4902-a47c-f33f57d4500f\"},\"type\":\"telemetry.plot.overlay\",\"composition\":[\"004440ea-2706-457f-964c-786ed7825e02\"],\"configuration\":{\"series\":[{\"identifier\":{\"namespace\":\"\",\"key\":\"004440ea-2706-457f-964c-786ed7825e02\"},\"color\":\"#43b0ff\",\"alarmMarkers\":false,\"limitLines\":true}],\"yAxis\":{\"autoscale\":false,\"range\":{\"min\":5,\"max\":13}},\"xAxis\":{}},\"name\":\"Joint Velocity\",\"location\":\"c3215dc9-0cb1-4989-b57c-37450885fcc6\",\"modified\":1702542980623,\"created\":1702542980623,\"persisted\":1702542980623},\"dfc3dea9-a220-4cd2-a363-00401b357ce0\":{\"identifier\":{\"key\":\"dfc3dea9-a220-4cd2-a363-00401b357ce0\",\"namespace\":\"\"},\"name\":\"Battery Widget\",\"type\":\"conditionWidget\",\"configuration\":{\"objectStyles\":{\"styles\":[{\"conditionId\":\"735765b4-bf8f-4a54-b31d-3fdf6282503e\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#274e13\",\"border\":\"1px solid #00ff00\",\"color\":\"#00ff00\",\"output\":\"BATT. GOOD\"}},{\"conditionId\":\"d849fc0b-a42d-4551-b456-cd0d6a35f216\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#b45f06\",\"border\":\"1px solid #ffff00\",\"color\":\"#ffff00\",\"output\":\"BATT. MODERATE\"}},{\"conditionId\":\"0d5c4526-37bf-449c-bf21-4aaec93b088c\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#660000\",\"border\":\"1px solid #ff0000\",\"color\":\"#ff0000\",\"output\":\"BATT. MARGINAL\"}},{\"conditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\",\"output\":\"BATT. ERROR\"}}],\"staticStyle\":{\"style\":{\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\"}},\"selectedConditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"defaultConditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"conditionSetIdentifier\":{\"key\":\"098a3834-5fd3-4ed2-b9e2-f3191fea5d56\",\"namespace\":\"\"}},\"useConditionSetOutputAsLabel\":true},\"label\":\"Battery Widget\",\"conditionalLabel\":\"BATT. MODERATE\",\"modified\":1702542980631,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980631,\"persisted\":1702542980631},\"01a348fc-e0dd-41c3-a487-e6d4af0ccda2\":{\"identifier\":{\"key\":\"01a348fc-e0dd-41c3-a487-e6d4af0ccda2\",\"namespace\":\"\"},\"name\":\"Roll Widget\",\"type\":\"conditionWidget\",\"configuration\":{\"objectStyles\":{\"styles\":[{\"conditionId\":\"56c960e7-a5a5-43c1-b396-3e21889015ee\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#660000\",\"border\":\"1px solid #ff0000\",\"color\":\"#ff0000\",\"output\":\"ROLL DANGER\"}},{\"conditionId\":\"04ee9247-e704-4c1a-a377-c572f93e9b44\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#660000\",\"border\":\"1px solid #ff0000\",\"color\":\"#ff0000\",\"output\":\"ROLL DANGER\"}},{\"conditionId\":\"6ca762eb-343d-4757-b219-dbbac8a387e6\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#bf9000\",\"border\":\"1px solid #ffff00\",\"color\":\"#ffff00\",\"output\":\"ROLL WARN.\"}},{\"conditionId\":\"497ec5d7-75fb-429c-b16c-70dc8200f540\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#bf9000\",\"border\":\"1px solid #ffff00\",\"color\":\"#ffff00\",\"output\":\"ROLL WARN.\"}},{\"conditionId\":\"1184d0cc-555d-46ae-a48b-72ee8264934e\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#38761d\",\"border\":\"1px solid #00ff00\",\"color\":\"#00ff00\",\"output\":\"ROLL OK\"}},{\"conditionId\":\"5f345f62-eed3-42d0-8c88-0e654b5c378e\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\",\"output\":\"ROLL ERR.\"}}],\"staticStyle\":{\"style\":{\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\"}},\"selectedConditionId\":\"5f345f62-eed3-42d0-8c88-0e654b5c378e\",\"defaultConditionId\":\"5f345f62-eed3-42d0-8c88-0e654b5c378e\",\"conditionSetIdentifier\":{\"key\":\"3d8e6617-4479-467d-8a90-7ab24e2e5514\",\"namespace\":\"\"}},\"useConditionSetOutputAsLabel\":true},\"label\":\"Condition Widget\",\"conditionalLabel\":\"ROLL DANGER\",\"modified\":1702542980637,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980637,\"persisted\":1702542980637},\"e5bb3185-952b-4b73-9891-fee2050256f3\":{\"identifier\":{\"key\":\"e5bb3185-952b-4b73-9891-fee2050256f3\",\"namespace\":\"\"},\"name\":\"Mobility System\",\"type\":\"conditionWidget\",\"configuration\":{\"objectStyles\":{\"styles\":[{\"conditionId\":\"be20ffef-b80e-491d-8f4e-fbd14a104926\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#38761d\",\"border\":\"1px solid #00ff00\",\"color\":\"#00ff00\",\"output\":\"MOBILE\"}},{\"conditionId\":\"7cf90ae1-4742-476a-a81b-f6d5bc2feb58\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#666666\",\"border\":\"\",\"color\":\"\",\"output\":\"STATIONARY\"}}],\"staticStyle\":{\"style\":{\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\"}},\"selectedConditionId\":\"7cf90ae1-4742-476a-a81b-f6d5bc2feb58\",\"defaultConditionId\":\"7cf90ae1-4742-476a-a81b-f6d5bc2feb58\",\"conditionSetIdentifier\":{\"key\":\"fa8b9a91-57ed-47a8-971a-7b604a822801\",\"namespace\":\"\"}},\"useConditionSetOutputAsLabel\":true},\"label\":\"Condition Widget\",\"conditionalLabel\":\"MOBILE\",\"modified\":1702542980644,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980644,\"persisted\":1702542980644},\"48816cba-16c9-48ad-9717-ec4edf1df8cd\":{\"identifier\":{\"key\":\"48816cba-16c9-48ad-9717-ec4edf1df8cd\",\"namespace\":\"\"},\"name\":\"Thermal Widget\",\"type\":\"conditionWidget\",\"configuration\":{\"objectStyles\":{\"staticStyle\":{\"style\":{\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\"}},\"styles\":[{\"conditionId\":\"735765b4-bf8f-4a54-b31d-3fdf6282503e\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#274e13\",\"border\":\"1px solid #00ff00\",\"color\":\"#00ff00\",\"output\":\"THERMAL GOOD\"}},{\"conditionId\":\"d849fc0b-a42d-4551-b456-cd0d6a35f216\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#b45f06\",\"border\":\"1px solid #ffff00\",\"color\":\"#ffff00\",\"output\":\"THERMAL WARN.\"}},{\"conditionId\":\"0d5c4526-37bf-449c-bf21-4aaec93b088c\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"#660000\",\"border\":\"1px solid #ff0000\",\"color\":\"#ff0000\",\"output\":\"THERMAL DANGER\"}},{\"conditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"style\":{\"isStyleInvisible\":\"\",\"backgroundColor\":\"\",\"border\":\"\",\"color\":\"\",\"output\":\"BATT. ERROR\"}}],\"selectedConditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"defaultConditionId\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"conditionSetIdentifier\":{\"key\":\"99040f3d-3a35-4e35-b2d6-ba76c1302fd4\",\"namespace\":\"\"}},\"useConditionSetOutputAsLabel\":true},\"label\":\"Battery Widget\",\"conditionalLabel\":\"THERMAL WARN.\",\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"modified\":1702542980651,\"created\":1702542980651,\"persisted\":1702542980651},\"80964387-1304-48d5-8b7f-3063e219fef6\":{\"identifier\":{\"namespace\":\"\",\"key\":\"80964387-1304-48d5-8b7f-3063e219fef6\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Navcam Left\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980695,\"id\":\"80964387-1304-48d5-8b7f-3063e219fef6\",\"created\":1702542980694,\"persisted\":1702542980695},\"84b92e94-d5f2-4545-9bf9-775eeae700e1\":{\"identifier\":{\"namespace\":\"\",\"key\":\"84b92e94-d5f2-4545-9bf9-775eeae700e1\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Navcam Right\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980698,\"created\":1702542980698,\"persisted\":1702542980698},\"9cb58ae6-1c63-4576-aade-dd5ed3ac1637\":{\"identifier\":{\"namespace\":\"\",\"key\":\"9cb58ae6-1c63-4576-aade-dd5ed3ac1637\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Hazcam RR\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980702,\"created\":1702542980702,\"persisted\":1702542980702},\"5f5a9e82-8e04-4cb4-8c46-37de6173ad5f\":{\"identifier\":{\"namespace\":\"\",\"key\":\"5f5a9e82-8e04-4cb4-8c46-37de6173ad5f\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Hazcam RL\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980706,\"created\":1702542980706,\"persisted\":1702542980706},\"a7f011ab-f324-4718-bbc7-9442d9fa8f06\":{\"identifier\":{\"namespace\":\"\",\"key\":\"a7f011ab-f324-4718-bbc7-9442d9fa8f06\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Hazcam FL\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980709,\"created\":1702542980709,\"persisted\":1702542980709},\"90a1ae05-f10d-48b2-b07c-74cdcca92cfc\":{\"identifier\":{\"namespace\":\"\",\"key\":\"90a1ae05-f10d-48b2-b07c-74cdcca92cfc\"},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"name\":\"Hazcam FR\",\"type\":\"example.imagery\",\"location\":\"255bf177-3dc8-49d9-a0ad-3e51592c5a17\",\"modified\":1702542980713,\"created\":1702542980713,\"persisted\":1702542980713},\"32c7acde-e3fa-4683-a1e4-a5881b39a184\":{\"identifier\":{\"key\":\"32c7acde-e3fa-4683-a1e4-a5881b39a184\",\"namespace\":\"\"},\"name\":\"Navcam Left\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":20000,\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"modified\":1702542980761,\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"created\":1702542980675,\"persisted\":1702542980761},\"2240e46e-7313-4ea4-8747-332e43e0e425\":{\"identifier\":{\"key\":\"2240e46e-7313-4ea4-8747-332e43e0e425\",\"namespace\":\"\"},\"name\":\"Navcam Right\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":20000,\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"modified\":1702542980765,\"created\":1702542980678,\"persisted\":1702542980765},\"45a13587-e235-420c-93fb-e1d158b00548\":{\"identifier\":{\"key\":\"45a13587-e235-420c-93fb-e1d158b00548\",\"namespace\":\"\"},\"name\":\"example-imagery-memory-leak-test\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":\"10000\",\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"modified\":1702542980746,\"created\":1702542980662,\"persisted\":1702542980746},\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\":{\"identifier\":{\"key\":\"bcdf2b96-0017-4ce2-bd7b-c0b460ade21b\",\"namespace\":\"\"},\"name\":\"Hazcam FR\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":\"15000\",\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"modified\":1702542980750,\"created\":1702542980665,\"persisted\":1702542980750},\"447e47a6-95a4-422a-95db-73f02094883b\":{\"identifier\":{\"key\":\"447e47a6-95a4-422a-95db-73f02094883b\",\"namespace\":\"\"},\"name\":\"Hazcam RL\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":20000,\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"modified\":1702542980753,\"created\":1702542980668,\"persisted\":1702542980753},\"53c49c57-05ea-46d4-9b75-727525a0e33f\":{\"identifier\":{\"key\":\"53c49c57-05ea-46d4-9b75-727525a0e33f\",\"namespace\":\"\"},\"name\":\"Hazcam RR\",\"type\":\"example.imagery\",\"configuration\":{\"imageLocation\":\"https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg,\\nhttps://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg\",\"imageLoadDelayInMilliSeconds\":\"17000\",\"imageSamples\":[],\"layers\":[]},\"telemetry\":{\"values\":[{\"name\":\"Name\",\"key\":\"name\",\"source\":\"name\",\"hints\":{\"priority\":0}},{\"name\":\"Time\",\"key\":\"utc\",\"format\":\"utc\",\"hints\":{\"domain\":2,\"priority\":1},\"source\":\"utc\"},{\"name\":\"Local Time\",\"key\":\"local\",\"format\":\"local-format\",\"hints\":{\"domain\":1,\"priority\":2},\"source\":\"local\"},{\"name\":\"Image\",\"key\":\"url\",\"format\":\"image\",\"hints\":{\"image\":1,\"priority\":3},\"source\":\"url\"},{\"name\":\"Image Download Name\",\"key\":\"imageDownloadName\",\"format\":\"imageDownloadName\",\"hints\":{\"imageDownloadName\":1,\"priority\":4},\"source\":\"imageDownloadName\"}]},\"location\":\"f522fd95-8f83-47de-9f48-7e18f6b36c7e\",\"modified\":1702542980757,\"created\":1702542980672,\"persisted\":1702542980758},\"fa8b9a91-57ed-47a8-971a-7b604a822801\":{\"identifier\":{\"key\":\"fa8b9a91-57ed-47a8-971a-7b604a822801\",\"namespace\":\"\"},\"name\":\"Mobility System\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"id\":\"be20ffef-b80e-491d-8f4e-fbd14a104926\",\"configuration\":{\"name\":\"MOBILE\",\"output\":\"MOBILE\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"9e751e8c-9ed0-4676-b19d-796f3b55febb\",\"telemetry\":{\"namespace\":\"\",\"key\":\"004440ea-2706-457f-964c-786ed7825e02\"},\"operation\":\"greaterThan\",\"input\":[\"0\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Joint Velocity Sine  > 0 \"},{\"isDefault\":true,\"id\":\"7cf90ae1-4742-476a-a81b-f6d5bc2feb58\",\"configuration\":{\"name\":\"STATIONARY\",\"output\":\"STATIONARY\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"\"}]},\"composition\":[{\"namespace\":\"\",\"key\":\"004440ea-2706-457f-964c-786ed7825e02\"}],\"telemetry\":{},\"modified\":1702542980647,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980647,\"persisted\":1702542980647},\"098a3834-5fd3-4ed2-b9e2-f3191fea5d56\":{\"identifier\":{\"key\":\"098a3834-5fd3-4ed2-b9e2-f3191fea5d56\",\"namespace\":\"\"},\"name\":\"Battery System\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"id\":\"735765b4-bf8f-4a54-b31d-3fdf6282503e\",\"configuration\":{\"name\":\"Good Charge\",\"output\":\"BATT. GOOD\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"cef3f1fa-77f4-4284-a299-19c82803fb50\",\"telemetry\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"operation\":\"greaterThan\",\"input\":[\"50\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 50 \"},{\"id\":\"d849fc0b-a42d-4551-b456-cd0d6a35f216\",\"configuration\":{\"name\":\"Moderate Charge\",\"output\":\"BATT. MODERATE\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"905362dd-0b55-4955-a768-c8597651cfdb\",\"telemetry\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"operation\":\"greaterThan\",\"input\":[\"20\"],\"metadata\":\"sin\"},{\"id\":\"c3e3a90d-3cfc-4e47-bfff-377dc142a367\",\"telemetry\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"operation\":\"lessThan\",\"input\":[\"50\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 20 and Battery SOC Sine  < 50 \"},{\"id\":\"0d5c4526-37bf-449c-bf21-4aaec93b088c\",\"configuration\":{\"name\":\"Marginal Charge\",\"output\":\"BATT. MARGINAL\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"3247c741-26cb-49b0-97ed-dc8cc9c4df33\",\"telemetry\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"operation\":\"greaterThan\",\"input\":[\"0\"],\"metadata\":\"sin\"},{\"id\":\"8124207a-c2a9-4fc4-bec9-77af2b713b50\",\"telemetry\":{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"},\"operation\":\"greaterThanOrEq\",\"input\":[\"20\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 0 and Battery SOC Sine  >= 20 \"},{\"isDefault\":true,\"id\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"configuration\":{\"name\":\"Default\",\"output\":\"BATT. ERROR\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"\"}]},\"composition\":[{\"namespace\":\"\",\"key\":\"00e2856c-eacc-435f-881f-afb371712e26\"}],\"telemetry\":{},\"modified\":1702542980633,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980632,\"persisted\":1702542980633},\"3d8e6617-4479-467d-8a90-7ab24e2e5514\":{\"identifier\":{\"key\":\"3d8e6617-4479-467d-8a90-7ab24e2e5514\",\"namespace\":\"\"},\"name\":\"Rover Roll\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"id\":\"56c960e7-a5a5-43c1-b396-3e21889015ee\",\"configuration\":{\"name\":\"DANGER\",\"output\":\"ROLL DANGER\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"7399459d-c932-4868-b1e8-77eaf9173c3a\",\"telemetry\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"operation\":\"greaterThan\",\"input\":[\"15\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Rover Roll Sine  > 15 \"},{\"id\":\"04ee9247-e704-4c1a-a377-c572f93e9b44\",\"configuration\":{\"name\":\"DANGER LOW\",\"output\":\"ROLL DANGER\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"f33ac547-868d-49b6-aa1d-1ad3228e6a21\",\"telemetry\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"operation\":\"lessThan\",\"input\":[\"-15\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Rover Roll Sine  < -15 \"},{\"id\":\"6ca762eb-343d-4757-b219-dbbac8a387e6\",\"configuration\":{\"name\":\"WARNING\",\"output\":\"ROLL WARN.\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"6cb35ae7-4a6f-49c5-a30a-d05a22ee4f4e\",\"telemetry\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"operation\":\"between\",\"input\":[\"10\",\"15\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Rover Roll Sine  is between 10 and 15 \"},{\"id\":\"497ec5d7-75fb-429c-b16c-70dc8200f540\",\"configuration\":{\"name\":\"WARNING LOW\",\"output\":\"ROLL WARN.\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"e01fcd54-994d-47da-821e-ab2ec6bb9c1c\",\"telemetry\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"operation\":\"between\",\"input\":[\"-10\",\"-15\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Rover Roll Sine  is between -10 and -15 \"},{\"id\":\"1184d0cc-555d-46ae-a48b-72ee8264934e\",\"configuration\":{\"name\":\"OK\",\"output\":\"ROLL OK\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"695fca7b-800e-40ad-b85a-3881739744d1\",\"telemetry\":{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"},\"operation\":\"between\",\"input\":[\"-10\",\"10\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Rover Roll Sine  is between -10 and 10 \"},{\"isDefault\":true,\"id\":\"5f345f62-eed3-42d0-8c88-0e654b5c378e\",\"configuration\":{\"name\":\"Default\",\"output\":\"ROLL ERR.\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"\"}]},\"composition\":[{\"namespace\":\"\",\"key\":\"5918aeb8-a4bb-461d-beff-755b4493bd90\"}],\"telemetry\":{},\"modified\":1702542980639,\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"created\":1702542980639,\"persisted\":1702542980639},\"99040f3d-3a35-4e35-b2d6-ba76c1302fd4\":{\"identifier\":{\"key\":\"99040f3d-3a35-4e35-b2d6-ba76c1302fd4\",\"namespace\":\"\"},\"name\":\"Thermal System\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"id\":\"735765b4-bf8f-4a54-b31d-3fdf6282503e\",\"configuration\":{\"name\":\"Good\",\"output\":\"THERMAL GOOD\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"cef3f1fa-77f4-4284-a299-19c82803fb50\",\"telemetry\":{\"namespace\":\"\",\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\"},\"operation\":\"greaterThan\",\"input\":[\"50\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 50 \"},{\"id\":\"d849fc0b-a42d-4551-b456-cd0d6a35f216\",\"configuration\":{\"name\":\"Moderate Charge\",\"output\":\"THERMAL WARN.\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"905362dd-0b55-4955-a768-c8597651cfdb\",\"telemetry\":{\"namespace\":\"\",\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\"},\"operation\":\"greaterThan\",\"input\":[\"20\"],\"metadata\":\"sin\"},{\"id\":\"c3e3a90d-3cfc-4e47-bfff-377dc142a367\",\"telemetry\":{\"namespace\":\"\",\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\"},\"operation\":\"lessThan\",\"input\":[\"50\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 20 and Battery SOC Sine  < 50 \"},{\"id\":\"0d5c4526-37bf-449c-bf21-4aaec93b088c\",\"configuration\":{\"name\":\"Marginal Charge\",\"output\":\"THERMAL DANGER\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"3247c741-26cb-49b0-97ed-dc8cc9c4df33\",\"telemetry\":{\"namespace\":\"\",\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\"},\"operation\":\"greaterThan\",\"input\":[\"0\"],\"metadata\":\"sin\"},{\"id\":\"8124207a-c2a9-4fc4-bec9-77af2b713b50\",\"telemetry\":{\"namespace\":\"\",\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\"},\"operation\":\"greaterThanOrEq\",\"input\":[\"20\"],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met:  Battery SOC Sine  > 0 and Battery SOC Sine  >= 20 \"},{\"isDefault\":true,\"id\":\"7331d95a-aa7b-44c3-bf46-c9f78c64c344\",\"configuration\":{\"name\":\"Default\",\"output\":\"BATT. ERROR\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"\"}]},\"composition\":[{\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\",\"namespace\":\"\"}],\"telemetry\":{},\"location\":\"57ca2a85-5488-426d-b994-060531e05cd9\",\"modified\":1702542980654,\"created\":1702542980654,\"persisted\":1702542980654},\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\":{\"identifier\":{\"key\":\"0b42a2a1-6ab5-47be-aaa0-91d901c3bd37\",\"namespace\":\"\"},\"telemetry\":{\"period\":10000000,\"amplitude\":100,\"offset\":100,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0.001},\"name\":\"Battery SOC\",\"type\":\"generator\",\"id\":\"00e2856c-eacc-435f-881f-afb371712e26\",\"location\":\"99040f3d-3a35-4e35-b2d6-ba76c1302fd4\",\"modified\":1702542980657,\"created\":1702542980656,\"persisted\":1702542980657}},\"rootId\":\"9224ac93-50af-4bb9-ac72-89a42b33f031\"}"
  },
  {
    "path": "e2e/test-data/overlay_plot_storage.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605677,\\\"created\\\":1732413603298,\\\"persisted\\\":1732413605677},\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Overlay Plot with Telemetry Object\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate Overlay Plot with Telemetry Object\\\\nchrome\\\",\\\"modified\\\":1732413607031,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605018,\\\"persisted\\\":1732413607031},\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\":{\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"identifier\\\":{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"},\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1732413605677,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605677,\\\"persisted\\\":1732413605677}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1732413605677,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605677,\\\"persisted\\\":1732413605677},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605677,\\\"created\\\":1732413603298,\\\"persisted\\\":1732413605677},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":0,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1732413605677,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605677,\\\"persisted\\\":1732413605677}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Overlay Plot with Telemetry Object\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}}]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate Overlay Plot with Telemetry Object\\\\nchrome\\\",\\\"modified\\\":1732413607031,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605018,\\\"persisted\\\":1732413607031},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605677,\\\"created\\\":1732413603298,\\\"persisted\\\":1732413605677},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Overlay Plot with Telemetry Object\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}}]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate Overlay Plot with Telemetry Object\\\\nchrome\\\",\\\"modified\\\":1732413607031,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605018,\\\"persisted\\\":1732413607031}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605677,\\\"created\\\":1732413603298,\\\"persisted\\\":1732413605677},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"d3014736-1182-4b70-8122-6d0c6ef540e1\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8c53d61f-b514-4535-be87-0fb20eb56576\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605677,\\\"created\\\":1732413603298,\\\"persisted\\\":1732413605677}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/overlay_plot_with_delay_storage.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f34f457e-d7f4-4fc4-ba71-52e19e925646\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"modified\\\":1732413605044,\\\"created\\\":1732413603140,\\\"persisted\\\":1732413605044},\\\"f34f457e-d7f4-4fc4-ba71-52e19e925646\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"f34f457e-d7f4-4fc4-ba71-52e19e925646\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Overlay Plot with 5s Delay\\\",\\\"type\\\":\\\"telemetry.plot.overlay\\\",\\\"composition\\\":[{\\\"key\\\":\\\"c568fa66-62e0-4eee-97eb-cdbc7421e556\\\",\\\"namespace\\\":\\\"\\\"}],\\\"configuration\\\":{\\\"series\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"c568fa66-62e0-4eee-97eb-cdbc7421e556\\\",\\\"namespace\\\":\\\"\\\"}}]},\\\"notes\\\":\\\"framework/generateLocalStorageData.e2e.spec.js\\\\nGenerate Visual Test Data @localStorage @generatedata @clock\\\\nGenerate Overlay Plot with 5s Delay\\\\nchrome\\\",\\\"modified\\\":1732413606208.9,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1732413605044,\\\"persisted\\\":1732413606208.9},\\\"c568fa66-62e0-4eee-97eb-cdbc7421e556\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"c568fa66-62e0-4eee-97eb-cdbc7421e556\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"VIPER Rover Heading\\\",\\\"type\\\":\\\"generator\\\",\\\"telemetry\\\":{\\\"period\\\":10,\\\"amplitude\\\":1,\\\"offset\\\":0,\\\"dataRateInHz\\\":1,\\\"phase\\\":0,\\\"randomness\\\":0,\\\"loadDelay\\\":5000,\\\"infinityValues\\\":false,\\\"exceedFloat32\\\":false,\\\"staleness\\\":false},\\\"modified\\\":1732413606049,\\\"location\\\":\\\"f34f457e-d7f4-4fc4-ba71-52e19e925646\\\",\\\"created\\\":1732413605554,\\\"persisted\\\":1732413606049}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/test-data/recycled_local_storage.json",
    "content": "{\n  \"cookies\": [],\n  \"origins\": [\n    {\n      \"origin\": \"http://localhost:8080\",\n      \"localStorage\": [\n        {\n          \"name\": \"mct\",\n          \"value\": \"{\\\"mine\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1652303755999,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1652303756002},\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"4291d80c-303c-4d8d-85e1-10f012b864fb\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1654538965702,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1654538965702},\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1658610682787,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1658610682787},\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"b9a9c413-4b94-401d-b0c7-5e404f182616\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1658618261112,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1658618261112},\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1658618890910,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1658618890910},\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1658619295363,\\\"location\\\":\\\"mine\\\",\\\"persisted\\\":1658619295363},\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1689710689550,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1689710689550,\\\"persisted\\\":1689710689550},\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694110416938,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694110416938,\\\"persisted\\\":1694110416938},\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694112912985,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694112912985,\\\"persisted\\\":1694112912985},\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1704955298729,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1704955298729,\\\"persisted\\\":1704955298729},\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"9a72ce62-012c-4a81-8f2c-51db5410de76\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850626897,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850626897,\\\"persisted\\\":1721850626897},\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"71e8050e-e063-4a35-b891-f38ddf63fa6d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850725791,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850725791,\\\"persisted\\\":1721850725791},\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\":{\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"identifier\\\":{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"},\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850933438,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850933438,\\\"persisted\\\":1721850933439}}\"\n        },\n        {\n          \"name\": \"mct-tree-expanded\",\n          \"value\": \"[]\"\n        },\n        {\n          \"name\": \"tcHistory\",\n          \"value\": \"{\\\"utc\\\":[{\\\"start\\\":1658617494563,\\\"end\\\":1658619294563},{\\\"start\\\":1658617090044,\\\"end\\\":1658618890044},{\\\"start\\\":1658616460484,\\\"end\\\":1658618260484},{\\\"start\\\":1658608882159,\\\"end\\\":1658610682159},{\\\"start\\\":1654537164464,\\\"end\\\":1654538964464},{\\\"start\\\":1652301954635,\\\"end\\\":1652303754635}]}\"\n        },\n        {\n          \"name\": \"mct-recent-objects\",\n          \"value\": \"[{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850933438,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850933438,\\\"persisted\\\":1721850933439},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850933438,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850933438,\\\"persisted\\\":1721850933439}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"71e8050e-e063-4a35-b891-f38ddf63fa6d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850725791,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850725791,\\\"persisted\\\":1721850725791},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"71e8050e-e063-4a35-b891-f38ddf63fa6d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850725791,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850725791,\\\"persisted\\\":1721850725791}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"9a72ce62-012c-4a81-8f2c-51db5410de76\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850626897,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850626897,\\\"persisted\\\":1721850626897},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"9a72ce62-012c-4a81-8f2c-51db5410de76\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1721850626897,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1721850626897,\\\"persisted\\\":1721850626897}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1704955298729,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1704955298729,\\\"persisted\\\":1704955298729},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1704955298729,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1704955298729,\\\"persisted\\\":1704955298729}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694112912985,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694112912985,\\\"persisted\\\":1694112912985},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694112912985,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694112912985,\\\"persisted\\\":1694112912985}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694110416938,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694110416938,\\\"persisted\\\":1694110416938},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1694110416938,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1694110416938,\\\"persisted\\\":1694110416938}},{\\\"objectPath\\\":[{\\\"identifier\\\":{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1689710689550,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1689710689550,\\\"persisted\\\":1689710689550},{\\\"identifier\\\":{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"My Items\\\",\\\"type\\\":\\\"folder\\\",\\\"composition\\\":[{\\\"key\\\":\\\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"2d02a680-eb7e-4645-bba2-dd298f76efb8\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"3e294eae-6124-409b-a870-554d1bdcdd6f\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ec24d05d-5df5-4c96-9241-b73636cd19a9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"ffb49de1-af27-4318-a22f-59899988f4e9\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"c1717964-ffed-47aa-9ed9-647ba5a3db67\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"10581641-5de3-4606-95aa-04cd811f2f53\\\",\\\"namespace\\\":\\\"\\\"},{\\\"key\\\":\\\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\\\",\\\"namespace\\\":\\\"\\\"}],\\\"location\\\":\\\"ROOT\\\",\\\"persisted\\\":1721850933441,\\\"modified\\\":1721850933441},{\\\"identifier\\\":{\\\"key\\\":\\\"ROOT\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Open MCT\\\",\\\"type\\\":\\\"root\\\",\\\"composition\\\":[{\\\"key\\\":\\\"mine\\\",\\\"namespace\\\":\\\"\\\"}]}],\\\"navigationPath\\\":\\\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"domainObject\\\":{\\\"identifier\\\":{\\\"key\\\":\\\"0ec517e8-6c11-4d98-89b5-c300fe61b304\\\",\\\"namespace\\\":\\\"\\\"},\\\"name\\\":\\\"Unnamed Condition Set\\\",\\\"type\\\":\\\"conditionSet\\\",\\\"configuration\\\":{\\\"conditionTestData\\\":[],\\\"conditionCollection\\\":[{\\\"isDefault\\\":true,\\\"id\\\":\\\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\\\",\\\"configuration\\\":{\\\"name\\\":\\\"Default\\\",\\\"output\\\":\\\"Default\\\",\\\"trigger\\\":\\\"all\\\",\\\"criteria\\\":[]},\\\"summary\\\":\\\"Default condition\\\"}]},\\\"composition\\\":[],\\\"telemetry\\\":{},\\\"modified\\\":1689710689550,\\\"location\\\":\\\"mine\\\",\\\"created\\\":1689710689550,\\\"persisted\\\":1689710689550}}]\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "e2e/tests/framework/appActions.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport fs from 'fs';\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  createNotification,\n  createPlanFromJSON,\n  createStableStateTelemetry,\n  expandEntireTree,\n  getCanvasPixels,\n  linkParameterToObject,\n  navigateToObjectWithFixedTimeBounds,\n  navigateToObjectWithRealTime,\n  setEndOffset,\n  setFixedIndependentTimeConductorBounds,\n  setFixedTimeMode,\n  setRealTimeMode,\n  setStartOffset,\n  setTimeConductorBounds,\n  waitForPlotsToRender\n} from '../../appActions.js';\nimport { assertPlanActivities, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('AppActions @framework', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n  test('createDomainObjectsWithDefaults', async ({ page }) => {\n    const e2eFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'e2e folder'\n    });\n\n    await test.step('Create multiple flat objects in a row', async () => {\n      const timer1 = await createDomainObjectWithDefaults(page, {\n        type: 'Timer',\n        name: 'Timer Foo',\n        parent: e2eFolder.uuid\n      });\n      const timer2 = await createDomainObjectWithDefaults(page, {\n        type: 'Timer',\n        name: 'Timer Bar',\n        parent: e2eFolder.uuid\n      });\n      const timer3 = await createDomainObjectWithDefaults(page, {\n        type: 'Timer',\n        name: 'Timer Baz',\n        parent: e2eFolder.uuid\n      });\n\n      await page.goto(timer1.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);\n      await page.goto(timer2.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);\n      await page.goto(timer3.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);\n    });\n\n    await test.step('Create multiple nested objects in a row', async () => {\n      const folder1 = await createDomainObjectWithDefaults(page, {\n        type: 'Folder',\n        name: 'Folder Foo',\n        parent: e2eFolder.uuid\n      });\n      const folder2 = await createDomainObjectWithDefaults(page, {\n        type: 'Folder',\n        name: 'Folder Bar',\n        parent: folder1.uuid\n      });\n      const folder3 = await createDomainObjectWithDefaults(page, {\n        type: 'Folder',\n        name: 'Folder Baz',\n        parent: folder2.uuid\n      });\n      await page.goto(folder1.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);\n      await page.goto(folder2.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);\n      await page.goto(folder3.url);\n      await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);\n\n      expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);\n      expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);\n      expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);\n    });\n  });\n  test('createExampleTelemetryObject', async ({ page }) => {\n    const gauge = await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Gauge with no data'\n    });\n\n    const swgWithParent = await createExampleTelemetryObject(page, gauge.uuid);\n\n    await page.goto(swgWithParent.url);\n    await expect(page.locator('.l-browse-bar__object-name')).toHaveText(swgWithParent.name);\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n\n    // Check Default values of created object\n    await expect(page.getByLabel('Title', { exact: true })).toHaveValue('VIPER Rover Heading');\n    await expect(page.getByRole('spinbutton', { name: 'Period' })).toHaveValue('10');\n    await expect(page.getByRole('spinbutton', { name: 'Amplitude' })).toHaveValue('1');\n    await expect(page.getByRole('spinbutton', { name: 'Offset' })).toHaveValue('0');\n    await expect(page.getByRole('spinbutton', { name: 'Data Rate (hz)' })).toHaveValue('1');\n    await expect(page.getByRole('spinbutton', { name: 'Phase (radians)' })).toHaveValue('0');\n    await expect(page.getByRole('spinbutton', { name: 'Randomness' })).toHaveValue('0');\n    await expect(page.getByRole('spinbutton', { name: 'Loading Delay (ms)' })).toHaveValue('0');\n\n    await page.getByLabel('Cancel').click();\n\n    const swgWithoutParent = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n\n    expect(swgWithParent.url).toBe(`${gauge.url}/${swgWithParent.uuid}`);\n    expect(swgWithoutParent.url).toBe(`./#/browse/mine/${swgWithoutParent.uuid}`);\n  });\n  test('createNotification', async ({ page }) => {\n    await createNotification(page, {\n      message: 'Test info notification',\n      severity: 'info'\n    });\n    await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification');\n    await expect(page.locator('.c-message-banner')).toHaveClass(/info/);\n    await page.locator('[aria-label=\"Dismiss\"]').click();\n    await createNotification(page, {\n      message: 'Test alert notification',\n      severity: 'alert'\n    });\n    await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification');\n    await expect(page.locator('.c-message-banner')).toHaveClass(/alert/);\n    await page.locator('[aria-label=\"Dismiss\"]').click();\n    await createNotification(page, {\n      message: 'Test error notification',\n      severity: 'error'\n    });\n    await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification');\n    await expect(page.locator('.c-message-banner')).toHaveClass(/error/);\n    await page.locator('[aria-label=\"Dismiss\"]').click();\n  });\n  test('createPlanFromJSON', async ({ page }) => {\n    const examplePlanSmall1 = JSON.parse(\n      fs.readFileSync(\n        new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)\n      )\n    );\n    const plan = await createPlanFromJSON(page, {\n      name: 'Test Plan',\n      json: examplePlanSmall1\n    });\n    await setBoundsToSpanAllActivities(page, examplePlanSmall1, plan.url);\n    await assertPlanActivities(page, examplePlanSmall1, plan.url);\n  });\n  test('expandEntireTree', async ({ page }) => {\n    const rootFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n    const folder1 = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      parent: rootFolder.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      parent: folder1.uuid\n    });\n    const folder2 = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      parent: folder1.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      parent: folder1.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      parent: folder2.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      parent: folder2.uuid\n    });\n\n    await page.goto('./#/browse/mine');\n    await expandEntireTree(page);\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });\n    await expect(treePaneCollapsedItems).toHaveCount(0);\n\n    await page.goto('./#/browse/mine');\n    //Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click the object specified by 'type'\n    await page.getByRole('menuitem', { name: 'Clock' }).click();\n    await expandEntireTree(page, 'Create Modal Tree');\n    const locatorTree = page.getByRole('tree', {\n      name: 'Create Modal Tree'\n    });\n    const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');\n    await expect(locatorTreeCollapsedItems).toHaveCount(0);\n  });\n  test('getCanvasPixels', async ({ page }) => {\n    let overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    await createExampleTelemetryObject(page, overlayPlot.uuid);\n\n    await page.goto(overlayPlot.url);\n    //Get pixel data from Canvas\n    const plotPixels = await getCanvasPixels(page, 'canvas');\n    const plotPixelSize = plotPixels.length;\n    expect(plotPixelSize).toBeGreaterThan(0);\n  });\n  test('navigateToObjectWithFixedTimeBounds', async ({ page }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n    //Navigate without explicit bounds\n    await navigateToObjectWithFixedTimeBounds(page, exampleTelemetry.url);\n    await expect(page.getByLabel('Start bounds:')).toBeVisible();\n    await expect(page.getByLabel('End bounds:')).toBeVisible();\n    //Navigate with explicit bounds\n    await navigateToObjectWithFixedTimeBounds(\n      page,\n      exampleTelemetry.url,\n      1693592063607,\n      1693593893607\n    );\n    await expect(page.getByLabel('Start bounds: 2023-09-01 18:')).toBeVisible();\n    await expect(page.getByLabel('End bounds: 2023-09-01 18:44:')).toBeVisible();\n  });\n  test('navigateToObjectWithRealTime', async ({ page }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n    //Navigate without explicit bounds\n    await navigateToObjectWithRealTime(page, exampleTelemetry.url);\n    await expect(page.getByLabel('Start offset:')).toBeVisible();\n    await expect(page.getByLabel('End offset: 00:00:')).toBeVisible();\n    //Navigate with explicit bounds\n    await navigateToObjectWithRealTime(page, exampleTelemetry.url, 1693592063607, 1693593893607);\n    await expect(page.getByLabel('Start offset: 18:14:')).toBeVisible();\n    await expect(page.getByLabel('End offset: 18:44:')).toBeVisible();\n  });\n  test('setTimeConductorMode', async ({ page }) => {\n    await test.step('setFixedTimeMode', async () => {\n      await setFixedTimeMode(page);\n      await expect(page.getByLabel('Start bounds:')).toBeVisible();\n      await expect(page.getByLabel('End bounds:')).toBeVisible();\n    });\n    await test.step('setTimeConductorBounds', async () => {\n      await setTimeConductorBounds(page, {\n        startDate: '2024-01-01',\n        endDate: '2024-01-02',\n        startTime: '00:00:00',\n        endTime: '23:59:59'\n      });\n      await expect(page.getByLabel('Start bounds: 2024-01-01 00:00:00')).toBeVisible();\n      await expect(page.getByLabel('End bounds: 2024-01-02 23:59:59')).toBeVisible();\n    });\n    await test.step('setRealTimeMode', async () => {\n      await setRealTimeMode(page);\n      await expect(page.getByLabel('Start offset')).toBeVisible();\n      await expect(page.getByLabel('End offset')).toBeVisible();\n    });\n    await test.step('setStartOffset', async () => {\n      await setStartOffset(page, {\n        startHours: '04',\n        startMins: '20',\n        startSecs: '22'\n      });\n      await expect(page.getByLabel('Start offset: 04:20:22')).toBeVisible();\n    });\n    await test.step('setEndOffset', async () => {\n      await setEndOffset(page, {\n        endHours: '04',\n        endMins: '20',\n        endSecs: '22'\n      });\n      await expect(page.getByLabel('End offset: 04:20:22')).toBeVisible();\n    });\n  });\n  test('setFixedIndependentTimeConductorBounds', async ({ page }) => {\n    // Create a Display Layout\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Example Imagery',\n      parent: displayLayout.uuid\n    });\n\n    const startDate = '2021-12-30 01:01:00.000Z';\n    const endDate = '2021-12-30 01:11:00.000Z';\n    await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate });\n\n    // check image date\n    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();\n\n    // flip it off\n    await page.getByRole('switch').click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n  });\n  test.fail('waitForPlotsToRender', async ({ page }) => {\n    // Create a SWG\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n    // Edit the SWG\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n    // Set loading delay to 10 seconds\n    await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('10000');\n    await page.getByLabel('Save').click();\n    // Reload the page\n    await page.reload();\n    // Expect this step to fail\n    await waitForPlotsToRender(page, { timeout: 1000 });\n  });\n  test('createStableStateTelemetry', async ({ page }) => {\n    const stableStateTelemetry = await createStableStateTelemetry(page);\n    expect(stableStateTelemetry.name).toBe('Stable State Generator');\n    expect(stableStateTelemetry.url).toBe(`./#/browse/mine/${stableStateTelemetry.uuid}`);\n    expect(stableStateTelemetry.uuid).toBeDefined();\n  });\n  test('linkParameterToObject', async ({ page }) => {\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await linkParameterToObject(page, exampleTelemetry.name, displayLayout.name);\n    await page.goto(displayLayout.url);\n    await expect(page.getByRole('main').getByText('Test Display Layout')).toBeVisible();\n    await expandEntireTree(page);\n    await expect(page.getByLabel('Navigate to VIPER Rover').first()).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/framework/baseFixtures.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to testing our use of the playwright framework as it\nrelates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment\n(`npm start` and ./e2e/webpack-dev-middleware.js)\n*/\n\nimport { test } from '../../baseFixtures.js';\nimport { MISSION_TIME } from '../../constants.js';\n\ntest.describe('baseFixtures tests', () => {\n  //Skip this test for now https://github.com/nasa/openmct/issues/6785\n  test('Verify that tests fail if console.error is thrown', async ({ page }) => {\n    test.fail();\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Verify that ../fixtures.js detects console log errors\n    await Promise.all([\n      page.evaluate(() => console.error('This should result in a failure')),\n      page.waitForEvent('console') // always wait for the event to happen while triggering it!\n    ]);\n  });\n  test('Verify that tests fail if console.error is thrown with clock override @clock', async ({\n    page\n  }) => {\n    test.fail();\n    //Set clock time\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Verify that ../fixtures.js detects console log errors\n    await Promise.all([\n      page.evaluate(() => console.error('This should result in a failure')),\n      page.waitForEvent('console') // always wait for the event to happen while triggering it!\n    ]);\n  });\n  test('Verify that tests pass if console.warn is thrown', async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Verify that ../fixtures.js detects console log errors\n    await Promise.all([\n      page.evaluate(() => console.warn('This should result in a pass')),\n      page.waitForEvent('console') // always wait for the event to happen while triggering it!\n    ]);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/framework/exampleTemplate.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements\n * made by the Open MCT team. It will also follow our best practices as those evolve. Please use this structure as a _reference_ and clear\n * or update any references when creating a new test suite!\n *\n * To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object.\n *\n * Demonstrated:\n * - Using appActions to leverage existing functions\n * - Structure\n * - await, expect, test, describe syntax\n * - Writing a custom function for a test suite\n * - Test stub for unfinished test coverage (test.fixme)\n *\n * The structure should follow\n * 1. imports\n * 2. test.describe()\n * 3. -> test1\n *    -> test2\n *    -> test3(stub)\n * 4. Any custom functions\n */\n\n// Structure: Some standard Imports. Please update the required pathing.\nimport { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\n/**\n * Structure:\n *  Try to keep a single describe block per logical groups of tests.\n *  If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.\n *\n */\ntest.describe('Example - Renaming Timer Object', () => {\n  // Top-level declaration of the Timer object created in beforeEach().\n  // We can then use this throughout the entire test suite.\n  let timer;\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all network events to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.\n    // This example will create a Timer object with default properties, under the root folder:\n    timer = await createDomainObjectWithDefaults(page, { type: 'Timer' });\n\n    // Assert the object to be created and check its name in the title\n    await expect(page.getByRole('main')).toContainText(timer.name);\n  });\n\n  /**\n   * Make sure to use testcase names which are descriptive and easy to understand.\n   * A good testcase name concisely describes the test's goal(s) and should give\n   * some hint as to what went wrong if the test fails.\n   */\n  test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => {\n    const newObjectName = 'Renamed Timer';\n\n    // We've created an example of a shared function which passes the page and newObjectName values\n    await renameTimerFrom3DotMenu(page, timer.url, newObjectName);\n\n    // Assert that the name has changed in the browser bar to the value we assigned above\n    await expect(page.getByRole('main')).toContainText(newObjectName);\n  });\n\n  test('An existing Timer object can be renamed twice', async ({ page }) => {\n    const newObjectName = 'Renamed Timer';\n    const newObjectName2 = 'Re-Renamed Timer';\n\n    await renameTimerFrom3DotMenu(page, timer.url, newObjectName);\n\n    // Assert that the name has changed in the browser bar to the value we assigned above\n    await expect(page.getByRole('main')).toContainText(newObjectName);\n\n    // Rename the Timer object again\n    await renameTimerFrom3DotMenu(page, timer.url, newObjectName2);\n\n    // Assert that the name has changed in the browser bar to the second value\n    await expect(page.getByRole('main')).toContainText(newObjectName2);\n  });\n\n  /**\n   * If you run out of time to write new tests, please stub in the missing tests\n   * in-place with a test.fixme and BDD-style test steps.\n   * Someone will carry the baton!\n   */\n  test.fixme('Can Rename Timer Object from Tree', async ({ page }) => {\n    //Create a new object\n    //Copy this object\n    //Delete first object\n    //Expect copied object to persist\n  });\n});\n\n/**\n * The next most important concept in our testing is working with telemetry objects. Telemetry is at the core of Open MCT\n * and we have developed a great pattern for working with it.\n */\ntest.describe('Advanced Example - Working with telemetry objects', () => {\n  let displayLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Display Layout with a meaningful name\n    displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Display Layout with Embedded SWG'\n    });\n    // Create Telemetry object within the parent object created above\n    //reference the display layout in the creation process\n    await createExampleTelemetryObject(page, displayLayout.uuid);\n  });\n  test('Can directly navigate to a Display Layout with embedded telemetry', async ({ page }) => {\n    //Now you can directly navigate to the displayLayout created in the beforeEach with the embedded telemetry\n    await page.goto(displayLayout.url);\n    //Expect the created Telemetry Object to be visible when directly navigating to the displayLayout\n    await expect(page.getByLabel('Alpha-numeric telemetry name')).toBeVisible();\n  });\n});\n\n/**\n * Structure:\n * Custom functions should be declared last.\n * We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but highly recommended.\n */\n\n/**\n * This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking\n * for common functionality which makes sense to generalize for the entire test framework.\n * @param {import('@playwright/test').Page} page\n * @param {string} timerUrl The URL of the timer object to be renamed\n * @param {string} newNameForTimer New name for object\n */\nasync function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {\n  // Navigate to the timer object directly\n  await page.goto(timerUrl);\n\n  await page.getByLabel('More actions').click();\n  await page.getByLabel('Edit Properties...').click();\n\n  // Rename the timer object\n  await page.getByLabel('Title', { exact: true }).fill(newNameForTimer);\n\n  await page.getByLabel('Save').click();\n}\n"
  },
  {
    "path": "e2e/tests/framework/generateLocalStorageData.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/**\n * This test suite is dedicated to generating LocalStorage via Session Storage to be used\n * in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion\n * and generate an artifact in ./e2e/test-data/<name>_storage.json . This will run\n * on every commit to ensure that this object still loads into tests correctly and will retain the\n * *.e2e.spec.js suffix.\n *\n * TODO: Provide additional validation of object properties as it grows.\n * Verification of object properties happens in this file before the test-data is generated,\n * and is additionally verified in the validation test suites below.\n */\n\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  setFixedIndependentTimeConductorBounds,\n  setTimeConductorBounds\n} from '../../appActions.js';\nimport { MISSION_TIME } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\nconst overlayPlotName = 'Overlay Plot with Telemetry Object';\n\ntest.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    // Override the clock\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Generate display layout with 2 child display layouts', async ({ page, context }) => {\n    const parent = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Parent Display Layout'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Layout 1',\n      parent: parent.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Layout 2',\n      parent: parent.uuid\n    });\n\n    await page.goto(parent.url, { waitUntil: 'domcontentloaded' });\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Child Layout 2 Layout', { exact: true }).hover();\n    await page.getByLabel('Move Sub-object Frame').nth(1).click();\n    await page.getByLabel('X:').fill('30');\n\n    await page.getByLabel('Child Layout 1 Layout', { exact: true }).hover();\n    await page.getByLabel('Move Sub-object Frame').first().click();\n    await page.getByLabel('Y:').fill('30');\n\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    //Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../e2e/test-data/display_layout_with_child_layouts.json', import.meta.url)\n      )\n    });\n  });\n\n  test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {\n    const parent = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Parent Display Layout'\n    });\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Child Overlay Plot 1',\n      parent: parent.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Child SWG 1',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(parent.url, { waitUntil: 'domcontentloaded' });\n\n    await setFixedIndependentTimeConductorBounds(page, {\n      start: '2024-11-12 19:11:11.000Z',\n      end: '2024-11-12 20:11:11.000Z'\n    });\n\n    const NEW_GLOBAL_START_DATE = '2024-11-11';\n    const NEW_GLOBAL_START_TIME = '19:11:11';\n    const NEW_GLOBAL_END_DATE = '2024-11-11';\n    const NEW_GLOBAL_END_TIME = '20:11:11';\n\n    await setTimeConductorBounds(page, {\n      startDate: NEW_GLOBAL_START_DATE,\n      startTime: NEW_GLOBAL_START_TIME,\n      endDate: NEW_GLOBAL_END_DATE,\n      endTime: NEW_GLOBAL_END_TIME\n    });\n\n    // Verify that the global time conductor bounds have been updated\n    await expect(\n      page.getByLabel(`Start bounds: ${NEW_GLOBAL_START_DATE} ${NEW_GLOBAL_START_TIME}.000Z`)\n    ).toBeVisible();\n    await expect(\n      page.getByLabel(`End bounds: ${NEW_GLOBAL_END_DATE} ${NEW_GLOBAL_END_TIME}.000Z`)\n    ).toBeVisible();\n\n    //Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL(\n          '../../../e2e/test-data/display_layout_with_child_overlay_plot.json',\n          import.meta.url\n        )\n      )\n    });\n  });\n\n  test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {\n    // Create Display Layout\n    const parent = await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout',\n      name: 'Parent Flexible Layout'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Layout 1',\n      parent: parent.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Layout 2',\n      parent: parent.uuid\n    });\n\n    await page.goto(parent.url, { waitUntil: 'domcontentloaded' });\n\n    //Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../e2e/test-data/flexible_layout_with_child_layouts.json', import.meta.url)\n      )\n    });\n  });\n\n  // TODO: Visual test for the generated object here\n  // - Move to using appActions to create the overlay plot\n  //   and embedded standard telemetry object\n  test('Generate Overlay Plot with Telemetry Object', async ({ page, context }) => {\n    // Create Overlay Plot\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: overlayPlotName\n    });\n\n    // Create Telemetry Object\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    // Make Link from Telemetry Object to Overlay Plot\n    await page.locator('button[title=\"More actions\"]').click();\n\n    // Select 'Create Link' from dropdown\n    await page.getByRole('menuitem', { name: 'Create Link' }).click();\n\n    // Search and Select for overlay Plot within Create Modal\n    await page.getByRole('dialog').getByRole('searchbox', { name: 'Search Input' }).click();\n    await page\n      .getByRole('dialog')\n      .getByRole('searchbox', { name: 'Search Input' })\n      .fill(overlayPlot.name);\n    await page\n      .getByRole('treeitem', { name: new RegExp(overlayPlot.name) })\n      .locator('a')\n      .click();\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await page.goto(overlayPlot.url);\n\n    // TODO: Flesh Out Assertions against created Objects\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Plot Series Items').getByLabel('Expand').click();\n\n    // TODO: Modify the Overlay Plot to use fixed Scaling\n    // TODO: Verify Autoscaling.\n\n    // TODO: Fix accessibility of Plot Series Properties tables\n    // Assert that the Plot Series properties have the correct values\n    await expect(\n      page.locator('[role=cell]:has-text(\"Value\")~[role=cell]:has-text(\"sin\")')\n    ).toBeVisible();\n    await expect(\n      page.locator(\n        '[role=cell]:has-text(\"Line Method\")~[role=cell]:has-text(\"Linear interpolation\")'\n      )\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Markers\")~[role=cell]:has-text(\"Point: 2px\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Alarm Markers\")~[role=cell]:has-text(\"Enabled\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Limit Lines\")~[role=cell]:has-text(\"Disabled\")')\n    ).toBeVisible();\n\n    await page.goto(exampleTelemetry.url);\n    await page.getByRole('tab', { name: 'Properties' }).click();\n\n    // TODO: assert Example Telemetry property values\n    // await page.goto(exampleTelemetry.url);\n\n    // Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../e2e/test-data/overlay_plot_storage.json', import.meta.url)\n      )\n    });\n  });\n  // TODO: Merge this with previous test. Edit object created in previous test.\n  test('Generate Overlay Plot with 5s Delay', async ({ page, context }) => {\n    // add overlay plot with defaults\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Overlay Plot with 5s Delay'\n    });\n\n    const swgWith5sDelay = await createExampleTelemetryObject(page, overlayPlot.uuid);\n\n    await page.goto(swgWith5sDelay.url);\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n\n    //Edit Example Telemetry Object to include 5s loading Delay\n    await page.locator('[aria-label=\"Loading Delay \\\\(ms\\\\)\"]').fill('5000');\n\n    await Promise.all([\n      page.waitForNavigation(),\n      page.locator('text=OK').click(),\n      //Wait for Save Banner to appear\n      page.locator('.c-message-banner__message').hover({ trial: true })\n    ]);\n\n    // focus the overlay plot\n    await page.goto(overlayPlot.url);\n\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);\n\n    // Clear Recently Viewed\n    await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    //Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../e2e/test-data/overlay_plot_with_delay_storage.json', import.meta.url)\n      )\n    });\n  });\n});\n\ntest.describe('Generate Conditional Styling Data @localStorage @generatedata', () => {\n  test('Generate basic condition set', async ({ page, context }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create a Condition Set\n    const conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Test Condition Set'\n    });\n\n    // Create a Telemetry Object (Sine Wave Generator)\n    const swg = await createExampleTelemetryObject(page, conditionSet.uuid);\n\n    // Edit the Telemetry Object to have a 10hz data rate (Gotta go fast!)\n    await page.goto(swg.url);\n    await page.getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page.getByLabel('Period', { exact: true }).fill('5');\n    await page.getByLabel('Save').click();\n\n    // Edit the Condition Set\n    await page.goto(conditionSet.url);\n    await page.getByLabel('Edit Object').click();\n\n    // Add a Condition to the Condition Set\n    await page.getByLabel('Add Condition').click();\n    await page.getByLabel('Condition Name Input').first().fill('Test Condition');\n    await page.getByLabel('Condition Output Type').first().selectOption('String');\n    await page.getByLabel('Condition Output String').first().fill('Test Condition Met');\n\n    // Condition: True if sine value > 0 (half the time)\n    await page.getByLabel('Criterion Telemetry Selection').selectOption(swg.name);\n    await page.getByLabel('Criterion Metadata Selection').selectOption('Sine');\n    await page.getByLabel('Criterion Comparison Selection').selectOption('is greater than');\n    await page.getByLabel('Criterion Input').first().fill('0');\n\n    // Rename default condition\n    await page.getByLabel('Condition Output String').nth(1).fill('Test Condition Unmet');\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../e2e/test-data/condition_set_storage.json', import.meta.url)\n      )\n    });\n  });\n});\n\ntest.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {\n  test.use({\n    storageState: fileURLToPath(\n      new URL('../../../e2e/test-data/overlay_plot_storage.json', import.meta.url)\n    )\n  });\n  test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.locator('a').filter({ hasText: overlayPlotName }).click();\n    // TODO: Flesh Out Assertions against created Objects\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Plot Series Items').getByLabel('Expand').click();\n\n    // TODO: Modify the Overlay Plot to use fixed Scaling\n    // TODO: Verify Autoscaling.\n\n    // TODO: Fix accessibility of Plot Series Properties tables\n    // Assert that the Plot Series properties have the correct values\n    await expect(\n      page.locator('[role=cell]:has-text(\"Value\")~[role=cell]:has-text(\"sin\")')\n    ).toBeVisible();\n    await expect(\n      page.locator(\n        '[role=cell]:has-text(\"Line Method\")~[role=cell]:has-text(\"Linear interpolation\")'\n      )\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Markers\")~[role=cell]:has-text(\"Point: 2px\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Alarm Markers\")~[role=cell]:has-text(\"Enabled\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Limit Lines\")~[role=cell]:has-text(\"Disabled\")')\n    ).toBeVisible();\n  });\n});\n\ntest.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorage @generatedata', () => {\n  test.use({\n    storageState: fileURLToPath(\n      new URL('../../../e2e/test-data/overlay_plot_with_delay_storage.json', import.meta.url)\n    )\n  });\n  test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {\n    const plotName = 'Overlay Plot with 5s Delay';\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.locator('a').filter({ hasText: plotName }).click();\n    // TODO: Flesh Out Assertions against created Objects\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Plot Series Items').getByLabel('Expand').click();\n\n    // TODO: Modify the Overlay Plot to use fixed Scaling\n    // TODO: Verify Autoscaling.\n\n    // TODO: Fix accessibility of Plot Series Properties tables\n    // Assert that the Plot Series properties have the correct values\n    await expect(\n      page.locator('[role=cell]:has-text(\"Value\")~[role=cell]:has-text(\"sin\")')\n    ).toBeVisible();\n    await expect(\n      page.locator(\n        '[role=cell]:has-text(\"Line Method\")~[role=cell]:has-text(\"Linear interpolation\")'\n      )\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Markers\")~[role=cell]:has-text(\"Point: 2px\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Alarm Markers\")~[role=cell]:has-text(\"Enabled\")')\n    ).toBeVisible();\n    await expect(\n      page.locator('[role=cell]:has-text(\"Limit Lines\")~[role=cell]:has-text(\"Disabled\")')\n    ).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/framework/pluginFixtures.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to testing our use of our custom fixtures to verify\nthat they are working as expected.\n*/\n\nimport { test } from '../../pluginFixtures.js';\n\n// eslint-disable-next-line playwright/no-skipped-test\ntest.describe.skip('pluginFixtures tests', () => {\n  // test.use({ domainObjectName: 'Timer' });\n  // let timerUUID;\n  // test('Creates a timer object @framework', ({ domainObject }) => {\n  //     const { uuid } = domainObject;\n  //     const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;\n  //     expect(uuid).toMatch(uuidRegexp);\n  //     timerUUID = uuid;\n  // });\n  // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {\n  //     const { uuid } = domainObject;\n  //     expect(uuid).toEqual(timerUUID);\n  // });\n});\n"
  },
  {
    "path": "e2e/tests/functional/MCT.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Bootstrapping Open MCT', () => {\n  test('Open MCT can be bootstrapped into a specified div container using a selector string', async ({\n    page\n  }) => {\n    const openmctLocation = '/openmct.js';\n    await page.goto('./test-data/blank.html');\n    await page.setContent(`\n      <!doctype html>\n      <html>\n      <head>\n        <script src=\"${openmctLocation}\"></script>\n        <script>\n          openmct.install(openmct.plugins.LocalStorage());\n          openmct.install(openmct.plugins.Espresso());\n          openmct.install(openmct.plugins.UTCTimeSystem());\n          openmct.install(openmct.plugins.MyItems());\n          openmct.start('#test-container');\n        </script>\n      </head>\n      <body>\n        <div id=\"test-container\"></div>\n      </body>\n    </html>`);\n    //First, confirm initial test assumptions\n    await page.waitForLoadState('domcontentloaded');\n    await expect(page.locator('#openmct-app')).toBeVisible();\n  });\n\n  test('Open MCT can be bootstrapped into a specified div container using a dom element', async ({\n    page\n  }) => {\n    const openmctLocation = '/openmct.js';\n    await page.goto('./test-data/blank.html');\n    await page.setContent(`\n      <!doctype html>\n      <html>\n      <head>\n        <script src=\"${openmctLocation}\"></script>\n        <script>\n          openmct.install(openmct.plugins.LocalStorage());\n          openmct.install(openmct.plugins.Espresso());\n          openmct.install(openmct.plugins.UTCTimeSystem());\n          openmct.install(openmct.plugins.MyItems());\n          document.addEventListener('DOMContentLoaded', () => {\n            const testContainer = document.getElementById('test-container');\n            openmct.start(testContainer);\n          });\n        </script>\n      </head>\n      <body>\n        <div id=\"test-container\"></div>\n      </body>\n    </html>`);\n    await page.waitForLoadState('domcontentloaded');\n    await expect(page.locator('#openmct-app')).toBeVisible();\n  });\n\n  test('If no container is specified and the body has no child element, Open MCT will create a div and bootstrap into it', async ({\n    page\n  }) => {\n    const openmctLocation = '/openmct.js';\n    await page.goto('./test-data/blank.html');\n    await page.setContent(`\n      <!doctype html>\n      <html>\n      <head>\n        <script src=\"${openmctLocation}\"></script>\n        <script>\n          openmct.install(openmct.plugins.LocalStorage());\n          openmct.install(openmct.plugins.Espresso());\n          openmct.install(openmct.plugins.UTCTimeSystem());\n          openmct.install(openmct.plugins.MyItems());\n          openmct.start();\n        </script>\n      </head>\n      <body>\n      </body>\n    </html>`);\n    await page.waitForLoadState('domcontentloaded');\n    await expect(page.locator('#openmct-app')).toBeVisible();\n  });\n\n  test('If empty selector is provided, throws an error', async ({ page }) => {\n    const openmctLocation = '/openmct.js';\n    await page.goto('./test-data/blank.html');\n    await page.setContent(`\n      <!doctype html>\n      <html>\n      <head>\n        <script src=\"${openmctLocation}\"></script>\n        <script>\n          openmct.install(openmct.plugins.LocalStorage());\n          openmct.install(openmct.plugins.Espresso());\n          openmct.install(openmct.plugins.UTCTimeSystem());\n          openmct.install(openmct.plugins.MyItems());\n        </script>\n      </head>\n      <body>\n      </body>\n    </html>`);\n    await page.waitForLoadState('domcontentloaded');\n    const errorMessage = await page.evaluate(() => {\n      try {\n        // eslint-disable-next-line no-undef\n        openmct.start(' ');\n      } catch (error) {\n        return error.message;\n      }\n    });\n    expect(errorMessage).toContain('Invalid HTML element or selector');\n    await expect(page.locator('#openmct-app')).toBeHidden();\n  });\n\n  test('If invalid selector is provided, throws an error', async ({ page }) => {\n    const openmctLocation = '/openmct.js';\n    await page.goto('./test-data/blank.html');\n    await page.setContent(`\n      <!doctype html>\n      <html>\n      <head>\n        <script src=\"${openmctLocation}\"></script>\n        <script>\n          openmct.install(openmct.plugins.LocalStorage());\n          openmct.install(openmct.plugins.Espresso());\n          openmct.install(openmct.plugins.UTCTimeSystem());\n          openmct.install(openmct.plugins.MyItems());\n        </script>\n      </head>\n      <body>\n      </body>\n    </html>`);\n    await page.waitForLoadState('domcontentloaded');\n    const errorMessage = await page.evaluate(() => {\n      try {\n        // eslint-disable-next-line no-undef\n        openmct.start('someInvalidSelector1a2s3d4f5g6h7j8l');\n      } catch (error) {\n        return error.message;\n      }\n    });\n    expect(errorMessage).toContain(\n      'No element found with selector someInvalidSelector1a2s3d4f5g6h7j8l'\n    );\n    await expect(page.locator('#openmct-app')).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/branding.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify branding related components.\n*/\n\nimport { expect, test } from '../../baseFixtures.js';\n\ntest.describe('Branding tests', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n  test('About Modal launches with basic branding properties', async ({ page }) => {\n    await page.getByLabel('About Modal').click();\n\n    // Verify that the NASA Logo Appears\n    await expect(page.getByAltText('Open MCT Splash Logo')).toBeVisible();\n\n    // Modify the Build information in 'about' Modal\n    await expect.soft(page.getByLabel('Version Number')).toContainText(/Version: \\d/);\n    await expect\n      .soft(page.getByLabel('Build Date'))\n      .toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);\n    await expect.soft(page.getByLabel('Revision')).toContainText(/Revision: \\b[0-9a-f]{5,40}\\b/);\n    await expect.soft(page.getByLabel('Branch')).toContainText(/Branch: ./);\n  });\n  test('Verify Links in About Modal @2p', async ({ page }) => {\n    // Click About button\n    await page.getByLabel('About Modal').click();\n\n    // Verify that clicking on the third party licenses information opens up another tab on licenses url\n    const [page2] = await Promise.all([\n      page.waitForEvent('popup'),\n      page.getByText('click here for third party licensing information').click()\n    ]);\n    await page2.waitForLoadState('domcontentloaded'); //Avoids timing issues with juggler/firefox\n    expect(page2.waitForURL('**/licenses**')).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/clearDataAction.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nVerify that the \"Clear Data\" menu action performs as expected for various object types.\n*/\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\nconst backgroundImageSelector = '.c-imagery__main-image__background-image';\n\ntest.describe('Clear Data Action', () => {\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a default 'Example Imagery' object\n    const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });\n\n    // Verify that the created object is focused\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);\n    await page.locator('.c-imagery__main-image__bg').hover({ trial: true });\n    await expect(page.locator(backgroundImageSelector)).toBeVisible();\n  });\n  test('works as expected with Example Imagery', async ({ page }) => {\n    expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0);\n    // Click the \"Clear Data\" menu action\n    await page.getByTitle('More actions').click();\n    await expect(\n      page.getByRole('menuitem', {\n        name: 'Clear Data for Object'\n      })\n    ).toBeEnabled();\n    await page\n      .getByRole('menuitem', {\n        name: 'Clear Data for Object'\n      })\n      .click();\n\n    // Verify that the background image is no longer visible\n    await expect(page.locator(backgroundImageSelector)).toBeHidden();\n    await expect(page.locator('.c-thumb__image')).toHaveCount(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/couchdb.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is meant to be executed against a couchdb container. More doc to come\n *\n */\n\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {\n  test.use({ failOnConsoleError: false });\n  //TODO BeforeAll Verify CouchDB Connectivity with APIContext\n  test('Shows green if connected', async ({ page }) => {\n    await page.route('**/openmct/mine', (route) => {\n      route.fulfill({\n        status: 200,\n        contentType: 'application/json',\n        body: JSON.stringify({})\n      });\n    });\n\n    //Go to baseURL\n    await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {\n      waitUntil: 'domcontentloaded'\n    });\n    await expect(page.locator('div:has-text(\"CouchDB is connected\")').nth(3)).toBeVisible();\n  });\n  test('Shows red if not connected', async ({ page }) => {\n    await page.route('**/openmct/**', (route) => {\n      route.fulfill({\n        status: 503,\n        contentType: 'application/json',\n        body: JSON.stringify({})\n      });\n    });\n\n    //Go to baseURL\n    await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {\n      waitUntil: 'domcontentloaded'\n    });\n    await expect(page.locator('div:has-text(\"CouchDB is offline\")').nth(3)).toBeVisible();\n  });\n  test('Shows unknown if it receives an unexpected response code', async ({ page }) => {\n    await page.route('**/openmct/mine', (route) => {\n      route.fulfill({\n        status: 418,\n        contentType: 'application/json',\n        body: JSON.stringify({})\n      });\n    });\n\n    //Go to baseURL\n    await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {\n      waitUntil: 'domcontentloaded'\n    });\n    await expect(page.locator('div:has-text(\"CouchDB connectivity unknown\")').nth(3)).toBeVisible();\n  });\n});\n\ntest.describe('CouchDB initialization with mocked responses @couchdb', () => {\n  test.use({ failOnConsoleError: false });\n  test(\"'My Items' folder is created if it doesn't exist\", async ({ page }) => {\n    const mockedMissingObjectResponseFromCouchDB = {\n      status: 404,\n      contentType: 'application/json',\n      body: JSON.stringify({})\n    };\n\n    // Override the first request to GET openmct/mine to return a 404.\n    // This simulates the case of starting Open MCT with a fresh database\n    // and no \"My Items\" folder created yet.\n    await page.route(\n      '**/mine',\n      (route) => {\n        route.fulfill(mockedMissingObjectResponseFromCouchDB);\n      },\n      { times: 1 }\n    );\n\n    // Set up promise to verify that a PUT request to create \"My Items\"\n    // folder was made.\n    const putMineFolderRequest = page.waitForRequest(\n      (req) => req.url().endsWith('/mine') && req.method() === 'PUT'\n    );\n\n    // Set up promise to verify that a GET request to retrieve \"My Items\"\n    // folder was made.\n    const getMineFolderRequest = page.waitForRequest(\n      (req) => req.url().endsWith('/mine') && req.method() === 'GET'\n    );\n\n    // Go to baseURL.\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Wait for both requests to resolve.\n    await Promise.all([putMineFolderRequest, getMineFolderRequest]);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/example/eventGenerator.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding the example event generator.\n*/\n\nimport { createDomainObjectWithDefaults } from '../../../appActions.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\ntest.describe('Example Event Generator CRUD Operations', () => {\n  test('Can create a Test Event Generator and it results in the table View', async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Create a name for the object\n    const newObjectName = 'Test Event Generator';\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      name: newObjectName\n    });\n\n    //Assertions against newly created object which define standard behavior\n    await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);\n  });\n});\n\ntest.describe('Example Event Generator Telemetry Event Verification', () => {\n  test.fixme('telemetry is coming in for test event', async ({ page }) => {\n    // Go to object created in step one\n    // Verify the telemetry table is filled with > 1 row\n  });\n  test.fixme('telemetry is sorted by time ascending', async ({ page }) => {\n    // Go to object created in step one\n    // Verify the telemetry table has a class with \"is-sorting asc\"\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/example/eventWithAcknowledgeGenerator.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';\nimport { MISSION_TIME } from '../../../constants.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst TELEMETRY_RATE = 2500;\n\ntest.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await setRealTimeMode(page);\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator with Acknowledge'\n    });\n  });\n\n  test('Rows are updatable in place', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7938'\n    });\n\n    await test.step('First telemetry datum gets added as new row', async () => {\n      await page.clock.fastForward(TELEMETRY_RATE);\n      const rows = page.getByLabel('table content').getByLabel('Table Row');\n      const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');\n\n      await expect(rows).toHaveCount(1);\n      await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');\n    });\n\n    await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {\n      await page.clock.fastForward(TELEMETRY_RATE * 2);\n      const rows = page.getByLabel('table content').getByLabel('Table Row');\n      const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');\n\n      await expect(rows).toHaveCount(1);\n      await expect(acknowledgeCell).toHaveAttribute('title', 'OK');\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding conditionSets.\n*/\n\nimport { expect, test } from '../../../../baseFixtures.js';\n\ntest.describe('Sine Wave Generator', () => {\n  test('Create new Sine Wave Generator Object and validate create Form Logic', async ({\n    page,\n    browserName\n  }) => {\n    // eslint-disable-next-line playwright/no-skipped-test\n    test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');\n\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click Sine Wave Generator\n    await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();\n\n    // Verify that the each required field has required indicator\n    // Title\n    await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/);\n\n    const formLocator = page.locator('.c-form__contents');\n\n    // Verify that the Notes row does not have a required indicator\n    await expect(\n      formLocator.locator('.form-row', { hasText: 'Notes' }).locator('.c-form-row__state-indicator')\n    ).not.toHaveClass(/req/);\n    await formLocator.locator('textarea[type=\"text\"]').fill('Optional Note Text');\n\n    // Period\n    await expect(\n      formLocator\n        .locator('.form-row', { hasText: 'Period' })\n        .locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Amplitude\n    await expect(\n      formLocator\n        .locator('.form-row', { hasText: 'Amplitude' })\n        .locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Offset\n    await expect(\n      formLocator\n        .locator('.form-row', { hasText: 'Offset' })\n        .locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Data Rate\n    await expect(\n      formLocator\n        .locator('.form-row', { hasText: 'Data Rate (hz)' })\n        .locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Phase\n    await expect(\n      formLocator.locator('.form-row', { hasText: 'Phase' }).locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Randomness\n    await expect(\n      formLocator\n        .locator('.form-row', { hasText: 'Randomness' })\n        .locator('.c-form-row__state-indicator')\n    ).toHaveClass(/req/);\n\n    // Verify that by removing value from required text field shows invalid indicator\n    await page\n      .locator(\n        'text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type=\"text\"]'\n      )\n      .clear();\n    await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);\n\n    // Verify that by adding value to empty required text field changes invalid to valid indicator\n    await page\n      .locator(\n        'text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type=\"text\"]'\n      )\n      .fill('New Sine Wave Generator');\n    await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/);\n\n    // Verify that by removing value from required number field shows invalid indicator\n    await page.locator('.field.control.l-input-sm input').first().clear();\n    await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(\n      /invalid/\n    );\n\n    // Verify that by adding value to empty required number field changes invalid to valid indicator\n    await page.locator('.field.control.l-input-sm input').first().fill('3');\n    await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(\n      /valid/\n    );\n\n    // Verify that can change value of number field by up/down arrows keys\n    // Click .field.control.l-input-sm input >> nth=0\n    await page.locator('.field.control.l-input-sm input').first().click();\n    // Press ArrowUp 3 times to change value from 3 to 6\n    await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');\n    await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');\n    await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');\n\n    const value = page.locator('.field.control.l-input-sm input').first();\n    await expect(value).toHaveValue('6');\n\n    //Click save button\n    await page.getByLabel('Save').click();\n\n    // Verify that the Sine Wave Generator is displayed and correct\n    // Verify object properties\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(\n      'New Sine Wave Generator'\n    );\n\n    // Verify canvas rendered and can be interacted with\n    await page\n      .locator('canvas')\n      .nth(1)\n      .click({\n        position: {\n          x: 341,\n          y: 28\n        }\n      });\n\n    // Verify that where we click on canvas shows the number we clicked on\n    // Note that any number will do, we just care that a number exists\n    await expect(page.locator('.value-to-display-nearestValue')).toContainText(\n      /[+-]?([0-9]*[.])?[0-9]+/\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/example/generator/sineWaveStalenessProvider.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithRealTime\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Staleness', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Does not show staleness after navigating from a stale object', async ({ page }) => {\n    const staleSWG = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG'\n    });\n\n    // edit properties and enable staleness updates\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit properties...').click();\n    await page.getByLabel('Provide Staleness Updates', { exact: true }).click();\n    await page.getByLabel('Save').click();\n\n    const folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Folder 1'\n    });\n\n    // Navigate to the stale object\n    await navigateToObjectWithRealTime(page, staleSWG.url);\n\n    // Assert that staleness is shown\n    await expect(page.getByLabel('Object View')).toHaveClass(/is-stale/, {\n      timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated\n    });\n\n    // Immediately navigate to the folder\n    await page.goto(folder.url);\n\n    // Verify that staleness is not shown\n    await expect(page.getByLabel('Object View')).not.toHaveClass(/is-stale/);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/forms.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify form functionality in isolation\n*/\n\nimport { fileURLToPath } from 'url';\nimport { v4 as genUuid } from 'uuid';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\nconst TEST_FOLDER = 'test folder';\nconst jsonFilePath = 'test-data/ExampleLayouts.json';\nconst imageFilePath = 'test-data/rick.jpg';\n\ntest.describe('Form Validation Behavior', () => {\n  test('Required Field indicators appear if title is empty and can be corrected', async ({\n    page\n  }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByRole('menuitem', { name: 'Folder' }).click();\n\n    // Fill in empty string into title and trigger validation with 'Tab'\n    await page.getByLabel('Title', { exact: true }).fill('');\n    await page.getByLabel('Title', { exact: true }).press('Tab');\n\n    //Required Field Form Validation\n    await expect(page.getByLabel('Save')).toBeDisabled();\n    await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);\n\n    //Correct Form Validation for missing title and trigger validation with 'Tab'\n    await page.getByLabel('Title', { exact: true }).fill(TEST_FOLDER);\n    await page.getByLabel('Title', { exact: true }).press('Tab');\n\n    //Required Field Form Validation is corrected\n    await expect(page.getByLabel('Save')).toBeEnabled();\n    await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);\n\n    //Finish Creating Domain Object\n    await page.getByLabel('Save').click();\n\n    //Verify that the Domain Object has been created with the corrected title property\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);\n  });\n});\n\ntest.describe('Form File Input Behavior', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addInitFileInputObject.js', import.meta.url))\n    });\n  });\n\n  test('Can select a JSON file type', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();\n\n    await page.setInputFiles('#fileElem', jsonFilePath);\n\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    const type = page.locator('#file-input-type');\n    await expect(type).toHaveText(`\"string\"`);\n  });\n\n  test('Can select an image file type', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();\n\n    await page.setInputFiles('#fileElem', imageFilePath);\n\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    const type = page.locator('#file-input-type');\n    await expect(type).toHaveText(`\"object\"`);\n  });\n});\n\ntest.describe('Persistence operations @addInit', () => {\n  // add non persistable root item\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addNoneditableObject.js', import.meta.url))\n    });\n  });\n\n  test('Persistability should be respected in the create form location field', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/4323'\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await page.getByRole('menuitem', { name: 'Condition Set' }).click();\n\n    await page.locator('form[name=\"mctForm\"] >> text=Persistence Testing').click();\n\n    const okButton = page.getByLabel('Save');\n    await expect(okButton).toBeDisabled();\n  });\n});\n\ntest.describe('Persistence operations @couchdb @network', () => {\n  test.use({ failOnConsoleError: false });\n  test('Editing object properties should generate a single persistence operation', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5616'\n    });\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a new 'Clock' object with default settings\n    const clock = await createDomainObjectWithDefaults(page, {\n      type: 'Clock'\n    });\n\n    // Count all persistence operations (PUT requests) for this specific object\n    let putRequestCount = 0;\n    page.on('request', (req) => {\n      if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {\n        putRequestCount += 1;\n      }\n    });\n\n    // Open the edit form for the clock object\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n\n    // Modify the display format from default 12hr -> 24hr and click 'Save'\n    await page.getByLabel('12 or 24 hour clock').selectOption({ value: 'clock24' });\n    await page.getByLabel('Save').click();\n\n    await expect\n      .poll(() => putRequestCount, {\n        message: 'Verify a single PUT request was made to persist the object',\n        timeout: 1000\n      })\n      .toEqual(1);\n  });\n  test('Can create an object after a conflict error @couchdb @network @2p', async ({\n    page,\n    openmctConfig\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5982'\n    });\n    const { myItemsFolderName } = openmctConfig;\n    // Instantiate a second page/tab\n    const page2 = await page.context().newPage();\n\n    // Both pages: Go to baseURL\n    await Promise.all([\n      page.goto('./', { waitUntil: 'domcontentloaded' }),\n      page2.goto('./', { waitUntil: 'domcontentloaded' })\n    ]);\n\n    //Slow down the test a bit\n    await expect(\n      page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })\n    ).toBeVisible();\n    await expect(\n      page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })\n    ).toBeVisible();\n\n    // Both pages: Click the Create button\n    await Promise.all([\n      page.getByRole('button', { name: 'Create' }).click(),\n      page2.getByRole('button', { name: 'Create' }).click()\n    ]);\n\n    // Both pages: Click \"Clock\" in the Create menu\n    await Promise.all([\n      page.getByRole('menuitem', { name: 'Clock' }).click(),\n      page2.getByRole('menuitem', { name: 'Clock' }).click()\n    ]);\n\n    // Generate unique names for both objects\n    const nameInput = page.locator('form[name=\"mctForm\"] .first input[type=\"text\"]');\n    const nameInput2 = page2.locator('form[name=\"mctForm\"] .first input[type=\"text\"]');\n\n    // Both pages: Fill in the 'Name' form field.\n    await Promise.all([\n      nameInput.fill(''),\n      nameInput.fill(`Clock:${genUuid()}`),\n      nameInput2.fill(''),\n      nameInput2.fill(`Clock:${genUuid()}`)\n    ]);\n\n    // Both pages: Fill the \"Notes\" section with information about the\n    // currently running test and its project.\n    const testNotes = page.testNotes;\n    const notesInput = page.locator('form[name=\"mctForm\"] #notes-textarea');\n    const notesInput2 = page2.locator('form[name=\"mctForm\"] #notes-textarea');\n    await Promise.all([notesInput.fill(testNotes), notesInput2.fill(testNotes)]);\n\n    // Page 2: Click \"OK\" to create the domain object and wait for navigation.\n    // This will update the composition of the parent folder, setting the\n    // conditions for a conflict error from the first page.\n    await Promise.all([\n      page2.waitForLoadState(),\n      page2.getByLabel('Save').click(),\n      // Wait for Save Banner to appear\n      page2.locator('.c-message-banner__message').hover({ trial: true })\n    ]);\n\n    // Close Page 2, we're done with it.\n    await page2.close();\n\n    // Page 1: Click \"OK\" to create the domain object and wait for navigation.\n    // This will trigger a conflict error upon attempting to update\n    // the composition of the parent folder.\n    await Promise.all([\n      page.waitForLoadState(),\n      page.getByLabel('Save').click(),\n      // Wait for Save Banner to appear\n      page.locator('.c-message-banner__message').hover({ trial: true })\n    ]);\n\n    // Page 1: Verify that the conflict has occurred and an error notification is displayed.\n    await expect(\n      page.locator('.c-message-banner__message', {\n        hasText: 'Conflict detected while saving mine'\n      })\n    ).toBeVisible();\n\n    // Page 1: Start logging console errors from this point on\n    let errors = [];\n    page.on('console', (msg) => {\n      if (msg.type() === 'error') {\n        errors.push(msg.text());\n      }\n    });\n\n    // Page 1: Try to create a clock with the page that received the conflict.\n    const clockAfterConflict = await createDomainObjectWithDefaults(page, {\n      type: 'Clock'\n    });\n\n    // Page 1: Wait for save progress dialog to appear/disappear\n    await page\n      .locator('.c-message-banner__message', {\n        hasText:\n          'Do not navigate away from this page or close this browser tab while this message is displayed.',\n        state: 'visible'\n      })\n      .waitFor({ state: 'hidden' });\n\n    // Page 1: Navigate to 'My Items' and verify that the second clock was created\n    await page.goto('./#/browse/mine');\n    await expect(\n      page.locator(`.c-grid-item__name[title=\"${clockAfterConflict.name}\"]`)\n    ).toBeVisible();\n\n    // Verify no console errors occurred\n    expect(errors).toHaveLength(0);\n  });\n});\n\ntest.describe('Form Correctness by Object Type', () => {\n  test.fixme('Verify correct behavior of number object (SWG)', async ({ page }) => {});\n  test.fixme('Verify correct behavior of number object Timer', async ({ page }) => {});\n  test.fixme('Verify correct behavior of number object Plan View', async ({ page }) => {});\n  test.fixme('Verify correct behavior of number object Clock', async ({ page }) => {});\n  test.fixme('Verify correct behavior of number object Hyperlink', async ({ page }) => {});\n});\n"
  },
  {
    "path": "e2e/tests/functional/menu.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify persistability checks\n*/\n\nimport { fileURLToPath } from 'url';\n\nimport { expect, test } from '../../baseFixtures.js';\n\ntest.describe('Persistence operations @addInit', () => {\n  // add non persistable root item\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addNoneditableObject.js', import.meta.url))\n    });\n  });\n\n  test('Non-persistable objects should not show persistence related actions', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.locator('text=Persistence Testing').first().click({\n      button: 'right'\n    });\n\n    const menuOptions = page.locator('.c-menu li');\n\n    await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']);\n    await expect(menuOptions).not.toContainText([\n      'Move',\n      'Duplicate',\n      'Remove',\n      'Add New Folder',\n      'Edit Properties...',\n      'Export as JSON',\n      'Import from JSON'\n    ]);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/missionStatus.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify persistability checks\n*/\n\nimport { fileURLToPath } from 'url';\n\nimport { expect, test } from '../../baseFixtures.js';\n\ntest.describe('Mission Status @addInit', () => {\n  const NO_GO = '0';\n  const GO = '1';\n  test.beforeEach(async ({ page }) => {\n    // FIXME: determine if plugins will be added to index.html or need to be injected\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await expect(page.getByText('Select Role')).toBeVisible();\n    // Description should be empty https://github.com/nasa/openmct/issues/6978\n    await expect(page.getByLabel('Dialog message')).toBeHidden();\n    // set role\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    // dismiss role confirmation popup\n    await page.getByRole('button', { name: 'Dismiss' }).click();\n  });\n\n  test('Basic functionality', async ({ page }) => {\n    const imageryStatusSelect = page.getByRole('combobox', { name: 'Imagery' });\n    const commandingStatusSelect = page.getByRole('combobox', { name: 'Commanding' });\n    const drivingStatusSelect = page.getByRole('combobox', { name: 'Driving' });\n    const missionStatusPanel = page.getByRole('dialog', { name: 'User Control Panel' });\n\n    await test.step('Mission status panel shows/hides when toggled', async () => {\n      // Ensure that clicking the button toggles the dialog\n      await page.getByLabel('Toggle Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeVisible();\n      await page.getByLabel('Toggle Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeHidden();\n      await page.getByLabel('Toggle Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeVisible();\n\n      // Ensure that clicking the close button closes the dialog\n      await page.getByLabel('Close Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeHidden();\n      await page.getByLabel('Toggle Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeVisible();\n\n      // Ensure clicking off the dialog also closes it\n      await page.getByLabel('My Items Grid View').click();\n      await expect(missionStatusPanel).toBeHidden();\n      await page.getByLabel('Toggle Mission Status Panel').click();\n      await expect(missionStatusPanel).toBeVisible();\n    });\n\n    await test.step('Mission action statuses have correct defaults and can be set', async () => {\n      await expect(imageryStatusSelect).toHaveValue(NO_GO);\n      await expect(commandingStatusSelect).toHaveValue(NO_GO);\n      await expect(drivingStatusSelect).toHaveValue(NO_GO);\n\n      await setMissionStatus(page, 'Imagery', GO);\n      await expect(imageryStatusSelect).toHaveValue(GO);\n      await expect(commandingStatusSelect).toHaveValue(NO_GO);\n      await expect(drivingStatusSelect).toHaveValue(NO_GO);\n\n      await setMissionStatus(page, 'Commanding', GO);\n      await expect(imageryStatusSelect).toHaveValue(GO);\n      await expect(commandingStatusSelect).toHaveValue(GO);\n      await expect(drivingStatusSelect).toHaveValue(NO_GO);\n\n      await setMissionStatus(page, 'Driving', GO);\n      await expect(imageryStatusSelect).toHaveValue(GO);\n      await expect(commandingStatusSelect).toHaveValue(GO);\n      await expect(drivingStatusSelect).toHaveValue(GO);\n\n      await setMissionStatus(page, 'Imagery', NO_GO);\n      await expect(imageryStatusSelect).toHaveValue(NO_GO);\n      await expect(commandingStatusSelect).toHaveValue(GO);\n      await expect(drivingStatusSelect).toHaveValue(GO);\n\n      await setMissionStatus(page, 'Commanding', NO_GO);\n      await expect(imageryStatusSelect).toHaveValue(NO_GO);\n      await expect(commandingStatusSelect).toHaveValue(NO_GO);\n      await expect(drivingStatusSelect).toHaveValue(GO);\n\n      await setMissionStatus(page, 'Driving', NO_GO);\n      await expect(imageryStatusSelect).toHaveValue(NO_GO);\n      await expect(commandingStatusSelect).toHaveValue(NO_GO);\n      await expect(drivingStatusSelect).toHaveValue(NO_GO);\n    });\n  });\n});\n\n/**\n *\n * @param {import('@playwright/test').Page} page\n * @param {'Commanding'|'Imagery'|'Driving'} action\n * @param {'0'|'1'} status\n */\nasync function setMissionStatus(page, action, status) {\n  await page.getByRole('combobox', { name: action }).selectOption(status);\n  await expect(\n    page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })\n  ).toBeVisible();\n  await page.getByLabel('Dismiss').click();\n}\n"
  },
  {
    "path": "e2e/tests/functional/moveAndLinkObjects.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding moving & linking objects.\n*/\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Move & link item tests', () => {\n  test('Create a basic object and verify that it can be moved to another folder', async ({\n    page,\n    openmctConfig\n  }) => {\n    const { myItemsFolderName } = openmctConfig;\n\n    // Go to Open MCT\n    await page.goto('./');\n\n    const parentFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Parent Folder'\n    });\n    const childFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Child Folder',\n      parent: parentFolder.uuid\n    });\n    const grandchildFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Grandchild Folder',\n      parent: childFolder.uuid\n    });\n\n    // Attempt to move parent to its own grandparent\n    await page.locator('button[title=\"Show selected item in tree\"]').click();\n\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    await treePane\n      .getByRole('treeitem', {\n        name: 'Parent Folder'\n      })\n      .click({\n        button: 'right'\n      });\n\n    await page\n      .getByRole('menuitem', {\n        name: /Move/\n      })\n      .click();\n\n    const createModalTree = page.getByRole('tree', {\n      name: 'Create Modal Tree'\n    });\n    const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: myItemsFolderName\n    });\n    await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await myItemsLocatorTreeItem.click();\n\n    const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: parentFolder.name\n    });\n    await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await parentFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: new RegExp(childFolder.name)\n    });\n    await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await childFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: grandchildFolder.name\n    });\n    await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await grandchildFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    await parentFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n    await page.locator('[aria-label=\"Cancel\"]').click();\n\n    // Move Child Folder from Parent Folder to My Items\n    await treePane\n      .getByRole('treeitem', {\n        name: new RegExp(childFolder.name)\n      })\n      .click({\n        button: 'right'\n      });\n    await page\n      .getByRole('menuitem', {\n        name: /Move/\n      })\n      .click();\n    await myItemsLocatorTreeItem.click();\n\n    await page.locator('[aria-label=\"Save\"]').click();\n    const myItemsPaneTreeItem = treePane.getByRole('treeitem', {\n      name: myItemsFolderName\n    });\n\n    // Expect that Child Folder is in My Items, the root folder\n    expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();\n  });\n  test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({\n    page,\n    openmctConfig\n  }) => {\n    const { myItemsFolderName } = openmctConfig;\n\n    // Go to Open MCT\n    await page.goto('./');\n\n    // Create Telemetry Table\n    let telemetryTable = 'Test Telemetry Table';\n    await page.locator('button:has-text(\"Create\")').click();\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Telemetry Table\")').click();\n    await page.locator('text=Properties Title Notes >> input[type=\"text\"]').click();\n    await page.locator('text=Properties Title Notes >> input[type=\"text\"]').fill(telemetryTable);\n\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // Finish editing and save Telemetry Table\n    await page.locator('.c-button--menu.c-button--major.icon-save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Create New Folder Basic Domain Object\n    let folder = 'Test Folder';\n    await page.locator('button:has-text(\"Create\")').click();\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Folder\")').click();\n    await page.locator('text=Properties Title Notes >> input[type=\"text\"]').click();\n    await page.locator('text=Properties Title Notes >> input[type=\"text\"]').fill(folder);\n\n    // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)\n    await page.locator(`form[name=\"mctForm\"] >> text=${telemetryTable}`).click();\n    let okButton = page.locator('button.c-button.c-button--major:has-text(\"OK\")');\n    let okButtonStateDisabled = await okButton.isDisabled();\n    expect.soft(okButtonStateDisabled).toBeTruthy();\n\n    // Continue test regardless of assertion and create it in My Items\n    await page.locator(`form[name=\"mctForm\"] >> text=${myItemsFolderName}`).click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // Open My Items\n    await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();\n\n    // Select Folder Object and select Move from context menu\n    await Promise.all([page.waitForNavigation(), page.locator(`a:has-text(\"${folder}\")`).click()]);\n    await page\n      .locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon')\n      .click({\n        button: 'right'\n      });\n    await page.locator('li.icon-move').click();\n\n    // See if it's possible to put the folder in the Telemetry object after creation\n    await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();\n    await page.locator(`form[name=\"mctForm\"] >> text=${telemetryTable}`).click();\n    let okButton2 = page.locator('button.c-button.c-button--major:has-text(\"OK\")');\n    let okButtonStateDisabled2 = await okButton2.isDisabled();\n    expect(okButtonStateDisabled2).toBeTruthy();\n  });\n\n  test('Create a basic object and verify that it can be linked to another folder', async ({\n    page,\n    openmctConfig\n  }) => {\n    const { myItemsFolderName } = openmctConfig;\n\n    // Go to Open MCT\n    await page.goto('./');\n\n    const parentFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Parent Folder'\n    });\n    const childFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Child Folder',\n      parent: parentFolder.uuid\n    });\n    const grandchildFolder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Grandchild Folder',\n      parent: childFolder.uuid\n    });\n\n    // Attempt to move parent to its own grandparent\n    await page.locator('button[title=\"Show selected item in tree\"]').click();\n\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    await treePane\n      .getByRole('treeitem', {\n        name: 'Parent Folder'\n      })\n      .click({\n        button: 'right'\n      });\n\n    await page\n      .getByRole('menuitem', {\n        name: /Move/\n      })\n      .click();\n\n    const createModalTree = page.getByRole('tree', {\n      name: 'Create Modal Tree'\n    });\n    const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: myItemsFolderName\n    });\n    await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await myItemsLocatorTreeItem.click();\n\n    const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: parentFolder.name\n    });\n    await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await parentFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: new RegExp(childFolder.name)\n    });\n    await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await childFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {\n      name: grandchildFolder.name\n    });\n    await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();\n    await grandchildFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n\n    await parentFolderLocatorTreeItem.click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n    await page.locator('[aria-label=\"Cancel\"]').click();\n\n    // Move Child Folder from Parent Folder to My Items\n    await treePane\n      .getByRole('treeitem', {\n        name: new RegExp(childFolder.name)\n      })\n      .click({\n        button: 'right'\n      });\n    await page\n      .getByRole('menuitem', {\n        name: /Link/\n      })\n      .click();\n    await myItemsLocatorTreeItem.click();\n\n    await page.locator('[aria-label=\"Save\"]').click();\n    const myItemsPaneTreeItem = treePane.getByRole('treeitem', {\n      name: myItemsFolderName\n    });\n\n    // Expect that Child Folder is in My Items, the root folder\n    expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();\n  });\n});\n\ntest.fixme(\n  'Cannot move a previously created domain object to non-persistable object in Move Modal',\n  async ({ page }) => {\n    //Create a domain object\n    //Save Domain object\n    //Move Object and verify that cannot select non-persistable object\n    //Move Object to My Items\n    //Verify successful move\n  }\n);\n"
  },
  {
    "path": "e2e/tests/functional/notification.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify Open MCT's Notification functionality\n*/\n\nimport { createDomainObjectWithDefaults, createNotification } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Notifications List', () => {\n  test.fixme('Notifications can be dismissed individually', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6820'\n    });\n\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create an error notification with the message \"Error message\"\n    await createNotification(page, {\n      severity: 'error',\n      message: 'Error message'\n    });\n\n    // Create an alert notification with the message \"Alert message\"\n    await createNotification(page, {\n      severity: 'alert',\n      message: 'Alert message'\n    });\n\n    // Verify that there is a button with aria-label \"Review 2 Notifications\"\n    await expect(page.locator('button[aria-label=\"Review 2 Notifications\"]')).toHaveCount(1);\n\n    // Click on button with aria-label \"Review 2 Notifications\"\n    await page.getByLabel('Review 2 Notifications').click();\n\n    // Click on button with aria-label=\"Dismiss notification of Error message\"\n    await page.getByLabel('Dismiss notification of Error message').click();\n\n    // Verify there is no a notification (listitem) with the text \"Error message\" since it was dismissed\n    expect(await page.locator('div[role=\"dialog\"] div[role=\"listitem\"]').innerText()).not.toContain(\n      'Error message'\n    );\n\n    // Verify there is still a notification (listitem) with the text \"Alert message\"\n    expect(await page.locator('div[role=\"dialog\"] div[role=\"listitem\"]').innerText()).toContain(\n      'Alert message'\n    );\n\n    // Click on button with aria-label=\"Dismiss notification of Alert message\"\n    await page.getByLabel('Dismiss notification of Alert message').click();\n\n    // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed\n    await expect(page.locator('div[role=\"dialog\"]')).toHaveCount(0);\n  });\n});\n\ntest.describe('Notification Overlay', () => {\n  test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6130'\n    });\n\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a new Display Layout object\n    await createDomainObjectWithDefaults(page, { type: 'Display Layout' });\n\n    // Dismiss notification banner\n    await page.getByRole('button', { name: 'Dismiss' }).click();\n\n    // Click on the button \"Review 1 Notification\"\n    await page.getByRole('button', { name: 'Review 1 Notification' }).click();\n\n    // Verify that Notification List is open\n    await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeVisible();\n\n    // Wait until there is no Notification Banner\n    await expect(page.getByRole('alert')).not.toBeAttached();\n\n    // Click on the \"Close\" button of the Notification List\n    await page.getByRole('button', { name: 'Close' }).click();\n\n    // On the Display Layout object, click on the \"Edit\" button\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Click on the \"Save\" button\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Verify that Notification List is NOT open\n    await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/planning/ganttChart.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport fs from 'fs';\n\nimport { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';\nimport {\n  assertPlanActivities,\n  setBoundsToSpanAllActivities\n} from '../../../helper/planningUtils.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst testPlan1 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)\n  )\n);\nconst testPlan2 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)\n  )\n);\n\ntest.describe('Gantt Chart', () => {\n  let ganttChart;\n  let plan;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    ganttChart = await createDomainObjectWithDefaults(page, {\n      type: 'Gantt Chart'\n    });\n    plan = await createPlanFromJSON(page, {\n      json: testPlan1,\n      parent: ganttChart.uuid\n    });\n  });\n\n  test('Displays all plan events', async ({ page }) => {\n    await page.goto(ganttChart.url);\n\n    await assertPlanActivities(page, testPlan1, ganttChart.url);\n  });\n  test('Replaces a plan with a new plan', async ({ page }) => {\n    await assertPlanActivities(page, testPlan1, ganttChart.url);\n    await createPlanFromJSON(page, {\n      json: testPlan2,\n      parent: ganttChart.uuid\n    });\n    const replaceModal = page\n      .getByRole('dialog')\n      .filter({ hasText: 'This action will replace the current Plan. Do you want to continue?' });\n    await expect(replaceModal).toBeVisible();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    await assertPlanActivities(page, testPlan2, ganttChart.url);\n  });\n  test('Can select a single activity and display its details in the inspector', async ({\n    page\n  }) => {\n    test.slow();\n    await page.goto(ganttChart.url);\n\n    await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url);\n\n    const activities = Object.values(testPlan1).flat();\n    const activity = activities[0];\n    await page\n      .locator('g')\n      .filter({ hasText: new RegExp(activity.name) })\n      .click();\n    await page.getByRole('tab', { name: 'Activity' }).click();\n\n    const startDateTime = await page\n      .locator(\n        '.c-inspect-properties__label:has-text(\"Start DateTime\")+.c-inspect-properties__value'\n      )\n      .innerText();\n    const endDateTime = await page\n      .locator('.c-inspect-properties__label:has-text(\"End DateTime\")+.c-inspect-properties__value')\n      .innerText();\n    const duration = await page\n      .locator('.c-inspect-properties__label:has-text(\"duration\")+.c-inspect-properties__value')\n      .innerText();\n\n    const expectedStartDate = new Date(activity.start).toISOString();\n    const actualStartDate = new Date(startDateTime).toISOString();\n    const expectedEndDate = new Date(activity.end).toISOString();\n    const actualEndDate = new Date(endDateTime).toISOString();\n    const expectedDuration = getPreciseDuration(activity.end - activity.start);\n    const actualDuration = duration;\n\n    expect(expectedStartDate).toEqual(actualStartDate);\n    expect(expectedEndDate).toEqual(actualEndDate);\n    expect(expectedDuration).toEqual(actualDuration);\n  });\n  test(\"Displays a Plan's draft status\", async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6641'\n    });\n\n    // Mark the Plan's status as draft in the OpenMCT API\n    await page.evaluate(async (planObject) => {\n      await window.openmct.status.set(planObject.uuid, 'draft');\n    }, plan);\n\n    // Navigate to the Gantt Chart\n    await page.goto(ganttChart.url);\n\n    // Assert that the Plan's status is displayed as draft\n    expect(await page.locator('.c-swimlane.is-status--draft').count()).toBe(\n      Object.keys(testPlan1).length\n    );\n  });\n});\n\nconst ONE_SECOND = 1000;\nconst ONE_MINUTE = 60 * ONE_SECOND;\nconst ONE_HOUR = ONE_MINUTE * 60;\nconst ONE_DAY = ONE_HOUR * 24;\n\nfunction normalizeAge(num) {\n  const hundredtized = num * 100;\n  const isWhole = hundredtized % 100 === 0;\n\n  return isWhole ? hundredtized / 100 : num;\n}\n\nfunction padLeadingZeros(num, numOfLeadingZeros) {\n  return num.toString().padStart(numOfLeadingZeros, '0');\n}\n\nfunction toDoubleDigits(num) {\n  return padLeadingZeros(num, 2);\n}\n\nfunction toTripleDigits(num) {\n  return padLeadingZeros(num, 3);\n}\n\nfunction getPreciseDuration(value, { excludeMilliSeconds, useDayFormat } = {}) {\n  let preciseDuration;\n  const ms = value || 0;\n\n  const duration = [\n    Math.floor(normalizeAge(ms / ONE_DAY)),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))\n  ];\n  if (!excludeMilliSeconds) {\n    duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));\n  }\n\n  if (useDayFormat) {\n    // Format days as XD\n    const days = duration.shift();\n    if (days > 0) {\n      preciseDuration = `${days}D ${duration.join(':')}`;\n    } else {\n      preciseDuration = duration.join(':');\n    }\n  } else {\n    const days = toDoubleDigits(duration.shift());\n    duration.unshift(days);\n    preciseDuration = duration.join(':');\n  }\n\n  return preciseDuration;\n}\n"
  },
  {
    "path": "e2e/tests/functional/planning/plan.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport fs from 'fs';\n\nimport { createPlanFromJSON, navigateToObjectWithFixedTimeBounds } from '../../../appActions.js';\nimport {\n  addPlanGetInterceptor,\n  assertPlanActivities,\n  assertPlanOrderedSwimLanes\n} from '../../../helper/planningUtils.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst testPlan1 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)\n  )\n);\n\nconst testPlanWithOrderedLanes = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlanWithOrderedLanes.json', import.meta.url)\n  )\n);\n\ntest.describe('Plan', () => {\n  let plan;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    plan = await createPlanFromJSON(page, {\n      json: testPlan1\n    });\n  });\n\n  test('Displays all plan events', async ({ page }) => {\n    await assertPlanActivities(page, testPlan1, plan.url);\n  });\n\n  test('Displays plans with ordered swim lanes configuration', async ({ page }) => {\n    // Add configuration for swim lanes\n    await addPlanGetInterceptor(page);\n    // Create the plan\n    const planWithSwimLanes = await createPlanFromJSON(page, {\n      json: testPlanWithOrderedLanes\n    });\n    await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);\n  });\n\n  test('Allows setting the state of an activity when selected.', async ({ page }) => {\n    const groups = Object.keys(testPlan1);\n    const firstGroupKey = groups[0];\n    const firstGroupItems = testPlan1[firstGroupKey];\n    const firstActivity = firstGroupItems[0];\n    const lastActivity = firstGroupItems[firstGroupItems.length - 1];\n    const startBound = firstActivity.start;\n    // Set the endBound to the end time of the current activity\n    let endBound = lastActivity.end;\n    // eslint-disable-next-line playwright/no-conditional-in-test\n    if (endBound === startBound) {\n      // Prevent oddities with setting start and end bound equal\n      // via URL params\n      endBound += 1;\n    }\n\n    // Switch to fixed time mode with all plan events within the bounds\n    await navigateToObjectWithFixedTimeBounds(page, plan.url, startBound, endBound);\n\n    // select the first activity in the list\n    await page.getByText('Past event 1').click();\n\n    // Find the activity state section in the inspector\n    await page.getByRole('tab', { name: 'Activity' }).click();\n\n    // Check that activity state dropdown selection shows the `set status` option by default\n    await expect(page.getByLabel('Activity Status').locator(\"[aria-selected='true']\")).toHaveText(\n      'Not started'\n    );\n\n    // Change the selection of the activity status\n    await page.getByRole('combobox').selectOption({ label: 'Aborted' });\n    // select a different activity and back to the previous one\n    await page.getByText('Past event 2').click();\n    await page.getByText('Past event 1').click();\n    // Check that activity state dropdown selection shows the previously selected option by default\n    await expect(page.getByLabel('Activity Status').locator(\"[aria-selected='true']\")).toHaveText(\n      'Aborted'\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/planning/timelist.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport fs from 'fs';\n\nimport {\n  createDomainObjectWithDefaults,\n  createPlanFromJSON,\n  navigateToObjectWithFixedTimeBounds\n} from '../../../appActions.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst examplePlanSmall1 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)\n  )\n);\ntest.describe('Time List', () => {\n  test(\"Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties\", async ({\n    page\n  }) => {\n    // Goto baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const timelist = await test.step('Create a Time List', async () => {\n      const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });\n      const objectName = await page.locator('.l-browse-bar__object-name').innerText();\n      expect(objectName).toBe(createdTimeList.name);\n\n      return createdTimeList;\n    });\n\n    await test.step('Create a Plan and add it to the timelist', async () => {\n      await createPlanFromJSON(page, {\n        name: 'Test Plan',\n        json: examplePlanSmall1,\n        parent: timelist.uuid\n      });\n      const groups = Object.keys(examplePlanSmall1);\n      const firstGroupKey = groups[0];\n      const firstGroupItems = examplePlanSmall1[firstGroupKey];\n      const firstActivity = firstGroupItems[0];\n      const lastActivity = firstGroupItems[firstGroupItems.length - 1];\n      const startBound = firstActivity.start;\n      const endBound = lastActivity.end;\n\n      // Switch to fixed time mode with all plan events within the bounds\n      await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound);\n\n      // Verify all events are displayed\n      const eventCount = await page.getByRole('row').count();\n      // subtracting one for the header\n      expect(eventCount - 1).toEqual(firstGroupItems.length);\n    });\n\n    await test.step('Does not show milliseconds in times', async () => {\n      // Get an activity\n      const row = page.getByRole('row').nth(2);\n      // Verify that none fo the times have milliseconds displayed.\n      // Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong\n\n      await expect(row.locator('.--start')).not.toContainText('.');\n      await expect(row.locator('.--end')).not.toContainText('.');\n      await expect(row.locator('.--duration')).not.toContainText('.');\n    });\n\n    await test.step('Shows activity properties when a row is selected', async () => {\n      await page.getByRole('row').nth(2).click();\n\n      // Find the activity state section in the inspector\n      await page.getByRole('tab', { name: 'Activity' }).click();\n      // Check that activity state label is displayed in the inspector.\n      await expect(page.getByLabel('Activity Status').locator(\"[aria-selected='true']\")).toHaveText(\n        'Not started'\n      );\n    });\n  });\n});\n\ntest(\"View a timelist in expanded view, verify all the activities are displayed and selecting an activity shows it's properties\", async ({\n  page\n}) => {\n  // Goto baseURL\n  await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n  const timelist = await test.step('Create a Time List', async () => {\n    const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });\n    const objectName = await page.locator('.l-browse-bar__object-name').innerText();\n    expect(objectName).toBe(createdTimeList.name);\n\n    return createdTimeList;\n  });\n\n  await test.step('Create a Plan and add it to the timelist', async () => {\n    await createPlanFromJSON(page, {\n      name: 'Test Plan',\n      json: examplePlanSmall1,\n      parent: timelist.uuid\n    });\n\n    // Ensure that all activities are shown in the expanded view\n    const groups = Object.keys(examplePlanSmall1);\n    const firstGroupKey = groups[0];\n    const firstGroupItems = examplePlanSmall1[firstGroupKey];\n    const firstActivity = firstGroupItems[0];\n    const lastActivity = firstGroupItems[firstGroupItems.length - 1];\n    const startBound = firstActivity.start;\n    const endBound = lastActivity.end;\n\n    // Switch to fixed time mode with all plan events within the bounds\n    await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound);\n\n    // Change the object to edit mode\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Find the display properties section in the inspector\n    await page.getByRole('tab', { name: 'Config' }).click();\n    // Switch to expanded view and save the setting\n    await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });\n\n    // Click on the \"Save\" button\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Verify all events are displayed\n    const eventCount = await page.getByRole('row').count();\n    expect(eventCount).toEqual(firstGroupItems.length);\n  });\n\n  await test.step('Shows activity properties when a row is selected in the expanded view', async () => {\n    await page.getByRole('row').nth(2).click();\n\n    // Find the activity state section in the inspector\n    await page.getByRole('tab', { name: 'Activity' }).click();\n    // Check that activity state label is displayed in the inspector.\n    await expect(page.getByLabel('Activity Status').locator(\"[aria-selected='true']\")).toHaveText(\n      'Not started'\n    );\n  });\n\n  await test.step(\"Verify absence of progress indication for an activity that's not in progress\", async () => {\n    // When an activity is not in progress, the progress pie is not visible\n    const hidden = page.getByRole('row').locator('path').nth(1);\n    await expect(hidden).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nCollection of Time List tests set to run with browser clock manipulate made possible with the\npage.clock() API.\n*/\n\nimport fs from 'fs';\n\nimport {\n  createDomainObjectWithDefaults,\n  createPlanFromJSON,\n  navigateToObjectWithRealTime\n} from '../../../appActions.js';\nimport {\n  createTimelistWithPlanAndSetActivityInProgress,\n  getEarliestStartTime,\n  getFirstActivity\n} from '../../../helper/planningUtils';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst examplePlanSmall3 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)\n  )\n);\n\nconst examplePlanSmall1 = JSON.parse(\n  fs.readFileSync(\n    new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)\n  )\n);\n\nconst TIME_TO_FROM_COLUMN = 'countdown';\n\n/**\n * The regular expression used to parse the countdown string.\n * Some examples of valid Countdown strings:\n * ```\n * '35D 02:03:04'\n * '-1D 01:02:03'\n * '01:02:03'\n * '-05:06:07'\n * ```\n */\nconst COUNTDOWN_REGEXP = /(-)?(\\d+D\\s)?(\\d{2}):(\\d{2}):(\\d{2})/;\n\n/**\n * @typedef {Object} CountdownOrUpObject\n * @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).\n * @property {string} days - The number of days in the countdown (undefined if there are no days).\n * @property {string} hours - The number of hours in the countdown.\n * @property {string} minutes - The number of minutes in the countdown.\n * @property {string} seconds - The number of seconds in the countdown.\n * @property {string} toString - The countdown string.\n */\n\n/**\n * Object representing the indices of the capture groups in a countdown regex match.\n *\n * @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}\n * @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).\n * @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).\n * @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).\n * @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).\n * @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).\n */\nconst COUNTDOWN = Object.freeze({\n  SIGN: 1,\n  DAYS: 2,\n  HOURS: 3,\n  MINUTES: 4,\n  SECONDS: 5\n});\n\nconst FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);\n\ntest.describe('Time List with controlled clock @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: getEarliestStartTime(examplePlanSmall3) });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create Time List\n    const timelist = await createDomainObjectWithDefaults(page, {\n      type: 'Time List'\n    });\n\n    // Create a Plan with events that count down and up.\n    // Add it as a child to the Time List.\n    await createPlanFromJSON(page, {\n      json: examplePlanSmall3,\n      parent: timelist.uuid\n    });\n\n    // Navigate to the Time List in real-time mode\n    await navigateToObjectWithRealTime(page, timelist.url, 900000, 1800000);\n\n    //Expand the viewport to show the entire time list\n    await page.getByLabel('Collapse Inspect Pane').click();\n    await page.getByLabel('Collapse Browse Pane').click();\n  });\n  test('Time List shows current events and counts down correctly in real-time mode', async ({\n    page\n  }) => {\n    const countUpRowsNames = ['Time since the last time I ate', 'Time since last accident'];\n    const countdownRowsNames = ['Time until birthday', 'Time until supper'];\n\n    // Verify that the countdown cells are counting down\n    for (let i = 0; i < countdownRowsNames.length; i++) {\n      await test.step(`Countdown cell ${i + 1} counts down`, async () => {\n        const countdownCell = getTimeListCellByName(\n          page,\n          countdownRowsNames[i],\n          TIME_TO_FROM_COLUMN\n        );\n        // Get the initial countdown timestamp object\n        const beforeCountdown = await getAndAssertCountdownOrUpObject(page, countdownRowsNames[i]);\n        // should have a '-' sign BECAUSE IT'S A COUNTDOWN\n        await expect(countdownCell).toContainText('-');\n        // Wait until it changes\n        await expect(countdownCell).not.toContainText(beforeCountdown.toString());\n        // Get the new countdown timestamp object\n        const afterCountdown = await getAndAssertCountdownOrUpObject(page, countdownRowsNames[i]);\n        // Verify that the new countdown timestamp object is less than the old one\n        expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));\n      });\n    }\n\n    // Verify that the count-up cells are counting up\n    for (let i = 0; i < countUpRowsNames.length; i++) {\n      await test.step(`Count-up cell ${i + 1} counts up`, async () => {\n        const countUpCell = getTimeListCellByName(page, countUpRowsNames[i], TIME_TO_FROM_COLUMN);\n        // Get the initial count-up timestamp object\n        const beforeCountUp = await getAndAssertCountdownOrUpObject(page, countUpRowsNames[i]);\n        // should have a '+' sign BECAUSE IT'S A COUNTUP\n        await expect(countUpCell).toContainText('+');\n        // Wait until it changes\n        await expect(countUpCell).not.toContainText(beforeCountUp.toString());\n        // Get the new count-up timestamp object\n        const afterCountUp = await getAndAssertCountdownOrUpObject(page, countUpRowsNames[i]);\n        // Verify that the new count-up timestamp object is greater than the old one\n        expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));\n      });\n    }\n  });\n});\n\ntest.describe('Activity progress when activity is in the future @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start - 1 });\n    await page.clock.resume();\n    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);\n  });\n});\n\ntest.describe('Activity progress when now is between start and end of the activity @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start + 50000 });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);\n  });\n});\n\ntest.describe('Activity progress when now is after end of the activity @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);\n  });\n});\n\n/**\n * Get the cell at the given row and column indices.\n * @param {import('@playwright/test').Page} page\n * @param {number} rowIndex\n * @param {number} columnIndex\n * @returns {import('@playwright/test').Locator} cell\n */\nfunction getTimeListCellByName(page, rowName, columnName) {\n  const rowLocator = page.getByRole('row', { name: rowName });\n  const cellLocator = rowLocator.locator(`td.--${columnName}`);\n\n  return cellLocator;\n}\n\n/**\n * Return the innerText of the cell at the given row and column indices.\n * @param {import('@playwright/test').Page} page\n * @param {number} rowIndex\n * @param {number} columnIndex\n * @returns {Promise<string>} text\n */\nasync function getTimeListCellTextByName(page, rowName, columnName) {\n  const text = await getTimeListCellByName(page, rowName, columnName).innerText();\n  return text;\n}\n\n/**\n * Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup\n * regex, and return an object representing the countdown.\n * @param {import('@playwright/test').Page} page\n * @param {number} rowIndex the row index\n * @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object\n */\nasync function getAndAssertCountdownOrUpObject(page, rowName) {\n  const timeToFrom = await getTimeListCellTextByName(page, rowName, TIME_TO_FROM_COLUMN);\n\n  expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);\n  const match = timeToFrom.match(COUNTDOWN_REGEXP);\n\n  return {\n    sign: match[COUNTDOWN.SIGN],\n    days: match[COUNTDOWN.DAYS],\n    hours: match[COUNTDOWN.HOURS],\n    minutes: match[COUNTDOWN.MINUTES],\n    seconds: match[COUNTDOWN.SECONDS],\n    toString: () => timeToFrom\n  };\n}\n"
  },
  {
    "path": "e2e/tests/functional/planning/timestrip.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  createPlanFromJSON,\n  navigateToObjectWithFixedTimeBounds,\n  setFixedIndependentTimeConductorBounds,\n  setFixedTimeMode,\n  setTimeConductorBounds\n} from '../../../appActions.js';\nimport { expect, test } from '../../../pluginFixtures.js';\n\nconst testPlan = {\n  TEST_GROUP: [\n    {\n      name: 'Past event 1',\n      start: 1660320408000,\n      end: 1660343797000,\n      type: 'TEST-GROUP',\n      color: 'orange',\n      textColor: 'white'\n    },\n    {\n      name: 'Past event 2',\n      start: 1660406808000,\n      end: 1660429160000,\n      type: 'TEST-GROUP',\n      color: 'orange',\n      textColor: 'white'\n    },\n    {\n      name: 'Past event 3',\n      start: 1660493208000,\n      end: 1660503981000,\n      type: 'TEST-GROUP',\n      color: 'orange',\n      textColor: 'white'\n    },\n    {\n      name: 'Past event 4',\n      start: 1660579608000,\n      end: 1660624108000,\n      type: 'TEST-GROUP',\n      color: 'orange',\n      textColor: 'white'\n    },\n    {\n      name: 'Past event 5',\n      start: 1660666008000,\n      end: 1660681529000,\n      type: 'TEST-GROUP',\n      color: 'orange',\n      textColor: 'white'\n    }\n  ]\n};\n\ntest.describe('Time Strip', () => {\n  let timestrip;\n  let plan;\n\n  test.beforeEach(async ({ page }) => {\n    // Goto baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    timestrip = await test.step('Create a Time Strip', async () => {\n      const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });\n      const objectName = await page.locator('.l-browse-bar__object-name').innerText();\n      expect(objectName).toBe(createdTimeStrip.name);\n\n      return createdTimeStrip;\n    });\n\n    plan = await test.step('Create a Plan and add it to the timestrip', async () => {\n      const createdPlan = await createPlanFromJSON(page, {\n        name: 'Test Plan',\n        json: testPlan\n      });\n\n      await page.goto(timestrip.url);\n      // Expand the tree to show the plan\n      await page.getByLabel('Show selected item in tree').click();\n      await page\n        .getByLabel(`Navigate to ${createdPlan.name}`)\n        .dragTo(page.getByLabel('Object View'));\n      await page.getByLabel('Save').click();\n      await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n      return createdPlan;\n    });\n  });\n  test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5627'\n    });\n\n    // Constant locators\n    const activityBounds = page.locator('.activity-bounds');\n\n    await test.step('Set time strip to fixed timespan mode and verify activities', async () => {\n      const startBound = testPlan.TEST_GROUP[0].start;\n      const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;\n\n      // Switch to fixed time mode with all plan events within the bounds\n      await navigateToObjectWithFixedTimeBounds(page, timestrip.url, startBound, endBound);\n\n      // Verify all events are displayed\n      const eventCount = await page.locator('.activity-bounds').count();\n      expect(eventCount).toEqual(testPlan.TEST_GROUP.length);\n    });\n\n    await test.step('TimeStrip can use the Independent Time Conductor', async () => {\n      expect(await activityBounds.count()).toEqual(5);\n\n      // Set the independent time bounds so that only one event is shown\n      const startBound = testPlan.TEST_GROUP[0].start;\n      const endBound = testPlan.TEST_GROUP[0].end;\n      const startBoundString = new Date(startBound).toISOString().replace('T', ' ');\n      const endBoundString = new Date(endBound).toISOString().replace('T', ' ');\n\n      await setFixedIndependentTimeConductorBounds(page, {\n        start: startBoundString,\n        end: endBoundString\n      });\n      expect(await activityBounds.count()).toEqual(1);\n    });\n\n    await test.step('Can have multiple TimeStrips with the same plan linked and different Independent Time Contexts', async () => {\n      // Create another Time Strip and verify that it has been created\n      const createdTimeStrip = await createDomainObjectWithDefaults(page, {\n        type: 'Time Strip',\n        name: 'Another Time Strip'\n      });\n\n      const objectName = await page.locator('.l-browse-bar__object-name').innerText();\n      expect(objectName).toBe(createdTimeStrip.name);\n\n      // Drag the existing Plan onto the newly created Time Strip, and save.\n      await page.getByLabel(`Navigate to ${plan.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel('Save').click();\n      await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n      // All events should be displayed at this point because the\n      // initial independent context bounds will match the global bounds\n      expect(await activityBounds.count()).toEqual(5);\n\n      // Set the independent time bounds so that two events are shown\n      const startBound = testPlan.TEST_GROUP[0].start;\n      const endBound = testPlan.TEST_GROUP[1].end;\n      const startBoundString = new Date(startBound).toISOString().replace('T', ' ');\n      const endBoundString = new Date(endBound).toISOString().replace('T', ' ');\n\n      await setFixedIndependentTimeConductorBounds(page, {\n        start: startBoundString,\n        end: endBoundString\n      });\n\n      // Verify that two events are displayed\n      expect(await activityBounds.count()).toEqual(2);\n\n      // Switch to the previous Time Strip and verify that only one event is displayed\n      await page.goto(timestrip.url);\n      expect(await activityBounds.count()).toEqual(1);\n    });\n  });\n\n  test('Time strip now line', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7817'\n    });\n\n    await test.step('Is displayed in realtime mode', async () => {\n      await expect(page.getByLabel('Now Marker')).toBeVisible();\n    });\n\n    await test.step('Is hidden when out of bounds of the time axis', async () => {\n      // Switch to fixed timespan mode\n      await setFixedTimeMode(page);\n      // Get the end bounds\n      const endBounds = await page.getByLabel('End bounds').textContent();\n\n      // Add 2 minutes to end bound datetime and use it as the new end time\n      let endTimeStamp = new Date(endBounds);\n      endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);\n      const endDate = endTimeStamp.toISOString().split('T')[0];\n      const milliseconds = endTimeStamp.getMilliseconds();\n      const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');\n\n      // Subtract 1 minute from the end bound and use it as the new start time\n      let startTimeStamp = new Date(endBounds);\n      startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);\n      const startDate = startTimeStamp.toISOString().split('T')[0];\n      const startMilliseconds = startTimeStamp.getMilliseconds();\n      const startTime = startTimeStamp\n        .toISOString()\n        .split('T')[1]\n        .replace(`.${startMilliseconds}Z`, '');\n      // Set fixed timespan mode to the future so that \"now\" is out of bounds.\n      await setTimeConductorBounds(page, {\n        startDate,\n        endDate,\n        startTime,\n        endTime\n      });\n\n      await expect(page.getByLabel('Now Marker')).toBeHidden();\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/clocks/clock.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding Clock.\n*/\n\nimport { expect, test } from '../../../../baseFixtures.js';\n\ntest.describe('Clock Generator CRUD Operations', () => {\n  test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/4878'\n    });\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click Clock\n    await page.getByRole('menuitem').first().click();\n\n    // Click .icon-arrow-down\n    await page.locator('.icon-arrow-down').click();\n    //verify if the autocomplete dropdown is visible\n    await expect(page.locator('.c-input--autocomplete__options')).toBeVisible();\n    // Click .icon-arrow-down\n    await page.locator('.icon-arrow-down').click();\n\n    // Verify clicking on the autocomplete arrow collapses the dropdown\n    await expect(page.locator('.c-input--autocomplete__options')).toBeHidden();\n\n    // Click timezone input to open dropdown\n    await page.locator('.c-input--autocomplete__input').click();\n    //verify if the autocomplete dropdown is visible\n    await expect(page.locator('.c-input--autocomplete__options')).toBeVisible();\n\n    // Verify clicking outside the autocomplete dropdown collapses it\n    await page.locator('text=Timezone').click();\n    // Verify clicking on the autocomplete arrow collapses the dropdown\n    await expect(page.locator('.c-input--autocomplete__options')).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// FIXME: Remove this eslint exception once tests are implemented\n// eslint-disable-next-line no-unused-vars\nimport { expect, test } from '../../../../baseFixtures.js';\n\ntest.describe('Remote Clock', () => {\n  // eslint-disable-next-line require-await\n  test.fixme('blocks historical requests until first tick is received', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5221'\n    });\n    // addInitScript to with remote clock\n    // Switch time conductor mode to 'remote clock'\n    // Navigate to telemetry\n    // Verify that the plot renders historical data within the correct bounds\n    // Refresh the page\n    // Verify again that the plot renders historical data within the correct bounds\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/comps/comps.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Comps', () => {\n  test.use({ failOnConsoleError: false });\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(\n        new URL('../../../../helper/addInitDerivedTelemetryPlugin.js', import.meta.url)\n      )\n    });\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Basic Functionality Works', async ({ page, openmctConfig }) => {\n    const folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n\n    // Create the comps with defaults\n    const comp = await createDomainObjectWithDefaults(page, {\n      type: 'Derived Telemetry',\n      parent: folder.uuid\n    });\n\n    const telemetryObject = await createExampleTelemetryObject(page, comp.uuid);\n\n    // Check that expressions can be edited\n    await page.goto(comp.url);\n    await page.getByLabel('Edit Object').click();\n    await page.getByPlaceholder('Enter an expression').fill('a*2');\n    await page.getByText('Current Output').click();\n    await expect(page.getByText('Expression valid')).toBeVisible();\n\n    // Check that expressions are marked invalid\n    await page.getByLabel('Reference Name Input for a').fill('b');\n    await page.getByText('Current Output').click();\n    await expect(page.getByText('Invalid: Undefined symbol a')).toBeVisible();\n\n    // Check that test data works\n    await page.getByPlaceholder('Enter an expression').fill('b*2');\n    await page.getByLabel('Reference Test Value for b').fill('5');\n    await page.getByLabel('Apply Test Data').click();\n    let testValue = await page.getByLabel('Current Output Value').textContent();\n    expect(testValue).toBe('10');\n\n    // Check that real data works\n    await page.getByLabel('Apply Test Data').click();\n    await setRealTimeMode(page);\n    testValue = await page.getByLabel('Current Output Value').textContent();\n    expect(testValue).not.toBe('10');\n    // should be a number\n    expect(parseFloat(testValue)).not.toBeNaN();\n\n    // Check that object path is correct\n    const { myItemsFolderName } = openmctConfig;\n    let objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();\n    const expectedObjectPath = `/${myItemsFolderName}/${folder.name}/${comp.name}/${telemetryObject.name}`;\n    expect(objectPath).toBe(expectedObjectPath);\n\n    // Check that the comps are saved\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    const expression = await page.getByLabel('Expression', { exact: true }).textContent();\n    expect(expression).toBe('b*2');\n\n    // Check that object path is still correct after save\n    objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();\n    expect(objectPath).toBe(expectedObjectPath);\n\n    // Check that comps work after being saved\n    testValue = await page.getByLabel('Current Output Value').textContent();\n    expect(testValue).not.toBe('10');\n    // should be a number\n    expect(parseFloat(testValue)).not.toBeNaN();\n\n    // Check that output format can be changed\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Output Format').click();\n    await page.getByLabel('Output Format').fill('%d');\n    await page.getByRole('tab', { name: 'Config' }).click();\n    // Ensure we only have one digit\n    await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);\n    // And that it persists post save\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this\nsuite is sharing state between tests which is considered an anti-pattern. Implementing in this way to\ndemonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.\n*/\n\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nlet conditionSetUrl;\n\ntest.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {\n  test.beforeAll(async ({ browser }) => {\n    //TODO: This needs to be refactored\n    const context = await browser.newContext();\n    const page = await context.newPage();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    const conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Unnamed Condition Set'\n    });\n\n    //Save localStorage for future test execution\n    await context.storageState({\n      path: fileURLToPath(\n        new URL('../../../../test-data/recycled_local_storage.json', import.meta.url)\n      )\n    });\n\n    //Set object identifier from url\n    conditionSetUrl = conditionSet.url;\n\n    await page.close();\n  });\n\n  //Load localStorage for subsequent tests\n  test.use({\n    storageState: fileURLToPath(\n      new URL('../../../../test-data/recycled_local_storage.json', import.meta.url)\n    )\n  });\n\n  //Begin suite of tests again localStorage\n  test('Condition set object properties persist in main view and inspector after reload @localStorage', async ({\n    page\n  }) => {\n    //Navigate to baseURL with injected localStorage\n    await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });\n\n    //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()\n    await expect.soft(page.getByRole('main')).toContainText('Unnamed Condition Set');\n\n    //Assertions on loaded Condition Set in Inspector\n    await expect(\n      page.getByLabel('Title inspector properties').getByLabel('inspector property value')\n    ).toContainText('Unnamed Condition Set');\n\n    //Reload Page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    //Re-verify after reload\n    await expect(page.getByRole('main')).toContainText('Unnamed Condition Set');\n\n    //Assertions on loaded Condition Set in Inspector\n    await expect(\n      page.getByLabel('Title inspector properties').getByLabel('inspector property value')\n    ).toContainText('Unnamed Condition Set');\n  });\n\n  test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {\n    const { myItemsFolderName } = openmctConfig;\n\n    await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });\n\n    //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');\n\n    //Update the Condition Set properties\n    // Click Edit Button\n    await page.locator('text=Conditions View Snapshot >> button').nth(3).click();\n\n    //Edit Condition Set Name from main view\n    await page\n      .locator('.l-browse-bar__object-name')\n      .filter({ hasText: 'Unnamed Condition Set' })\n      .first()\n      .fill('Renamed Condition Set');\n    await page\n      .locator('.l-browse-bar__object-name')\n      .filter({ hasText: 'Renamed Condition Set' })\n      .first()\n      .press('Enter');\n    // Click Save Button\n    await page\n      .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')\n      .nth(1)\n      .click();\n    // Click Save and Finish Editing Option\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    //Verify Main section reflects updated Name Property\n    await expect\n      .soft(page.locator('.l-browse-bar__object-name'))\n      .toContainText('Renamed Condition Set');\n\n    // Verify Inspector properties\n    // Verify Inspector has updated Name property\n    expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();\n    // Verify Inspector Details has updated Name property\n    expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();\n\n    // Verify Tree reflects updated Name property\n    // Expand Tree\n    await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();\n    // Verify Condition Set Object is renamed in Tree\n    expect(page.locator('a:has-text(\"Renamed Condition Set\")')).toBeTruthy();\n    // Verify Search Tree reflects renamed Name property\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').fill('Renamed');\n    expect(page.locator('a:has-text(\"Renamed Condition Set\")')).toBeTruthy();\n\n    //Reload Page\n    await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]);\n\n    //Verify Main section reflects updated Name Property\n    await expect\n      .soft(page.locator('.l-browse-bar__object-name'))\n      .toContainText('Renamed Condition Set');\n\n    // Verify Inspector properties\n    // Verify Inspector has updated Name property\n    expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();\n    // Verify Inspector Details has updated Name property\n    expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();\n\n    // Verify Tree reflects updated Name property\n    // Expand Tree\n    await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();\n    // Verify Condition Set Object is renamed in Tree\n    expect(page.locator('a:has-text(\"Renamed Condition Set\")')).toBeTruthy();\n    // Verify Search Tree reflects renamed Name property\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').fill('Renamed');\n    expect(page.locator('a:has-text(\"Renamed Condition Set\")')).toBeTruthy();\n  });\n  test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({\n    page\n  }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()\n    await expect(\n      page.locator('a:has-text(\"Unnamed Condition Set Condition Set\") >> nth=0')\n    ).toBeVisible();\n\n    const numberOfConditionSetsToStart = await page\n      .locator('a:has-text(\"Unnamed Condition Set Condition Set\")')\n      .count();\n\n    // Search for Unnamed Condition Set\n    await page\n      .locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]')\n      .fill('Unnamed Condition Set');\n    // Click Search Result\n    await page\n      .locator('[aria-label=\"OpenMCT Search\"] >> text=Unnamed Condition Set')\n      .first()\n      .click();\n    // Click hamburger button\n    await page.locator('[title=\"More actions\"]').click();\n\n    // Click 'Remove' and press OK\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    //Expect Unnamed Condition Set to be removed in Main View\n    const numberOfConditionSetsAtEnd = await page\n      .locator('a:has-text(\"Unnamed Condition Set Condition Set\")')\n      .count();\n\n    expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);\n\n    //Feature?\n    //Domain Object is still available by direct URL after delete\n    await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');\n  });\n});\n\ntest.describe('Basic Condition Set Use', () => {\n  let conditionSet;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all network events to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create a new condition set\n    conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Test Condition Set'\n    });\n  });\n  test('Creating a condition defaults the condition name to \"Unnamed Condition\"', async ({\n    page\n  }) => {\n    await page.goto(conditionSet.url);\n\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click Add Condition button\n    await page.locator('#addCondition').click();\n    // Check that the new Unnamed Condition section appears\n    const numOfUnnamedConditions = await page\n      .locator('.c-condition__name', { hasText: 'Unnamed Condition' })\n      .count();\n    expect(numOfUnnamedConditions).toEqual(1);\n  });\n  test('ConditionSet should display appropriate view options', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5924'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave Generator'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave Generator'\n    });\n\n    await page.goto(conditionSet.url);\n\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.getByLabel('Show selected item in tree').click();\n    // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const alphaGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: 'Alpha Sine Wave Generator'\n    });\n    const betaGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: 'Beta Sine Wave Generator'\n    });\n    const conditionCollection = page.locator('#conditionCollection');\n\n    await alphaGeneratorTreeItem.dragTo(conditionCollection);\n    await betaGeneratorTreeItem.dragTo(conditionCollection);\n\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.getByLabel('Open the View Switcher Menu').click();\n\n    await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();\n    await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();\n    await page.getByLabel('Plot').click();\n    await expect(\n      page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')\n    ).toBeVisible();\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Telemetry Table').click();\n    await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Conditions View').click();\n    await expect(page.getByText('Current Output')).toBeVisible();\n  });\n  test('ConditionSet has correct outputs when telemetry is and is not available', async ({\n    page\n  }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n    await page.goto(conditionSet.url);\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Create two conditions\n    await page.locator('#addCondition').click();\n    await page.locator('#addCondition').click();\n    await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');\n    await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');\n\n    // Add Telemetry to ConditionSet\n    const sineWaveGeneratorTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: exampleTelemetry.name\n      });\n    const conditionCollection = page.locator('#conditionCollection');\n    await sineWaveGeneratorTreeItem.dragTo(conditionCollection);\n\n    // Modify First Criterion\n    const firstCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=0'\n    );\n    firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n    const firstCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=0'\n    );\n    firstCriterionMetadata.selectOption({ label: 'Sine' });\n    const firstCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=0'\n    );\n    firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });\n    const firstCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=0');\n    await firstCriterionInput.fill('0');\n\n    // Modify First Criterion\n    const secondCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=1'\n    );\n    secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const secondCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=1'\n    );\n    secondCriterionMetadata.selectOption({ label: 'Sine' });\n\n    const secondCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=1'\n    );\n    secondCriterionComparison.selectOption({ label: 'is less than' });\n\n    const secondCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=1');\n    await secondCriterionInput.fill('0');\n\n    // Save ConditionSet\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Validate that the condition set is evaluating and outputting\n    // the correct value when the underlying telemetry subscription is active.\n    let outputValue = page.getByLabel('Current Output Value');\n    await expect(outputValue).toHaveText('false');\n\n    await page.goto(exampleTelemetry.url);\n\n    // Edit SWG to add 8 second loading delay to simulate the case\n    // where telemetry is not available.\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');\n    await page.getByLabel('Save').click();\n\n    // Expect that the output value is blank or '---' if the\n    // underlying telemetry subscription is not active.\n    await page.goto(conditionSet.url);\n    await expect(outputValue).toHaveText('---');\n  });\n\n  test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n    await page.goto(conditionSet.url);\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Create two conditions\n    await page.locator('#addCondition').click();\n    await page.locator('#addCondition').click();\n    await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');\n    await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');\n\n    // Add Telemetry to ConditionSet\n    const sineWaveGeneratorTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: exampleTelemetry.name\n      });\n    const conditionCollection = page.locator('#conditionCollection');\n    await sineWaveGeneratorTreeItem.dragTo(conditionCollection);\n\n    // Modify First Criterion\n    const firstCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=0'\n    );\n    firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n    const firstCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=0'\n    );\n    firstCriterionMetadata.selectOption({ label: 'Sine' });\n    const firstCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=0'\n    );\n    firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });\n    const firstCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=0');\n    await firstCriterionInput.fill('0');\n\n    // Modify Second Criterion\n    const secondCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=1'\n    );\n    await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const secondCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=1'\n    );\n    await secondCriterionMetadata.selectOption({ label: 'Sine' });\n\n    const secondCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=1'\n    );\n    await secondCriterionComparison.selectOption({ label: 'is less than' });\n\n    const secondCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=1');\n    await secondCriterionInput.fill('0');\n\n    // Enable test data\n    await page.getByLabel('Apply Test Data').nth(1).click();\n    const testDataTelemetry = page.locator('[aria-label=\"Test Data Telemetry Selection\"] >> nth=0');\n    await testDataTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const testDataMetadata = page.locator('[aria-label=\"Test Data Metadata Selection\"] >> nth=0');\n    await testDataMetadata.selectOption({ label: 'Sine' });\n\n    const testInput = page.locator('[aria-label=\"Test Data Input\"] >> nth=0');\n    await testInput.fill('0');\n\n    // Validate that the condition set is evaluating and outputting\n    // the correct value when the underlying telemetry subscription is active.\n    let outputValue = page.getByLabel('Current Output Value');\n    await expect(outputValue).toHaveText('false');\n\n    await page.goto(exampleTelemetry.url);\n  });\n\n  test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7484'\n    });\n  });\n});\n\ntest.describe('Condition Set Composition', () => {\n  let conditionSet;\n  let exampleTelemetry;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Condition Set\n    conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set'\n    });\n\n    // Create Telemetry Object as child to Condition Set\n    exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);\n\n    // Edit Condition Set\n    await page.goto(conditionSet.url);\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Add Condition to Condition Set\n    await page.getByRole('button', { name: 'Add Condition' }).click();\n\n    // Enter Condition Output\n    await page.getByLabel('Condition Name Input').first().fill('Negative');\n    await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });\n    await page.getByLabel('Condition Output String').first().fill('Negative');\n\n    // Condition Trigger default is okay so no change needed to form\n\n    // Enter Condition Criterion\n    await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });\n    await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });\n    await page\n      .locator('select[aria-label=\"Criterion Comparison Selection\"]')\n      .first()\n      .selectOption({ value: 'lessThan' });\n    await page.getByLabel('Criterion Input').first().fill('0');\n\n    // Save the Condition Set\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n  });\n\n  test('You can remove telemetry from a condition set with existing conditions', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7710'\n    });\n\n    await page.getByLabel('Expand My Items folder').click();\n    await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();\n\n    await page\n      .getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })\n      .click({ button: 'right' });\n\n    await page\n      .getByLabel(`${exampleTelemetry.name} Context Menu`)\n      .getByRole('menuitem', { name: 'Remove' })\n      .click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    await page\n      .getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })\n      .click();\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    expect(\n      await page\n        .getByRole('tabpanel', { name: 'Inspector Views' })\n        .getByRole('listitem', { name: exampleTelemetry.name })\n        .count()\n    ).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/conditionSet/conditionSetOperations.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this\nsuite is sharing state between tests which is considered an anti-pattern. Implementing in this way to\ndemonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Basic Condition Set Use', () => {\n  let conditionSet;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all network events to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create a new condition set\n    conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Test Condition Set'\n    });\n  });\n  test('Creating a condition defaults the condition name to \"Unnamed Condition\"', async ({\n    page\n  }) => {\n    await page.goto(conditionSet.url);\n\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click Add Condition button\n    await page.locator('#addCondition').click();\n    // Check that the new Unnamed Condition section appears\n    const numOfUnnamedConditions = await page\n      .locator('.c-condition__name', { hasText: 'Unnamed Condition' })\n      .count();\n    expect(numOfUnnamedConditions).toEqual(1);\n  });\n  test('ConditionSet should display appropriate view options', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5924'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave Generator'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave Generator'\n    });\n\n    await page.goto(conditionSet.url);\n\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.getByLabel('Show selected item in tree').click();\n    // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const alphaGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: 'Alpha Sine Wave Generator'\n    });\n    const betaGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: 'Beta Sine Wave Generator'\n    });\n    const conditionCollection = page.locator('#conditionCollection');\n\n    await alphaGeneratorTreeItem.dragTo(conditionCollection);\n    await betaGeneratorTreeItem.dragTo(conditionCollection);\n\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.getByLabel('Open the View Switcher Menu').click();\n\n    await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();\n    await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();\n    await page.getByLabel('Plot').click();\n    await expect(\n      page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')\n    ).toBeVisible();\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Telemetry Table').click();\n    await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Conditions View').click();\n    await expect(page.getByText('Current Output')).toBeVisible();\n  });\n  test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({\n    page\n  }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n    await page.goto(conditionSet.url);\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Create two conditions\n    await page.locator('#addCondition').click();\n    await page.locator('#addCondition').click();\n    await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');\n    await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');\n\n    // Add Telemetry to ConditionSet\n    const sineWaveGeneratorTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: exampleTelemetry.name\n      });\n    const conditionCollection = page.locator('#conditionCollection');\n    await sineWaveGeneratorTreeItem.dragTo(conditionCollection);\n\n    // Modify First Criterion\n    const firstCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=0'\n    );\n    firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n    const firstCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=0'\n    );\n    firstCriterionMetadata.selectOption({ label: 'Sine' });\n    const firstCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=0'\n    );\n    firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });\n    const firstCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=0');\n    await firstCriterionInput.fill('0');\n\n    // Modify First Criterion\n    const secondCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=1'\n    );\n    secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const secondCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=1'\n    );\n    secondCriterionMetadata.selectOption({ label: 'Sine' });\n\n    const secondCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=1'\n    );\n    secondCriterionComparison.selectOption({ label: 'is less than' });\n\n    const secondCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=1');\n    await secondCriterionInput.fill('0');\n\n    // Save ConditionSet\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Validate that the condition set is evaluating and outputting\n    // the correct value when the underlying telemetry subscription is active.\n    let outputValue = page.getByLabel('Current Output Value');\n    await expect(outputValue).toHaveText('false');\n\n    await page.goto(exampleTelemetry.url);\n\n    // Edit SWG to add 8 second loading delay to simulate the case\n    // where telemetry is not available.\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');\n    await page.getByLabel('Save').click();\n\n    // Expect that the output value is blank or '---' if the\n    // underlying telemetry subscription is not active.\n    await page.goto(conditionSet.url);\n    await expect(outputValue).toHaveText('---');\n  });\n\n  test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n    await page.goto(conditionSet.url);\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Create two conditions\n    await page.locator('#addCondition').click();\n    await page.locator('#addCondition').click();\n    await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');\n    await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');\n\n    // Add Telemetry to ConditionSet\n    const sineWaveGeneratorTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: exampleTelemetry.name\n      });\n    const conditionCollection = page.locator('#conditionCollection');\n    await sineWaveGeneratorTreeItem.dragTo(conditionCollection);\n\n    // Modify First Criterion\n    const firstCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=0'\n    );\n    firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n    const firstCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=0'\n    );\n    firstCriterionMetadata.selectOption({ label: 'Sine' });\n    const firstCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=0'\n    );\n    firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });\n    const firstCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=0');\n    await firstCriterionInput.fill('0');\n\n    // Modify Second Criterion\n    const secondCriterionTelemetry = page.locator(\n      '[aria-label=\"Criterion Telemetry Selection\"] >> nth=1'\n    );\n    await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const secondCriterionMetadata = page.locator(\n      '[aria-label=\"Criterion Metadata Selection\"] >> nth=1'\n    );\n    await secondCriterionMetadata.selectOption({ label: 'Sine' });\n\n    const secondCriterionComparison = page.locator(\n      '[aria-label=\"Criterion Comparison Selection\"] >> nth=1'\n    );\n    await secondCriterionComparison.selectOption({ label: 'is less than' });\n\n    const secondCriterionInput = page.locator('[aria-label=\"Criterion Input\"] >> nth=1');\n    await secondCriterionInput.fill('0');\n\n    // Enable test data\n    await page.getByLabel('Apply Test Data').nth(1).click();\n    const testDataTelemetry = page.locator('[aria-label=\"Test Data Telemetry Selection\"] >> nth=0');\n    await testDataTelemetry.selectOption({ label: exampleTelemetry.name });\n\n    const testDataMetadata = page.locator('[aria-label=\"Test Data Metadata Selection\"] >> nth=0');\n    await testDataMetadata.selectOption({ label: 'Sine' });\n\n    const testInput = page.locator('[aria-label=\"Test Data Input\"] >> nth=0');\n    await testInput.fill('0');\n\n    // Validate that the condition set is evaluating and outputting\n    // the correct value when the underlying telemetry subscription is active.\n    let outputValue = page.getByLabel('Current Output Value');\n    await expect(outputValue).toHaveText('false');\n\n    await page.goto(exampleTelemetry.url);\n  });\n\n  test('Short circuit evaluation does not cause incorrect evaluation https://github.com/nasa/openmct/issues/7992', async ({\n    page\n  }) => {\n    await setRealTimeMode(page);\n    await page.getByLabel('Create', { exact: true }).click();\n    await page.getByLabel('State Generator').click();\n    await page.getByLabel('Title', { exact: true }).fill('P1');\n    await page.getByLabel('State Duration (seconds)').fill('1');\n    await page.getByLabel('Save').click();\n    await page.getByLabel('Create', { exact: true }).click();\n    await page.getByLabel('State Generator').click();\n    await page.getByLabel('Title', { exact: true }).fill('P2');\n    await page.getByLabel('State Duration (seconds)', { exact: true }).fill('1');\n    await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();\n    await page.getByLabel('Save').click();\n    await page.getByLabel('Expand My Items folder').click();\n    await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Add Condition').click();\n    await page.getByLabel('Condition Name Input').first().fill('P1 IS ON AND P2 IS ON');\n    await page.getByLabel('Criterion Telemetry Selection').selectOption({ label: 'P1' });\n    await page.getByLabel('Criterion Metadata Selection').selectOption('value');\n    await page.getByLabel('Criterion Comparison Selection').selectOption('equalTo');\n    await page.getByLabel('Criterion Input').fill('1');\n    await page.getByLabel('Add Criteria - Enabled').click();\n    await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });\n    await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');\n    await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');\n    await page.getByLabel('Criterion Input').nth(1).fill('1');\n    await page.getByLabel('Add Condition').click();\n    await page.getByLabel('Condition Name Input').first().fill('P1 IS OFF OR P2 IS OFF');\n    await page.getByLabel('Condition Trigger').first().selectOption('any');\n    await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ label: 'P1' });\n    await page.getByLabel('Criterion Metadata Selection').first().selectOption('value');\n    await page.getByLabel('Criterion Comparison Selection').first().selectOption('equalTo');\n    await page.getByLabel('Criterion Input').first().fill('0');\n    await page.getByLabel('Add Criteria - Enabled').first().click();\n    await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });\n    await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');\n    await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');\n    await page.getByLabel('Criterion Input').nth(1).fill('0');\n    await page.getByLabel('Condition Name Input').first().dblclick();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await page.getByLabel('Edit Object').click();\n\n    /**\n     * Create default conditions for test. Start with invalid values to put condition set into\n     * \"default\" state\n     */\n    await page.getByLabel('Test Data Telemetry Selection').selectOption({ label: 'P1' });\n    await page.getByLabel('Test Data Metadata Selection').selectOption({ label: 'Value' });\n    await page.getByLabel('Test Data Input').fill('3');\n    await page.getByLabel('Add Test Datum').click();\n    await page.getByLabel('Test Data Telemetry Selection').nth(1).selectOption({ label: 'P2' });\n    await page.getByLabel('Test Data Metadata Selection').nth(1).selectOption({ label: 'Value' });\n    await page.getByLabel('Test Data Input').nth(1).fill('3');\n    await page.getByLabel('Apply Test Data').nth(1).click();\n\n    let activeCondition = page.getByLabel('Active Condition Set Condition');\n    let activeConditionName = activeCondition.getByLabel('Condition Name Label');\n\n    await expect(activeConditionName).toHaveText('Default');\n\n    /**\n     * Set P1 to 0\n     */\n    await page.getByLabel('Test Data Input').nth(0).fill('0');\n\n    activeCondition = page.getByLabel('Active Condition Set Condition');\n    activeConditionName = activeCondition.getByLabel('Condition Name Label');\n\n    await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');\n\n    /**\n     * Set P2 to 1\n     */\n    await page.getByLabel('Test Data Input').nth(1).fill('1');\n\n    activeCondition = page.getByLabel('Active Condition Set Condition');\n    activeConditionName = activeCondition.getByLabel('Condition Name Label');\n\n    await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');\n\n    /**\n     * Set P1 to 1\n     */\n    await page.getByLabel('Test Data Input').nth(0).fill('1');\n\n    activeCondition = page.getByLabel('Active Condition Set Condition');\n    activeConditionName = activeCondition.getByLabel('Condition Name Label');\n\n    await expect(activeConditionName).toHaveText('P1 IS ON AND P2 IS ON');\n  });\n\n  test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7484'\n    });\n  });\n\n  test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({\n    page\n  }) => {\n    const exampleTelemetry = await createExampleTelemetryObject(page);\n\n    await page.getByLabel('Show selected item in tree').click();\n    await page.goto(conditionSet.url);\n    // Change the object to edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Create a condition\n    await page.locator('#addCondition').click();\n    await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');\n\n    // Validate that the add criteria button is disabled\n    await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');\n\n    // Add Telemetry to ConditionSet\n    const sineWaveGeneratorTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: exampleTelemetry.name\n      });\n    const conditionCollection = page.locator('#conditionCollection');\n    await sineWaveGeneratorTreeItem.dragTo(conditionCollection);\n\n    // Validate that the add criteria button is enabled and adds a new criterion\n    await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');\n    await page.getByLabel('Add Criteria - Enabled').click();\n    const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();\n    expect(numOfUnnamedCriteria).toEqual(2);\n  });\n});\n\ntest.describe('Condition Set Composition', () => {\n  let conditionSet;\n  let exampleTelemetry;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Condition Set\n    conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set'\n    });\n\n    // Create Telemetry Object as child to Condition Set\n    exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);\n\n    // Edit Condition Set\n    await page.goto(conditionSet.url);\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Add Condition to Condition Set\n    await page.getByRole('button', { name: 'Add Condition' }).click();\n\n    // Enter Condition Output\n    await page.getByLabel('Condition Name Input').first().fill('Negative');\n    await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });\n    await page.getByLabel('Condition Output String').first().fill('Negative');\n\n    // Condition Trigger default is okay so no change needed to form\n\n    // Enter Condition Criterion\n    await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });\n    await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });\n    await page\n      .locator('select[aria-label=\"Criterion Comparison Selection\"]')\n      .first()\n      .selectOption({ value: 'lessThan' });\n    await page.getByLabel('Criterion Input').first().fill('0');\n\n    // Save the Condition Set\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n  });\n\n  test('You can remove telemetry from a condition set with existing conditions', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7710'\n    });\n\n    await page.getByLabel('Expand My Items folder').click();\n    await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();\n\n    await page\n      .getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })\n      .click({ button: 'right' });\n\n    await page\n      .getByLabel(`${exampleTelemetry.name} Context Menu`)\n      .getByRole('menuitem', { name: 'Remove' })\n      .click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    await page\n      .getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })\n      .click();\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    expect(\n      await page\n        .getByRole('tabpanel', { name: 'Inspector Views' })\n        .getByRole('listitem', { name: exampleTelemetry.name })\n        .count()\n    ).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/correlationTelemetry/correlationTelemetry.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  getNextSineValueFromSWG,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Correlation Telemetry', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await page.evaluate(() => {\n      const openmct = window.openmct;\n      openmct.install(openmct.plugins.CorrelationTelemetry());\n    });\n  });\n\n  test('will correlate telemetry from two objects based on timestamp', async ({ page }) => {\n    const sineWaveGenerator1 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 1'\n    });\n    const sineWaveGenerator2 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 2'\n    });\n\n    // create correlation telemetry object, with x and y sources\n    await page.getByRole('button', { name: 'Create', exact: true }).click();\n    await page.getByRole('menuitem', { name: 'Correlation Telemetry' }).click();\n    await page.getByLabel('Title', { exact: true }).fill('');\n    await page.getByLabel('Title', { exact: true }).fill('Test Correlation Telemetry');\n\n    // choose sine wave generator 1 as x source\n    const createModalTreeFirst = page.getByLabel('Create Modal Tree').first();\n    await createModalTreeFirst.getByLabel('Expand My Items folder').click();\n    await createModalTreeFirst\n      .getByLabel('Navigate to Sine Wave Generator 1 generator Object')\n      .click();\n\n    // choose sine wave generator 2 as y source\n    const createModalTreeSecond = page.getByLabel('Create Modal Tree').nth(1);\n    await createModalTreeSecond.getByLabel('Expand My Items folder').click();\n    await createModalTreeSecond\n      .getByLabel('Navigate to Sine Wave Generator 2 generator Object')\n      .click();\n\n    // save in my items folder\n    const createModalTreeThird = page.getByLabel('Create Modal Tree').nth(2);\n    await createModalTreeThird.getByLabel('Navigate to My Items folder').click();\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await setRealTimeMode(page);\n\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Telemetry Table').click();\n\n    const getSWG1ValuePromise = getNextSineValueFromSWG(page, sineWaveGenerator1.uuid, false);\n    const getSWG2ValuePromise = getNextSineValueFromSWG(page, sineWaveGenerator2.uuid, false);\n\n    const swg1Value = await getSWG1ValuePromise;\n    const swg2Value = await getSWG2ValuePromise;\n    const correlatedTelemetryObject = {\n      x: swg1Value.sin,\n      y: swg2Value.sin,\n      formattedTimestamp: swg1Value.formattedTimestamp,\n      timestampsMatch: swg1Value.utc === swg2Value.utc\n    };\n\n    // wait for correlated telemetry object formatted timestamp to be visible in the telemetry table\n    await expect(page.getByText(correlatedTelemetryObject.formattedTimestamp)).toBeVisible();\n    await expect(correlatedTelemetryObject.timestampsMatch).toBe(true);\n\n    // check that the x and y values are correlated in the same row, based on column names: x and y, respectively\n    const telemetryTableRows = page.getByRole('row');\n    const correlatedRow = telemetryTableRows.filter((row) =>\n      row.getByText(correlatedTelemetryObject.formattedTimestamp).isVisible()\n    );\n    await expect(\n      correlatedRow.getByLabel(`x table cell ${correlatedTelemetryObject.x}`).first()\n    ).toBeVisible();\n    await expect(\n      correlatedRow.getByLabel(`y table cell ${correlatedTelemetryObject.y}`).first()\n    ).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  getNextSineValueFromSWG,\n  navigateToObjectWithFixedTimeBounds,\n  setFixedIndependentTimeConductorBounds,\n  setFixedTimeMode,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(\n  new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)\n);\nconst TEST_DISPLAY_LAYOUT_ID = {\n  namespace: '',\n  key: '712d07f1-3585-465a-a6db-3c40a9edcde7'\n};\nconst CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(\n  new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)\n);\nconst TINY_IMAGE_BASE64 =\n  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';\n\ntest.describe('Display Layout Sub-object Actions @localStorage', () => {\n  const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';\n  const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';\n  const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';\n  const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';\n\n  test.use({\n    storageState: CHILD_PLOT_STORAGE_STATE_PATH\n  });\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await page.getByLabel('Expand My Items folder').click();\n    const waitForDisplayLayoutNavigation = page.waitForURL(\n      //eslint-disable-next-line\n      new RegExp(`.*/${TEST_DISPLAY_LAYOUT_ID.key}/\\?.*`)\n    );\n    await page\n      .getByLabel('Main Tree')\n      .getByLabel('Navigate to Parent Display Layout layout Object')\n      .click();\n    // Wait for the URL to change to the display layout\n    await waitForDisplayLayoutNavigation;\n  });\n  test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7524'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6982'\n    });\n\n    const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z\n    const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z\n\n    // Verify the ITC has the expected initial bounds\n    await expect(\n      page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds')\n    ).toHaveText(INIT_ITC_START_BOUNDS);\n    await expect(\n      page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds')\n    ).toHaveText(INIT_ITC_END_BOUNDS);\n\n    // Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z\n    const url = page.url().split('?')[0];\n    await navigateToObjectWithFixedTimeBounds(\n      page,\n      url,\n      TEST_FIXED_START_TIME,\n      TEST_FIXED_END_TIME\n    );\n\n    // ITC bounds should still match the initial ITC bounds\n    await expect(\n      page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds')\n    ).toHaveText(INIT_ITC_START_BOUNDS);\n    await expect(\n      page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds')\n    ).toHaveText(INIT_ITC_END_BOUNDS);\n\n    // Open the Child Overlay Plot 1 in a new tab\n    await page.getByLabel('View menu items').click();\n    const pagePromise = page.context().waitForEvent('page');\n    await page.getByLabel('Open In New Tab').click();\n\n    const newPage = await pagePromise;\n    await newPage.waitForLoadState('domcontentloaded');\n\n    // Verify that the global time conductor bounds in the new page match the updated global bounds\n    await expect(newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds')).toHaveText(\n      NEW_GLOBAL_START_BOUNDS\n    );\n    await expect(newPage.getByLabel('Global Time Conductor').getByLabel('End bounds')).toHaveText(\n      NEW_GLOBAL_END_BOUNDS\n    );\n\n    // Verify that the ITC is enabled in the new page\n    await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();\n    // Verify that the ITC bounds in the new page match the original ITC bounds\n    await expect(\n      newPage.getByLabel('Independent Time Conductor Panel').getByLabel('Start bounds')\n    ).toHaveText(INIT_ITC_START_BOUNDS);\n    await expect(\n      newPage.getByLabel('Independent Time Conductor Panel').getByLabel('End bounds')\n    ).toHaveText(INIT_ITC_END_BOUNDS);\n  });\n});\n\ntest.describe('Display Layout Toolbar Actions @localStorage', () => {\n  const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';\n  const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await setRealTimeMode(page);\n    await page\n      .locator('a')\n      .filter({ hasText: 'Parent Display Layout Display Layout' })\n      .first()\n      .click();\n    await page.getByLabel('Edit Object').click();\n  });\n  test.use({\n    storageState: CHILD_LAYOUT_STORAGE_STATE_PATH\n  });\n\n  test('can add/remove Text element to a single layout', async ({ page }) => {\n    const layoutObject = 'Text';\n    await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);\n    });\n    await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);\n    });\n  });\n  test('can add/remove Image to a single layout', async ({ page }) => {\n    const layoutObject = 'Image';\n    await test.step(\"Add and remove image element from the parent's layout\", async () => {\n      await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);\n      await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject);\n      await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1);\n      await removeLayoutObject(page, layoutObject);\n      await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);\n    });\n    await test.step(\"Add and remove image from the child's layout\", async () => {\n      await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject);\n      await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1);\n      await removeLayoutObject(page, layoutObject);\n      await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);\n    });\n  });\n  test(`can add/remove Box to a single layout`, async ({ page }) => {\n    const layoutObject = 'Box';\n    await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);\n    });\n    await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);\n    });\n  });\n  test(`can add/remove Line to a single layout`, async ({ page }) => {\n    const layoutObject = 'Line';\n    await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);\n    });\n    await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);\n    });\n  });\n  test(`can add/remove Ellipse to a single layout`, async ({ page }) => {\n    const layoutObject = 'Ellipse';\n    await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);\n    });\n    await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {\n      await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);\n    });\n  });\n  test.fixme('Can switch view types of a single SWG in a layout', async ({ page }) => {});\n  test.fixme('Can merge multiple plots in a layout', async ({ page }) => {});\n  test.fixme('Can adjust stack order of a single object in a layout', async ({ page }) => {});\n  test.fixme('Can duplicate a single object in a layout', async ({ page }) => {});\n});\n\ntest.describe('Display Layout', () => {\n  /** @type {import('../../../../appActions').CreatedObjectInfo} */\n  let sineWaveObject;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await setRealTimeMode(page);\n\n    // Create Sine Wave Generator\n    sineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n  });\n\n  test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({\n    page\n  }) => {\n    // Create a Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Subscribe to the Sine Wave Generator data\n    // On getting data, check if the value found in the  Display Layout is the most recent value\n    // from the Sine Wave Generator\n    const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);\n    const formattedTelemetryValue = await getTelemValuePromise;\n    await expect(page.getByText(formattedTelemetryValue)).toBeVisible();\n    const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent();\n    const trimmedDisplayValue = displayLayoutValue.trim();\n\n    expect(trimmedDisplayValue).toBe(formattedTelemetryValue);\n\n    // ensure we can right click on the alpha-numeric widget and view historical data\n    await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({\n      button: 'right'\n    });\n    await page.getByLabel('View Historical Data').click();\n    await expect(page.getByLabel('Plot Container Style Target')).toBeVisible();\n  });\n  test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({\n    page\n  }) => {\n    // Create a Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Subscribe to the Sine Wave Generator data\n    const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);\n    // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window\n    await setStartOffset(page, { startMins: '1' });\n    await setFixedTimeMode(page);\n\n    // On getting data, check if the value found in the Display Layout is the most recent value\n    // from the Sine Wave Generator\n    const formattedTelemetryValue = await getTelemValuePromise;\n    await expect(page.getByText(formattedTelemetryValue)).toBeVisible();\n    const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent();\n    const trimmedDisplayValue = displayLayoutValue.trim();\n\n    expect(trimmedDisplayValue).toBe(formattedTelemetryValue);\n  });\n  test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({\n    page\n  }) => {\n    // Create a Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);\n\n    // Expand the Display Layout so we can remove the sine wave generator\n    await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();\n\n    // Bring up context menu and remove\n    await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // delete\n\n    expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);\n  });\n  test('items in a display layout can be removed with object tree context menu when viewing another item', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/3117'\n    });\n    // Create a Display Layout\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);\n\n    // Expand the Display Layout so we can remove the sine wave generator\n    await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();\n\n    // Go to the original Sine Wave Generator to navigate away from the Display Layout\n    await page.goto(sineWaveObject.url);\n\n    // Bring up context menu and remove\n    await sineWaveGeneratorTreeItem.first().click({ button: 'right' });\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // navigate back to the display layout to confirm it has been removed\n    await page.goto(displayLayout.url);\n\n    expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);\n  });\n\n  test('independent time works with display layouts and its children', async ({ page }) => {\n    await setFixedTimeMode(page);\n    // Create Example Imagery\n    const exampleImageryObject = await createDomainObjectWithDefaults(page, {\n      type: 'Example Imagery'\n    });\n    // Create a Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const exampleImageryTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(exampleImageryObject.name)\n    });\n    await exampleImageryTreeItem.dragTo(page.getByLabel('Layout Grid'));\n\n    //adjust so that we can see the independent time conductor toggle\n    // Adjust object height\n    await page.locator('div[title=\"Resize object height\"] > input').click();\n    await page.locator('div[title=\"Resize object height\"] > input').fill('70');\n\n    // Adjust object width\n    await page.locator('div[title=\"Resize object width\"] > input').click();\n    await page.locator('div[title=\"Resize object width\"] > input').fill('70');\n\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    const startDate = '2021-12-30 01:01:00.000Z';\n    const endDate = '2021-12-30 01:11:00.000Z';\n    await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate });\n\n    // check image date\n    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();\n\n    // flip it off\n    await page.getByRole('switch').click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n  });\n\n  test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb @network', async ({\n    page\n  }) => {\n    await setFixedTimeMode(page);\n    // Create another Sine Wave Generator\n    const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.01'\n      }\n    });\n    // Create a Display Layout\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n\n    // eslint-disable-next-line playwright/no-force-option\n    await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true });\n\n    await page.getByText('View type').click();\n    await page.getByText('Overlay Plot').click();\n\n    const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(anotherSineWaveObject.name)\n    });\n    // eslint-disable-next-line playwright/no-force-option\n    await anotherSineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true });\n\n    await page.getByText('View type').click();\n    await page.getByText('Overlay Plot').click();\n\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Time to inspect some network traffic\n    let networkRequests = [];\n    page.on('request', (request) => {\n      const searchRequest =\n        request.url().endsWith('_find') || request.url().includes('by_keystring');\n      const fetchRequest = request.resourceType() === 'fetch';\n      if (searchRequest && fetchRequest) {\n        networkRequests.push(request);\n      }\n    });\n\n    await page.reload();\n    await expect(page.getByLabel('Browse bar object name')).toHaveText(displayLayout.name);\n    // Network requests for the composite telemetry with multiple items should be:\n    // 1.  a single batched request for annotations\n    await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(1);\n  });\n\n  test('Same objects with different request options have unique subscriptions', async ({\n    page\n  }) => {\n    // Expand My Items\n    await page.getByLabel('Expand My Items folder').click();\n\n    // Create a Display Layout\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display'\n    });\n\n    // Create a State Generator, set to higher frequency updates\n    const stateGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'State Generator',\n      name: 'State Generator'\n    });\n    const stateGeneratorTreeItem = page.getByRole('treeitem', {\n      name: stateGenerator.name\n    });\n    await stateGeneratorTreeItem.click({ button: 'right' });\n    await page.getByLabel('Edit Properties...').click();\n    await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');\n    await page.getByLabel('Save', { exact: true }).click();\n\n    // Create a Table for filtering ON values\n    const tableFilterOnValue = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      name: 'Table Filter On Value'\n    });\n    const tableFilterOnTreeItem = page.getByRole('treeitem', {\n      name: tableFilterOnValue.name\n    });\n\n    // Create a Table for filtering OFF values\n    const tableFilterOffValue = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      name: 'Table Filter Off Value'\n    });\n    const tableFilterOffTreeItem = page.getByRole('treeitem', {\n      name: tableFilterOffValue.name\n    });\n\n    // Navigate to ON filtering table and add state generator and setup filters\n    await page.goto(tableFilterOnValue.url);\n    await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));\n    await selectFilterOption(page, '1');\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Navigate to OFF filtering table and add state generator and setup filters\n    await page.goto(tableFilterOffValue.url);\n    await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));\n    await selectFilterOption(page, '0');\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Navigate to the display layout and edit it\n    await page.goto(displayLayout.url);\n\n    // Add the tables to the display layout\n    await page.getByLabel('Edit Object').click();\n    await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {\n      targetPosition: { x: 10, y: 300 }\n    });\n    await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {\n      targetPosition: { x: 400, y: 500 },\n      // eslint-disable-next-line playwright/no-force-option\n      force: true\n    });\n    await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {\n      targetPosition: { x: 10, y: 100 }\n    });\n    await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {\n      targetPosition: { x: 400, y: 300 },\n      // eslint-disable-next-line playwright/no-force-option\n      force: true\n    });\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Get the tables so we can verify filtering is working as expected\n    const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {\n      exact: true\n    });\n    const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {\n      exact: true\n    });\n\n    // Verify filtering is working correctly\n\n    // Check that no filtered values appear for at least 2 seconds\n    const VERIFICATION_TIME = 2000; // 2 seconds\n    const CHECK_INTERVAL = 100; // Check every 100ms\n\n    // Create a promise that will check for filtered values periodically\n    const checkForCorrectValues = new Promise((resolve, reject) => {\n      const interval = setInterval(async () => {\n        const offCount = await tableFilterOn.locator('td[title=\"OFF\"]').count();\n        const onCount = await tableFilterOff.locator('td[title=\"ON\"]').count();\n        if (offCount > 0 || onCount > 0) {\n          clearInterval(interval);\n          reject(\n            new Error(\n              `Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`\n            )\n          );\n        }\n      }, CHECK_INTERVAL);\n\n      // After VERIFICATION_TIME, if no filtered values were found, resolve successfully\n      setTimeout(() => {\n        clearInterval(interval);\n        resolve();\n      }, VERIFICATION_TIME);\n    });\n\n    await expect(checkForCorrectValues).resolves.toBeUndefined();\n  });\n});\n\nasync function selectFilterOption(page, filterOption) {\n  await page.getByRole('tab', { name: 'Filters' }).click();\n  await page\n    .getByLabel('Inspector Views')\n    .locator('li')\n    .filter({ hasText: 'State Generator' })\n    .locator('span')\n    .click();\n  await page.getByRole('switch').click();\n  await page.selectOption('select[name=\"setSelectionThreshold\"]', filterOption);\n}\n\nasync function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {\n  await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);\n  await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);\n  expect(\n    await page\n      .getByLabel(layoutObject, {\n        exact: true\n      })\n      .count()\n  ).toBe(1);\n  await removeLayoutObject(page, layoutObject);\n  await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);\n}\n\n/**\n * Remove the first matching layout object from the layout\n * @param {import('@playwright/test').Page} page\n * @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject\n */\nasync function removeLayoutObject(page, layoutObject) {\n  await page\n    .getByLabel(`Move ${layoutObject} Frame`, { exact: true })\n    .or(page.getByLabel(layoutObject, { exact: true }))\n    .first()\n    // eslint-disable-next-line playwright/no-force-option\n    .click({ force: true });\n  await page.getByTitle('Delete the selected object').click();\n  await page.getByRole('button', { name: 'Ok', exact: true }).click();\n}\n\n/**\n * Add a layout object to the specified layout\n * @param {import('@playwright/test').Page} page\n * @param {string} layoutName\n * @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject\n */\nasync function addLayoutObject(page, layoutName, layoutObject) {\n  await page.getByLabel(`${layoutName} Layout`, { exact: true }).click();\n  await page.getByText('Add Drawing Object').click();\n  await page\n    .getByRole('menuitem', {\n      name: layoutObject\n    })\n    .click();\n  if (layoutObject === 'Text') {\n    await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!');\n    await page.getByText('Ok').click();\n  } else if (layoutObject === 'Image') {\n    await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);\n    await page.getByText('Ok').click();\n  }\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/event/eventTimelineView.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Event Timeline View', () => {\n  let eventTimelineView;\n  let eventGenerator1;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    eventTimelineView = await createDomainObjectWithDefaults(page, {\n      type: 'Time Strip'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: eventTimelineView.uuid\n    });\n\n    eventGenerator1 = await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: eventTimelineView.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator with Acknowledge',\n      parent: eventTimelineView.uuid\n    });\n\n    await setTimeConductorBounds(page, {\n      startDate: '2024-01-01',\n      endDate: '2024-01-01',\n      startTime: '01:01:00',\n      endTime: '01:04:00'\n    });\n  });\n\n  test('Ensure we can build a Time Strip with event', async ({ page }) => {\n    await page.goto(eventTimelineView.url);\n\n    // click on an event\n    await page\n      .getByLabel(eventTimelineView.name)\n      .getByLabel(/PROGRAM ALARM/)\n      .click();\n\n    // click on the event inspector tab\n    await page.getByRole('tab', { name: 'Event' }).click();\n\n    // ensure the event inspector has the the same event\n    await expect(page.getByText(/PROGRAM ALARM/)).toBeVisible();\n\n    // count the event lines\n    const eventWrappersContainer = page.locator('.c-events-tsv__container');\n    const eventWrappers = eventWrappersContainer.locator('.c-events-tsv__event-line');\n    const expectedEventWrappersCount = 25;\n    await expect(eventWrappers).toHaveCount(expectedEventWrappersCount);\n\n    // click on another event\n    await page\n      .getByLabel(eventTimelineView.name)\n      .getByLabel(/pegged/)\n      .click();\n\n    // ensure the tooltip shows up\n    await expect(\n      page.getByRole('tooltip').getByText(/pegged on horizontal velocity/)\n    ).toBeVisible();\n\n    // and that event appears in the inspector\n    await expect(\n      page.getByLabel('Inspector Views').getByText(/pegged on horizontal velocity/)\n    ).toBeVisible();\n\n    // turn on extended lines\n    await page\n      .getByRole('button', {\n        name: `Toggle extended event lines overlay for ${eventGenerator1.name}`\n      })\n      .click();\n\n    // count the extended lines\n    const overlayLinesContainer = page.locator('.c-timeline__overlay-lines');\n    const extendedLines = overlayLinesContainer.locator('.c-timeline__event-line--extended');\n    const expectedCount = 25;\n    await expect(extendedLines).toHaveCount(expectedCount);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  acknowledgeFault,\n  acknowledgeMultipleFaults,\n  changeViewTo,\n  getFault,\n  getFaultByName,\n  getFaultName,\n  getFaultNamespace,\n  getFaultTriggerTime,\n  navigateToFaultManagementWithoutExample,\n  navigateToFaultManagementWithStaticExample,\n  selectFaultItem,\n  shelveFault,\n  shelveMultipleFaults,\n  sortFaultsBy\n} from '../../../../helper/faultUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('The Fault Management Plugin using example faults', () => {\n  test.beforeEach(async ({ page }) => {\n    await navigateToFaultManagementWithStaticExample(page);\n  });\n\n  test('Shows a criticality icon for every fault', async ({ page }) => {\n    const faultCount = await page.locator('.c-fault-mgmt__list').count();\n    const criticalityIconCount = await page.locator('.c-fault-mgmt__list-severity').count();\n\n    expect(faultCount).toEqual(criticalityIconCount);\n  });\n\n  test('When selecting a fault, it has an \"is-selected\" class and its information shows in the inspector', async ({\n    page\n  }) => {\n    await selectFaultItem(page, 1);\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    const inspectorFaultName = page\n      .getByLabel('Source inspector properties')\n      .getByLabel('inspector property value');\n\n    await expect(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/);\n    await expect(inspectorFaultName).toHaveCount(1);\n  });\n\n  test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({\n    page\n  }) => {\n    await selectFaultItem(page, 1);\n    await selectFaultItem(page, 2);\n\n    const selectedRows = page.getByRole('checkbox', { checked: true });\n    await expect(selectedRows).toHaveCount(2);\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n    const firstSelectedFaultName = await selectedRows.nth(0).textContent();\n    const secondSelectedFaultName = await selectedRows.nth(1).textContent();\n    await expect(\n      page.locator(`.c-inspector__properties >> :text(\"${firstSelectedFaultName}\")`)\n    ).toHaveCount(0);\n    await expect(\n      page.locator(`.c-inspector__properties >> :text(\"${secondSelectedFaultName}\")`)\n    ).toHaveCount(0);\n  });\n\n  test('Allows you to shelve a fault', async ({ page }) => {\n    const shelvedFaultName = await getFaultName(page, 2);\n    const beforeShelvedFault = getFaultByName(page, shelvedFaultName);\n\n    await expect(beforeShelvedFault).toHaveCount(1);\n\n    await shelveFault(page, 2);\n\n    // check it is removed from standard view\n    const afterShelvedFault = getFaultByName(page, shelvedFaultName);\n    await expect(afterShelvedFault).toHaveCount(0);\n\n    await changeViewTo(page, 'shelved');\n\n    const shelvedViewFault = getFaultByName(page, shelvedFaultName);\n\n    await expect(shelvedViewFault).toHaveCount(1);\n  });\n\n  test('Allows you to acknowledge a fault', async ({ page }) => {\n    const acknowledgedFaultName = await getFaultName(page, 3);\n\n    await acknowledgeFault(page, 3);\n\n    // the acknowledged fault moves to position 5 since the list is sorted by unacknowledged first\n    const fault = getFault(page, 5);\n    await expect(fault).toHaveClass(/is-acknowledged/);\n\n    await changeViewTo(page, 'acknowledged');\n\n    const acknowledgedViewFaultName = await getFaultName(page, 1);\n    expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);\n  });\n\n  test('Allows you to shelve multiple faults', async ({ page }) => {\n    const shelvedFaultNameOne = await getFaultName(page, 1);\n    const shelvedFaultNameFour = await getFaultName(page, 4);\n\n    const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);\n    const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);\n\n    await expect(beforeShelvedFaultOne).toHaveCount(1);\n    await expect(beforeShelvedFaultFour).toHaveCount(1);\n\n    await shelveMultipleFaults(page, 1, 4);\n\n    // check it is removed from standard view\n    const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);\n    const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);\n    await expect(afterShelvedFaultOne).toHaveCount(0);\n    await expect(afterShelvedFaultFour).toHaveCount(0);\n\n    await changeViewTo(page, 'shelved');\n\n    const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);\n    const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);\n\n    await expect(shelvedViewFaultOne).toHaveCount(1);\n    await expect(shelvedViewFaultFour).toHaveCount(1);\n  });\n\n  test('Allows you to acknowledge multiple faults', async ({ page }) => {\n    const acknowledgedFaultNameTwo = await getFaultName(page, 2);\n    const acknowledgedFaultNameFive = await getFaultName(page, 5);\n\n    await acknowledgeMultipleFaults(page, 2, 5);\n\n    // the acknowledged faults move to positions 4 and 5 since the list is sorted by unacknowledged first\n    const faultTwoNowFour = getFault(page, 4);\n    const faultFive = getFault(page, 5);\n\n    // check they have been acknowledged\n    await expect(faultTwoNowFour).toHaveClass(/is-acknowledged/);\n    await expect(faultFive).toHaveClass(/is-acknowledged/);\n\n    await changeViewTo(page, 'acknowledged');\n\n    const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);\n    const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);\n\n    await expect(acknowledgedViewFaultTwo).toHaveCount(1);\n    await expect(acknowledgedViewFaultFive).toHaveCount(1);\n  });\n\n  test('Allows you to search faults', async ({ page }) => {\n    const faultThreeNamespace = await getFaultNamespace(page, 3);\n    const faultTwoName = await getFaultName(page, 2);\n    const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);\n\n    // should be all faults (5)\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);\n\n    // search namespace\n    await page\n      .getByLabel('Fault Management Object View')\n      .getByLabel('Search Input')\n      .fill(faultThreeNamespace);\n\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);\n    expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);\n\n    // all faults\n    await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);\n\n    // search name\n    await page\n      .getByLabel('Fault Management Object View')\n      .getByLabel('Search Input')\n      .fill(faultTwoName);\n\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);\n    expect(await getFaultName(page, 1)).toEqual(faultTwoName);\n\n    // all faults\n    await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);\n\n    // search triggerTime\n    await page\n      .getByLabel('Fault Management Object View')\n      .getByLabel('Search Input')\n      .fill(faultFiveTriggerTime);\n\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);\n    expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);\n  });\n\n  test('Confirms default sort is unacknowledged-first', async ({ page }) => {\n    // acknowledge 2 faults\n    await acknowledgeMultipleFaults(page, 2, 5);\n    // get a list of all faults.\n    const allFaults = page.locator('.c-fault-mgmt__list');\n\n    const { lastUnack, firstAck } =\n      await getFirstAndLastUnacknowledgedAndAcknowledgedFaults(allFaults);\n\n    // confirm that the last unacknowledged fault is before the first acknowledged fault.\n    expect(lastUnack).toBeGreaterThan(-1);\n    expect(firstAck).toBeGreaterThan(-1);\n    expect(lastUnack).toBeLessThan(firstAck);\n  });\n\n  test('Allows you to sort faults', async ({ page }) => {\n    /**\n     * Compares two severity levels and returns a number indicating their relative order.\n     *\n     * @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity1 - The first severity level to compare.\n     * @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity2 - The second severity level to compare.\n     * @returns {number} - A negative number if severity1 is less severe than severity2,\n     *                     a positive number if severity1 is more severe than severity2,\n     *                     or 0 if they are equally severe.\n     */\n    // eslint-disable-next-line func-style\n    const compareSeverity = (severity1, severity2) => {\n      const severityOrder = ['WATCH', 'WARNING', 'CRITICAL'];\n      return severityOrder.indexOf(severity1) - severityOrder.indexOf(severity2);\n    };\n\n    const faultOneName = 'Example Fault 1';\n    const faultFiveName = 'Example Fault 5';\n    let firstFaultName = await getFaultName(page, 1);\n\n    expect(firstFaultName).toEqual(faultOneName);\n\n    await sortFaultsBy(page, 'oldest-first');\n\n    firstFaultName = await getFaultName(page, 1);\n    expect(firstFaultName).toEqual(faultFiveName);\n\n    await sortFaultsBy(page, 'severity');\n\n    const firstFaultSeverityLabel = await page\n      .getByLabel('Severity:')\n      .first()\n      .getAttribute('aria-label');\n    const firstFaultSeverity = firstFaultSeverityLabel.split(' ').slice(1).join(' ');\n\n    const lastFaultSeverityLabel = await page\n      .getByLabel('Severity:')\n      .last()\n      .getAttribute('aria-label');\n    const lastFaultSeverity = lastFaultSeverityLabel.split(' ').slice(1).join(' ');\n\n    expect(compareSeverity(firstFaultSeverity, lastFaultSeverity)).toBeGreaterThan(0);\n\n    // acknowledge 2 faults\n    await acknowledgeMultipleFaults(page, 2, 5);\n    // Sort by Unacknowledged First\n    await sortFaultsBy(page, 'unacknowledged-first');\n    let allFaults = page.locator('.c-fault-mgmt__list');\n    const { lastUnack, firstAck } =\n      await getFirstAndLastUnacknowledgedAndAcknowledgedFaults(allFaults);\n\n    // confirm that the last unacknowledged fault is before the first acknowledged fault.\n    expect(lastUnack).toBeGreaterThan(-1);\n    expect(firstAck).toBeGreaterThan(-1);\n    expect(lastUnack).toBeLessThan(firstAck);\n  });\n});\n\ntest.describe('The Fault Management Plugin without using example faults', () => {\n  test.beforeEach(async ({ page }) => {\n    await navigateToFaultManagementWithoutExample(page);\n  });\n\n  test('Shows no faults when no faults are provided', async ({ page }) => {\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);\n\n    await changeViewTo(page, 'acknowledged');\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);\n\n    await changeViewTo(page, 'shelved');\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);\n  });\n\n  test('Will return no faults when searching', async ({ page }) => {\n    await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('fault');\n\n    await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);\n  });\n});\n\nasync function getFirstAndLastUnacknowledgedAndAcknowledgedFaults(allFaults) {\n  const { lastUnack, firstAck } = await allFaults.evaluateAll((faults) => {\n    let lastUnackIndex = -1;\n    let firstAckIndex = -1;\n    console.log('faults', faults);\n\n    for (let i = 0; i < faults.length; i++) {\n      const fault = faults[i];\n\n      // get the index of the last unacknowledged fault in the list.\n      if (fault.classList.contains('is-unacknowledged')) {\n        lastUnackIndex = i;\n      }\n\n      // get the index of the first acknowledged fault in the list.\n      if (fault.classList.contains('is-acknowledged') && firstAckIndex === -1) {\n        firstAckIndex = i;\n        break;\n      }\n    }\n\n    return { lastUnack: lastUnackIndex, firstAck: firstAckIndex };\n  });\n  return { lastUnack, firstAck };\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  setFixedIndependentTimeConductorBounds\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst LOCALSTORAGE_PATH = fileURLToPath(\n  new URL('../../../../test-data/flexible_layout_with_child_layouts.json', import.meta.url)\n);\n\ntest.describe('Flexible Layout', () => {\n  let sineWaveObject;\n  let clockObject;\n  let treePane;\n  let sineWaveGeneratorTreeItem;\n  let clockTreeItem;\n  let flexibleLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Sine Wave Generator\n    sineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n\n    // Create Clock Object\n    clockObject = await createDomainObjectWithDefaults(page, {\n      type: 'Clock'\n    });\n\n    // Create a Flexible Layout\n    flexibleLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout'\n    });\n\n    // Define the Sine Wave Generator and Clock tree items\n    treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    clockTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(clockObject.name)\n    });\n  });\n  test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({\n    page\n  }) => {\n    await page.goto(flexibleLayout.url);\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();\n    // Add the Sine Wave Generator and Clock to the Flexible Layout\n    await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());\n    await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));\n    // Check that panes can be dragged while Flexible Layout is in Edit mode\n    let dragWrapper = page\n      .locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')\n      .first();\n    await expect(dragWrapper).toHaveAttribute('draggable', 'true');\n    // Save Flexible Layout\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    // Check that panes are not draggable while Flexible Layout is in Browse mode\n    dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();\n    await expect(dragWrapper).toHaveAttribute('draggable', 'false');\n  });\n  test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6942'\n    });\n\n    await page.goto(flexibleLayout.url);\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();\n    // Add the Sine Wave Generator and Clock to the Flexible Layout\n    await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());\n    await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));\n\n    // Click on the first frame to select it\n    await page.locator('.c-fl-container__frame').first().click();\n    await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(\n      's-selected',\n      ''\n    );\n\n    // Assert the toolbar is visible\n    await expect(page.locator('.c-toolbar')).toBeInViewport();\n\n    // Assert the layout is in columns orientation\n    expect(await page.locator('.c-fl--rows').count()).toEqual(0);\n\n    // Change the layout to rows orientation\n    await page.getByTitle('Switch to rows layout').click();\n\n    // Assert the layout is in rows orientation\n    expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);\n\n    // Assert the frame of the first item is visible\n    await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);\n\n    // Hide the frame of the first item\n    await page.getByTitle('Frame visible').click();\n\n    // Assert the frame is hidden\n    await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);\n\n    // Assert there are 2 containers\n    expect(await page.locator('.c-fl-container').count()).toEqual(2);\n\n    // Add a container\n    await page.getByTitle('Add Container').click();\n\n    // Assert there are 3 containers\n    expect(await page.locator('.c-fl-container').count()).toEqual(3);\n\n    // Save Flexible Layout\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Nav away and back\n    await page.goto(sineWaveObject.url);\n    await page.goto(flexibleLayout.url);\n\n    // Wait for the first frame to be visible so we know the layout has loaded\n    await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();\n\n    // Assert the settings have persisted\n    expect(await page.locator('.c-fl-container').count()).toEqual(3);\n    expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);\n    await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);\n  });\n  test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({\n    page\n  }) => {\n    await page.goto(flexibleLayout.url);\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();\n    // Add the Sine Wave Generator to the Flexible Layout and save changes\n    await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);\n\n    // Expand the Flexible Layout so we can remove the sine wave generator\n    await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();\n\n    // Bring up context menu and remove\n    await sineWaveGeneratorTreeItem.first().click({ button: 'right' });\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // Verify that the item has been removed from the layout\n    expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);\n  });\n  test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/3117'\n    });\n    await page.goto(flexibleLayout.url);\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    // Add the Sine Wave Generator to the Flexible Layout and save changes\n    await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);\n\n    // Expand the Flexible Layout so we can remove the sine wave generator\n    await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();\n\n    // Go to the original Sine Wave Generator to navigate away from the Flexible Layout\n    await page.goto(sineWaveObject.url);\n\n    // Bring up context menu and remove\n    await sineWaveGeneratorTreeItem.first().click({ button: 'right' });\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // navigate back to the display layout to confirm it has been removed\n    await page.goto(flexibleLayout.url);\n\n    // Verify that the item has been removed from the layout\n    expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);\n  });\n\n  test('independent time works with flexible layouts and its children', async ({ page }) => {\n    // Create Example Imagery\n    const exampleImageryObject = await createDomainObjectWithDefaults(page, {\n      type: 'Example Imagery'\n    });\n\n    await page.goto(flexibleLayout.url);\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();\n    const exampleImageryTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(exampleImageryObject.name)\n    });\n    // Add the Sine Wave Generator to the Flexible Layout and save changes\n    await exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());\n\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // flip on independent time conductor\n    await setFixedIndependentTimeConductorBounds(page, {\n      start: '2021-12-30 01:01:00.000Z',\n      end: '2021-12-30 01:11:00.000Z'\n    });\n\n    // check image date\n    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();\n\n    // flip it off\n    await page.getByRole('switch').click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n  });\n});\n\ntest.describe('Flexible Layout Toolbar Actions @localStorage', () => {\n  test.use({\n    storageState: LOCALSTORAGE_PATH\n  });\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await page\n      .locator('a')\n      .filter({ hasText: 'Parent Flexible Layout Flexible Layout' })\n      .first()\n      .click();\n    await page.getByLabel('Edit Object').click();\n  });\n  test('Add/Remove Container', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7234'\n    });\n\n    const containerHandles = page.getByRole('columnheader', { name: 'Handle' });\n    expect(await containerHandles.count()).toEqual(2);\n    await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();\n    await page.getByTitle('Add Container').click();\n    expect(await containerHandles.count()).toEqual(3);\n    await page.getByTitle('Remove Container').click();\n    await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(\n      'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'\n    );\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    expect(await containerHandles.count()).toEqual(2);\n  });\n  test('Remove Frame', async ({ page }) => {\n    expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);\n    await page.getByRole('group', { name: 'Child Layout 1' }).click();\n    await page.getByTitle('Remove Frame').click();\n    await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(\n      'This action will remove this frame from this Flexible Layout. Do you want to continue?'\n    );\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(1);\n  });\n  test('Columns/Rows Layout Toggle', async ({ page }) => {\n    await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();\n    const flexRows = page.getByLabel('Flexible Layout Row');\n    expect(await flexRows.count()).toEqual(0);\n    await page.getByTitle('Switch to rows layout').click();\n    expect(await flexRows.count()).toEqual(1);\n    await page.getByTitle('Switch to columns layout').click();\n    expect(await flexRows.count()).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/folders/viewPersist.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Folder View Persistence', () => {\n  let folder;\n  let sineWaveGenerator;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a folder\n    folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n\n    // Create a sine wave generator inside the folder\n    sineWaveGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: folder.uuid\n    });\n\n    // Navigate to the folder\n    await page.goto(folder.url);\n  });\n\n  test('Folder view persists when navigating away and back', async ({ page }) => {\n    // Click the view switcher button to open the menu\n    await page.getByLabel('Open the View Switcher Menu').click();\n\n    // Click the List View option from the dropdown menu\n    await page.getByRole('menuitem', { name: /List View/ }).click();\n\n    // Verify that we're now in List view by checking for the c-list-view class\n    await expect(page.locator('.c-list-view')).toBeVisible();\n\n    // Navigate to the sine wave generator\n    await page.goto(sineWaveGenerator.url);\n\n    // Verify we're on the sine wave generator page by checking for the object view container\n    await expect(page.locator('.c-object-view.is-object-type-generator')).toBeVisible();\n\n    // Navigate back to the folder\n    await page.goto(folder.url);\n\n    // Verify that the folder is still in List view\n    await expect(page.locator('.c-list-view')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is dedicated to testing the Gauge component.\n */\n\nimport { v4 as uuid } from 'uuid';\n\nimport {\n  createDomainObjectWithDefaults,\n  createExampleTelemetryObject,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Gauge', () => {\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Can add and remove telemetry sources', async ({ page }) => {\n    // Create the gauge with defaults\n    const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });\n\n    // Create a sine wave generator within the gauge\n    const swg1 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: `swg-${uuid()}`,\n      parent: gauge.uuid\n    });\n\n    // Navigate to the gauge and verify that\n    // the SWG appears in the elements pool\n    await page.goto(gauge.url);\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Create another sine wave generator within the gauge\n    const swg2 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: `swg-${uuid()}`,\n      parent: gauge.uuid\n    });\n\n    // Verify that the 'Replace telemetry source' modal appears and accept it\n    await expect(\n      page.getByText(\n        'This action will replace the current telemetry source. Do you want to continue?'\n      )\n    ).toBeVisible();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Navigate to the gauge and verify that the new SWG\n    // appears in the elements pool and the old one is gone\n    await page.goto(gauge.url);\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden();\n    await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible();\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Right click on the new SWG in the elements pool and delete it\n    await page.getByLabel(`Preview ${swg2.name}`).click({\n      button: 'right'\n    });\n    await page.getByLabel('Remove').click();\n\n    // Verify that the 'Remove object' confirmation modal appears and accept it\n    await expect(\n      page.getByText(\n        'Warning! This action will remove this object. Are you sure you want to continue?'\n      )\n    ).toBeVisible();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Verify that the elements pool shows no elements\n    await expect(page.locator('text=\"No contained elements\"')).toBeVisible();\n  });\n  test('Can create a non-default Gauge', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5356'\n    });\n    //Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click the object specified by 'type'\n    await page.getByRole('menuitem', { name: 'Gauge' }).click();\n    // FIXME: We need better selectors for these custom form controls\n    const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');\n    await displayCurrentValueSwitch.uncheck();\n    await page.getByLabel('Save').click();\n\n    // TODO: Verify changes in the UI\n  });\n  test('Can edit a single Gauge-specific property', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5985'\n    });\n\n    // Create the gauge with defaults\n    await createDomainObjectWithDefaults(page, { type: 'Gauge' });\n    await page.getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    // FIXME: We need better selectors for these custom form controls\n    const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');\n    await displayCurrentValueSwitch.uncheck();\n    await page.getByLabel('Save').click();\n\n    // TODO: Verify changes in the UI\n  });\n\n  test('Gauge does not display NaN when data not available', async ({ page }) => {\n    // Create a Gauge\n    const gauge = await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Gauge with no data'\n    });\n\n    // Create a Sine Wave Generator in the Gauge with a loading delay\n    const swgWith5sDelay = await createExampleTelemetryObject(page, gauge.uuid);\n\n    await page.goto(swgWith5sDelay.url);\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();\n\n    //Edit Example Telemetry Object to include 5s loading Delay\n    await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('5000');\n\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Wait until the URL is updated\n    await page.waitForURL(`**/${gauge.uuid}/*`);\n\n    // Nav to the Gauge\n    await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });\n    // Check that the value is not displayed\n    //TODO https://github.com/nasa/openmct/issues/7790 update this locator\n    await expect(page.getByTitle('Value is currently out of')).toHaveAttribute(\n      'aria-valuenow',\n      '--'\n    );\n  });\n\n  test('Gauge does not break when an object is missing', async ({ page }) => {\n    // Set up error listeners\n    const pageErrors = [];\n\n    // Listen for uncaught exceptions\n    page.on('pageerror', (err) => {\n      pageErrors.push(err.message);\n    });\n\n    await setRealTimeMode(page);\n\n    // Create a Gauge\n    const gauge = await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Gauge with missing object'\n    });\n\n    // Create a Sine Wave Generator in the Gauge with a loading delay\n    const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);\n\n    // Remove the object from local storage\n    await page.evaluate(\n      ([missingObject]) => {\n        const mct = localStorage.getItem('mct');\n        const mctObjects = JSON.parse(mct);\n        delete mctObjects[missingObject.uuid];\n        localStorage.setItem('mct', JSON.stringify(mctObjects));\n      },\n      [missingSWG]\n    );\n\n    // Verify start bounds\n    await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();\n\n    // Nav to the Gauge\n    await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });\n\n    // adjust time bounds and ensure they are updated\n    await setStartOffset(page, {\n      startHours: '00',\n      startMins: '45',\n      startSecs: '00'\n    });\n\n    // Verify start bounds changed\n    await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();\n\n    // // Verify no errors were thrown\n    expect(pageErrors).toHaveLength(0);\n  });\n\n  test('Gauge enforces composition policy', async ({ page }) => {\n    // Create a Gauge\n    await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Unnamed Gauge'\n    });\n\n    // Try to create a Folder into the Gauge. Should be disallowed.\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByRole('menuitem', { name: /Folder/ }).click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n    await page.getByLabel('Cancel').click();\n\n    // Try to create a Display Layout into the Gauge. Should be disallowed.\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByRole('menuitem', { name: /Display Layout/ }).click();\n    await expect(page.locator('[aria-label=\"Save\"]')).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding imagery,\nbut only assume that example imagery is present.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithRealTime,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport {\n  createImageryViewWithShortDelay,\n  FIVE_MINUTES,\n  IMAGE_LOAD_DELAY,\n  MOUSE_WHEEL_DELTA_Y,\n  THIRTY_SECONDS\n} from '../../../../helper/imageryUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];\nconst tagHotkey = ['Shift', 'Alt'];\nconst expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';\nconst thumbnailUrlParamsRegexp = /\\?w=100&h=100/;\n\n//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.\ntest.describe('Example Imagery Object', () => {\n  test.beforeEach(async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a default 'Example Imagery' object\n    const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });\n\n    // Verify that the created object is focused\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);\n    await page.getByLabel('Focused Image Element').hover({ trial: true });\n\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n\n  test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {\n    // Zoom in x2 and assert\n    await mouseZoomOnImageAndAssert(page, 2);\n\n    // Zoom out x2 and assert\n    await mouseZoomOnImageAndAssert(page, -2);\n  });\n\n  test('Compass HUD should be hidden by default', async ({ page }) => {\n    await expect(page.locator('.c-hud')).toBeHidden();\n  });\n\n  test('Can right click on image and open it in a new tab @2p', async ({ page, context }) => {\n    // try to right click on image\n    const backgroundImage = page.getByLabel('Focused Image Element');\n    await backgroundImage.click({\n      button: 'right',\n      // Need force option here due to annotation overlay which blocks playwright's click\n      // eslint-disable-next-line playwright/no-force-option\n      force: true\n    });\n    // expect context menu to appear\n    await expect(page.getByText('Save Image As')).toBeVisible();\n    await expect(page.getByText('Open Image in New Tab')).toBeVisible();\n\n    // click on open image in new tab\n    const pagePromise = context.waitForEvent('page');\n    await page.getByText('Open Image in New Tab').click();\n    // expect new tab to be in browser\n    const newPage = await pagePromise;\n    await newPage.waitForLoadState();\n    // expect new tab url to have jpg in it\n    expect(newPage.url()).toContain('.jpg');\n  });\n\n  test('Can adjust image brightness/contrast by dragging the sliders', async ({\n    page,\n    browserName\n  }) => {\n    // eslint-disable-next-line playwright/no-skipped-test\n    test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');\n    // Open the image filter menu\n    await page.locator('[role=toolbar] button[title=\"Brightness and contrast\"]').click();\n\n    // Drag the brightness and contrast sliders around and assert filter values\n    await dragBrightnessSliderAndAssertFilterValues(page);\n    await dragContrastSliderAndAssertFilterValues(page);\n  });\n\n  test('Can use independent time conductor to change time', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6821'\n    });\n\n    // Test independent fixed time with global fixed time\n    // flip on independent time conductor\n    await page.getByLabel('Enable Independent Time Conductor').click();\n\n    await expect(page.locator('#independentTCToggle')).toBeChecked();\n    await expect(page.locator('.c-compact-tc').first()).toBeVisible();\n    await page.getByLabel('Independent Time Conductor Panel').click();\n    await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();\n    await page.getByLabel('Time Conductor Options').hover({ trial: true });\n\n    await page.getByRole('textbox', { name: 'Start date' }).hover({ trial: true });\n    await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');\n    await page.keyboard.press('Tab');\n    await page.getByRole('textbox', { name: 'Start time' }).hover({ trial: true });\n    await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');\n    await page.keyboard.press('Tab');\n    await page.getByRole('textbox', { name: 'End date' }).hover({ trial: true });\n    await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');\n    await page.keyboard.press('Tab');\n    await page.getByRole('textbox', { name: 'End time' }).hover({ trial: true });\n    await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');\n    await page.getByLabel('Submit time bounds').click();\n\n    // wait for image thumbnails to stabilize\n    await page.getByLabel('Image Thumbnails', { exact: true }).hover({ trial: true });\n    await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();\n\n    // flip it off\n    await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n\n    // Test independent fixed time with global realtime\n    await setRealTimeMode(page);\n    await expect(\n      page.getByRole('switch', { name: 'Enable Independent Time Conductor' })\n    ).toBeEnabled();\n    await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();\n    // check image date to be in the past\n    await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();\n    // flip it off\n    await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n\n    // Test independent realtime with global realtime\n    await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();\n    // check image date\n    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();\n    // change independent time to realtime\n    await page.getByLabel('Independent Time Conductor Panel').click();\n    await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();\n    await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();\n    await page.getByRole('menuitem', { name: /Real-Time/ }).click();\n    // timestamp shouldn't be in the past anymore\n    await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();\n    // back to the past\n    await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();\n    await page.getByRole('menuitem', { name: /Real-Time/ }).click();\n    await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();\n    await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();\n    // check image date to be in the past\n    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();\n  });\n\n  test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {\n    await page.locator('.c-imagery__main-image__bg').hover({ trial: true });\n\n    // zoom in\n    await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * 2);\n    const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;\n    const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;\n    // move to the right\n\n    // center the mouse pointer\n    await page.mouse.move(imageCenterX, imageCenterY);\n\n    //Get Diagnostic info about process environment\n    console.log('process.platform is ' + process.platform);\n    const getUA = await page.evaluate(() => navigator.userAgent);\n    console.log('navigator.userAgent ' + getUA);\n    // Pan Imagery Hints\n    const imageryHintsText = await page.locator('.c-imagery__hints').innerText();\n    expect(expectedAltText).toEqual(imageryHintsText);\n\n    // pan right\n    await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n    await page.mouse.down();\n    await page.mouse.move(imageCenterX - 200, imageCenterY, 10);\n    await page.mouse.up();\n    await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n    const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);\n\n    // pan left\n    await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n    await page.mouse.down();\n    await page.mouse.move(imageCenterX, imageCenterY, 10);\n    await page.mouse.up();\n    await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n    const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);\n\n    // pan up\n    await page.mouse.move(imageCenterX, imageCenterY);\n    await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n    await page.mouse.down();\n    await page.mouse.move(imageCenterX, imageCenterY + 200, 10);\n    await page.mouse.up();\n    await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n    const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);\n\n    // pan down\n    await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n    await page.mouse.down();\n    await page.mouse.move(imageCenterX, imageCenterY - 200, 10);\n    await page.mouse.up();\n    await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n    const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);\n  });\n\n  test('Can use alt+shift+drag to create a tag and ensure toolbars disappear', async ({ page }) => {\n    const canvas = page.locator('canvas');\n    await canvas.hover({ trial: true });\n\n    const canvasBoundingBox = await canvas.boundingBox();\n    const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;\n    const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;\n    await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));\n    await page.mouse.down();\n    // steps not working for me here\n    await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);\n    await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);\n    // toolbar should hide when we're creating annotations with a drag\n    await expect(page.locator('[role=\"toolbar\"][aria-label=\"Image controls\"]')).toBeHidden();\n    await page.mouse.up();\n    // toolbar should reappear when we're done creating annotations\n    await expect(page.locator('[role=\"toolbar\"][aria-label=\"Image controls\"]')).toBeVisible();\n    await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));\n\n    // Wait for canvas to stabilize.\n    await canvas.hover({ trial: true });\n\n    // add some tags\n    await page.getByText('Annotations').click();\n    await page.getByRole('button', { name: /Add Tag/ }).click();\n    await page.getByPlaceholder('Type to select tag').click();\n    await page.getByText('Driving').click();\n\n    await page.getByRole('button', { name: /Add Tag/ }).click();\n    await page.getByPlaceholder('Type to select tag').click();\n    await page.getByText('Science').click();\n\n    // click on a separate part of the canvas to ensure no tags appear\n    await page.mouse.click(canvasCenterX + 10, canvasCenterY + 10);\n    await expect(page.getByText('Driving')).toBeHidden();\n    await expect(page.getByText('Science')).toBeHidden();\n\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7083'\n    });\n    // click on annotation again and expect tags to appear\n    await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50);\n    await expect(page.getByText('Driving')).toBeVisible();\n    await expect(page.getByText('Science')).toBeVisible();\n\n    // add another tag and expect it to appear without changing selection\n    await page.getByRole('button', { name: /Add Tag/ }).click();\n    await page.getByPlaceholder('Type to select tag').click();\n    await page.getByText('Drilling').click();\n    await expect(page.getByText('Driving')).toBeVisible();\n    await expect(page.getByText('Science')).toBeVisible();\n    await expect(page.getByText('Drilling')).toBeVisible();\n  });\n\n  test('Can use + - buttons to zoom on the image', async ({ page }) => {\n    await buttonZoomOnImageAndAssert(page);\n  });\n\n  test('Can use the reset button to reset the image', async ({ page }) => {\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(1) translate(0px, 0px)'\n    );\n\n    // Get initial image dimensions\n    const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n\n    // Zoom in twice via button\n    await zoomIntoImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(2) translate(0px, 0px)'\n    );\n    await zoomIntoImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(3) translate(0px, 0px)'\n    );\n\n    // Get and assert zoomed in image dimensions\n    const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);\n    expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);\n\n    // Reset pan and zoom and assert against initial image dimensions\n    await resetImageryPanAndZoom(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(1) translate(0px, 0px)'\n    );\n    const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(finalBoundingBox).toEqual(initialBoundingBox);\n  });\n\n  test('Using the zoom features does not pause telemetry', async ({ page }) => {\n    const pausePlayButton = page.locator('.c-button.pause-play');\n\n    // switch to realtime\n    await setRealTimeMode(page);\n\n    await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);\n\n    // Zoom in via button\n    await zoomIntoImageryByButton(page);\n    await expect(pausePlayButton).not.toHaveClass(/is-paused/);\n  });\n\n  test('Uses low fetch priority', async ({ page }) => {\n    const priority = page.locator('.js-imageryView-image');\n    await expect(priority).toHaveAttribute('fetchpriority', 'low');\n  });\n});\n\ntest.describe('Example Imagery in Display Layout', () => {\n  let displayLayout;\n\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });\n\n    // Create Example Imagery inside Display Layout\n    await createImageryViewWithShortDelay(page, {\n      name: 'Unnamed Example Imagery',\n      parent: displayLayout.uuid\n    });\n\n    await page.goto(displayLayout.url);\n  });\n\n  test('View Large action pauses imagery when in realtime and returns to realtime', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/3647'\n    });\n\n    // set realtime mode\n    await setRealTimeMode(page);\n\n    // pause/play button\n    const pausePlayButton = page.locator('.c-button.pause-play');\n\n    await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);\n\n    // Open context menu and click view large menu item\n    await page.locator('button[title=\"View menu items\"]').click();\n    await page.locator('li[title=\"View Large\"]').click();\n    await expect(pausePlayButton).toHaveClass(/is-paused/);\n\n    await page.getByRole('button', { name: 'Close' }).click();\n    await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);\n  });\n\n  test('View Large action leaves keeps realtime mode paused', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/3647'\n    });\n\n    // set realtime mode\n    await setRealTimeMode(page);\n\n    // pause/play button\n    const pausePlayButton = page.locator('.c-button.pause-play');\n    await pausePlayButton.click();\n    await expect.soft(pausePlayButton).toHaveClass(/is-paused/);\n\n    // Open context menu and click view large menu item\n    await page.locator('button[title=\"View menu items\"]').click();\n    await page.locator('li[title=\"View Large\"]').click();\n    await expect(pausePlayButton).toHaveClass(/is-paused/);\n\n    await page.getByRole('button', { name: 'Close' }).click();\n    await expect.soft(pausePlayButton).toHaveClass(/is-paused/);\n  });\n\n  test('Imagery View operations', async ({ page }) => {\n    // Edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click on example imagery to expose toolbar\n    await page.locator('.c-so-view__header').click();\n\n    // Adjust object height\n    await page.locator('div[title=\"Resize object height\"] > input').click();\n    await page.locator('div[title=\"Resize object height\"] > input').fill('50');\n\n    // Adjust object width\n    await page.locator('div[title=\"Resize object width\"] > input').click();\n    await page.locator('div[title=\"Resize object width\"] > input').fill('50');\n\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n\n    await performImageryViewOperationsAndAssert(page, displayLayout);\n  });\n\n  test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {\n    const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');\n    // Edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click on example imagery to expose toolbar\n    await page.locator('.c-so-view__header').click();\n\n    // expect thumbnails not be visible when first added\n    expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();\n\n    // Resize the example imagery vertically to change the thumbnail visibility\n    /*\n        The following arbitrary values are added to observe the separate visual\n        conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).\n        Specifically, height is set to 50px for small thumbs and 100px for regular\n        */\n    await page.locator('div[title=\"Resize object height\"] > input').click();\n    await page.locator('div[title=\"Resize object height\"] > input').fill('50');\n\n    expect(thumbsWrapperLocator.isVisible()).toBeTruthy();\n    await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);\n\n    // Resize the example imagery vertically to change the thumbnail visibility\n    await page.locator('div[title=\"Resize object height\"] > input').click();\n    await page.locator('div[title=\"Resize object height\"] > input').fill('100');\n\n    await expect(thumbsWrapperLocator).toBeVisible();\n    await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);\n  });\n\n  /**\n   * Toggle layer visibility checkbox by clicking on checkbox label\n   * - should toggle checkbox and layer visibility for that image view\n   * - should NOT toggle checkbox and layer visibility for the first image view in display\n   */\n  test('Toggle layer visibility by clicking on label', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6709'\n    });\n    await createImageryViewWithShortDelay(page, {\n      name: 'Unnamed Example Imagery',\n      parent: displayLayout.uuid\n    });\n    await page.goto(displayLayout.url);\n\n    const imageElements = page.locator('.c-imagery__main-image-wrapper');\n\n    await expect(imageElements).toHaveCount(2);\n\n    const imageOne = page.locator('.c-imagery__main-image-wrapper').nth(0);\n    const imageTwo = page.locator('.c-imagery__main-image-wrapper').nth(1);\n    const imageOneWrapper = imageOne.locator('.image-wrapper');\n    const imageTwoWrapper = imageTwo.locator('.image-wrapper');\n\n    await imageTwo.hover();\n\n    await imageTwo.locator('button[title=\"Layers\"]').click();\n\n    const imageTwoLayersMenuContent = imageTwo.locator('button[title=\"Layers\"] + div');\n    const imageTwoLayersToggleLabel = imageTwoLayersMenuContent.locator('label').last();\n\n    await imageTwoLayersToggleLabel.click();\n\n    const imageOneLayers = imageOneWrapper.locator('.layer-image');\n    const imageTwoLayers = imageTwoWrapper.locator('.layer-image');\n\n    await expect(imageOneLayers).toHaveCount(0);\n    await expect(imageTwoLayers).toHaveCount(1);\n  });\n});\n\ntest.describe('Example Imagery in Flexible layout', () => {\n  let flexibleLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });\n\n    // Create Example Imagery inside the Flexible Layout\n    await createImageryViewWithShortDelay(page, {\n      name: 'Unnamed Example Imagery',\n      parent: flexibleLayout.uuid\n    });\n\n    // Navigate back to Flexible Layout\n    await page.goto(flexibleLayout.url);\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n\n  test('Can double-click on the image to view large image', async ({ page }) => {\n    // Double-click on the image to open large view\n    const imageElement = page.getByRole('button', { name: 'Image Wrapper' });\n    await imageElement.dblclick();\n\n    // Check if the large view is visible\n    page.getByRole('button', { name: 'Focused Image Element', state: 'visible' });\n\n    // Close the large view\n    await page.getByRole('button', { name: 'Close' }).click();\n  });\n\n  test('Imagery View operations', async ({ page, browserName }) => {\n    test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5326'\n    });\n\n    await performImageryViewOperationsAndAssert(page, flexibleLayout);\n  });\n});\n\ntest.describe('Example Imagery in Tabs View', () => {\n  let tabsView;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });\n    await page.goto(tabsView.url);\n\n    /* Create Sine Wave Generator with minimum Image Load Delay */\n    // Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click text=Example Imagery\n    await page.getByRole('menuitem', { name: 'Example Imagery' }).click();\n\n    // Clear and set Image load delay to minimum value\n    await page.locator('input[type=\"number\"]').clear();\n    await page.locator('input[type=\"number\"]').fill(`${IMAGE_LOAD_DELAY}`);\n\n    await page.getByLabel('Save').click();\n\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(\n      'Unnamed Example Imagery'\n    );\n\n    await page.goto(tabsView.url);\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n\n  test('Imagery View operations', async ({ page }) => {\n    await performImageryViewOperationsAndAssert(page, tabsView);\n  });\n});\n\ntest.describe('Example Imagery in Time Strip', () => {\n  let timeStripObject;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    timeStripObject = await createDomainObjectWithDefaults(page, {\n      type: 'Time Strip'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Example Imagery',\n      parent: timeStripObject.uuid\n    });\n    // Navigate to timestrip\n    await page.goto(timeStripObject.url);\n  });\n\n  test('Clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5632'\n    });\n\n    // Hover over the timestrip to reveal a thumbnail image\n    await page.locator('.c-imagery-tsv-container').hover();\n\n    // Get the img src of the hovered image thumbnail\n    const hoveredThumbnailImg = page.locator(\n      '.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img'\n    );\n    const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src');\n\n    // Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails\n    expect(hoveredThumbnailImgSrc).toBeTruthy();\n    expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp);\n\n    // Click on the hovered thumbnail to open \"View Large\" view\n    await page.locator('.c-imagery-tsv-container').click();\n\n    // Get the img src of the large view image\n    const viewLargeImg = page.locator('img.c-imagery__main-image__image');\n    const viewLargeImgSrc = await viewLargeImg.getAttribute('src');\n    expect(viewLargeImgSrc).toBeTruthy();\n\n    // Verify that the image in the large view is the same as the hovered thumbnail\n    expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);\n  });\n});\n\n/**\n * Perform the common actions and assertions for the Imagery View.\n * This function verifies the following in order:\n * 1. Can zoom in/out using the zoom buttons\n * 2. Can zoom in/out using the mouse wheel\n * 3. Can pan the image using the pan hotkey + mouse drag\n * 4. Clicking on the left arrow button pauses imagery and moves to the previous image\n * 5. Imagery is updated as new images stream in, regardless of pause status\n * 6. Old images are discarded when their timestamps fall out of bounds\n * 7. Multiple images can be discarded when their timestamps fall out of bounds\n * 8. Image brightness/contrast can be adjusted by dragging the sliders\n * @param {import('@playwright/test').Page} page\n */\nasync function performImageryViewOperationsAndAssert(page, layoutObject) {\n  await test.step('Verify that imagery thumbnails use a thumbnail url', async () => {\n    const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');\n    const mainImage = page.locator('.c-imagery__main-image__image');\n    await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);\n    await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);\n  });\n\n  // Click previous image button\n  const previousImageButton = page.getByLabel('Previous image');\n  await expect(previousImageButton).toBeVisible();\n  await page.getByLabel('Image Wrapper').hover({ trial: true });\n\n  // Need to force click as the annotation canvas lies on top of the image\n  // and fails the accessibility checks\n  // eslint-disable-next-line playwright/no-force-option\n  await previousImageButton.click({ force: true });\n\n  // Use the zoom buttons to zoom in and out\n  await buttonZoomOnImageAndAssert(page);\n\n  // Use Mouse Wheel to zoom in to previous image\n  await mouseZoomOnImageAndAssert(page, 2);\n\n  // Use alt+drag to move around image once zoomed in\n  await panZoomAndAssertImageProperties(page);\n\n  // Use Mouse Wheel to zoom out of previous image\n  await mouseZoomOnImageAndAssert(page, -2);\n\n  // Click next image button\n  const nextImageButton = page.getByLabel('Next image');\n  await expect(nextImageButton).toBeVisible();\n  await page.getByLabel('Image Wrapper').hover({ trial: true });\n  // eslint-disable-next-line playwright/no-force-option\n  await nextImageButton.click({ force: true });\n  // set realtime mode\n  await navigateToObjectWithRealTime(\n    page,\n    layoutObject.url,\n    `${FIVE_MINUTES}`,\n    `${THIRTY_SECONDS}`\n  );\n  // Verify previous image\n  await expect(previousImageButton).toBeVisible();\n  await page.getByLabel('Image Wrapper').hover({ trial: true });\n  // eslint-disable-next-line playwright/no-force-option\n  await previousImageButton.click({ force: true });\n  await page.locator('.active').click();\n  const selectedImage = page.locator('.selected');\n  await expect(selectedImage).toBeVisible();\n\n  // Zoom in on next image\n  await mouseZoomOnImageAndAssert(page, 2);\n\n  // Clicking on the left arrow should pause the imagery and go to previous image\n  await previousImageButton.click();\n  await expect(page.getByLabel('Pause automatic scrolling of image thumbnails')).toBeVisible();\n  await expect(selectedImage).toBeVisible();\n\n  // Verify selected image is still displayed\n  await expect(selectedImage).toBeVisible();\n\n  // Unpause imagery\n  await page.locator('.pause-play').click();\n\n  // Open the image filter menu\n  await page.locator('[role=toolbar] button[title=\"Brightness and contrast\"]').click();\n\n  // Drag the brightness and contrast sliders around and assert filter values\n  await dragBrightnessSliderAndAssertFilterValues(page);\n  await dragContrastSliderAndAssertFilterValues(page);\n}\n\n/**\n * Drag the brightness slider to max, min, and midpoint and assert the filter values\n * @param {import('@playwright/test').Page} page\n */\nasync function dragBrightnessSliderAndAssertFilterValues(page) {\n  const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';\n  const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();\n  const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;\n  const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;\n\n  await page.locator(brightnessSlider).hover({ trial: true });\n  await page.mouse.down();\n  await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);\n  await assertBackgroundImageBrightness(page, '500');\n  await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);\n  await assertBackgroundImageBrightness(page, '0');\n  await page.mouse.move(brightnessMidX, brightnessMidY);\n  await assertBackgroundImageBrightness(page, '250');\n  await page.mouse.up();\n}\n\n/**\n * Drag the contrast slider to max, min, and midpoint and assert the filter values\n * @param {import('@playwright/test').Page} page\n */\nasync function dragContrastSliderAndAssertFilterValues(page) {\n  const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';\n  const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();\n  const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;\n  const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;\n\n  await page.locator(contrastSlider).hover({ trial: true });\n  await page.mouse.down();\n  await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);\n  await assertBackgroundImageContrast(page, '500');\n  await page.mouse.move(contrastBoundingBox.x, contrastMidY);\n  await assertBackgroundImageContrast(page, '0');\n  await page.mouse.move(contrastMidX, contrastMidY);\n  await assertBackgroundImageContrast(page, '250');\n  await page.mouse.up();\n}\n\n/**\n * Gets the filter:brightness value of the current background-image and\n * asserts against an expected value\n * @param {import('@playwright/test').Page} page\n * @param {string} expected The expected brightness value\n */\nasync function assertBackgroundImageBrightness(page, expected) {\n  const backgroundImage = page.locator('.c-imagery__main-image__background-image');\n\n  // Get the brightness filter value (i.e: filter: brightness(500%) => \"500\")\n  const actual = await backgroundImage.evaluate((el) => {\n    return el.style.filter.match(/brightness\\((\\d{1,3})%\\)/)[1];\n  });\n  expect(actual).toBe(expected);\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function panZoomAndAssertImageProperties(page) {\n  await expect(page.locator('.c-imagery__hints')).toContainText(expectedAltText);\n  const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;\n  const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;\n\n  // Pan right\n  await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n  await page.mouse.down();\n  await page.mouse.move(imageCenterX - 200, imageCenterY, 10);\n  await page.mouse.up();\n  await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n  const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);\n\n  // Pan left\n  await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n  await page.mouse.down();\n  await page.mouse.move(imageCenterX, imageCenterY, 10);\n  await page.mouse.up();\n  await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n  const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);\n\n  // Pan up\n  await page.mouse.move(imageCenterX, imageCenterY);\n  await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n  await page.mouse.down();\n  await page.mouse.move(imageCenterX, imageCenterY + 200, 10);\n  await page.mouse.up();\n  await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n  const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);\n\n  // Pan down\n  await Promise.all(panHotkey.map((x) => page.keyboard.down(x)));\n  await page.mouse.down();\n  await page.mouse.move(imageCenterX, imageCenterY - 200, 10);\n  await page.mouse.up();\n  await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));\n  const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);\n}\n\n/**\n * Use the mouse wheel to zoom in or out of an image and assert that the image\n * has successfully zoomed in or out.\n * @param {import('@playwright/test').Page} page\n * @param {number} [factor = 2] The zoom factor. Positive for zoom in, negative for zoom out.\n */\nasync function mouseZoomOnImageAndAssert(page, factor = 2) {\n  // Zoom in\n  await page.getByLabel('Focused Image Element').hover({ trial: true });\n  const originalImageDimensions = await page.getByLabel('Focused Image Element').boundingBox();\n  await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * factor);\n  await waitForZoomAndPanTransitions(page);\n\n  const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n  const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;\n  const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;\n\n  // center the mouse pointer\n  await page.mouse.move(imageCenterX, imageCenterY);\n\n  // Wait for zoom animation to finish and get the new image dimensions\n  const imageMouseZoomed = await page.getByLabel('Focused Image Element').boundingBox();\n\n  if (factor > 0) {\n    expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height);\n    expect(imageMouseZoomed.width).toBeGreaterThan(originalImageDimensions.width);\n  } else {\n    expect(imageMouseZoomed.height).toBeLessThan(originalImageDimensions.height);\n    expect(imageMouseZoomed.width).toBeLessThan(originalImageDimensions.width);\n  }\n}\n\n/**\n * Zoom in and out of the image using the buttons, and assert that the image has\n * been successfully zoomed in or out.\n * @param {import('@playwright/test').Page} page\n */\nasync function buttonZoomOnImageAndAssert(page) {\n  await test.step('Can zoom using buttons', async () => {\n    // Lock the zoom and pan so it doesn't reset if a new image comes in\n    await page.getByLabel('Focused Image Element').hover({ trial: true });\n    const lockButton = page.getByRole('button', {\n      name: 'Lock current zoom and pan across all images'\n    });\n\n    await lockButton.isVisible();\n    // if (!(await lockButton.isVisible())) {\n    //   await page.getByLabel('Focused Image Element').hover({ trial: true });\n    // }\n    await lockButton.click();\n\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(1) translate(0px, 0px)'\n    );\n\n    // Get initial image dimensions\n    const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n\n    // Zoom in twice via button\n    await zoomIntoImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(2) translate(0px, 0px)'\n    );\n    await zoomIntoImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(3) translate(0px, 0px)'\n    );\n\n    // Get and assert zoomed in image dimensions\n    const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);\n    expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);\n\n    // Zoom out once via button\n    await zoomOutOfImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(2) translate(0px, 0px)'\n    );\n\n    // Get and assert zoomed out image dimensions\n    const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);\n    expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);\n\n    // Zoom out again via button, assert against the initial image dimensions\n    await zoomOutOfImageryByButton(page);\n    await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(\n      'style.transform',\n      'scale(1) translate(0px, 0px)'\n    );\n\n    const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();\n    expect(finalBoundingBox).toEqual(initialBoundingBox);\n  });\n}\n\n/**\n * Gets the filter:contrast value of the current background-image and\n * asserts against an expected value\n * @param {import('@playwright/test').Page} page\n * @param {string} expected The expected contrast value\n */\nasync function assertBackgroundImageContrast(page, expected) {\n  const backgroundImage = page.locator('.c-imagery__main-image__background-image');\n\n  // Get the contrast filter value (i.e: filter: contrast(500%) => \"500\")\n  const actual = await backgroundImage.evaluate((el) => {\n    return el.style.filter.match(/contrast\\((\\d{1,3})%\\)/)[1];\n  });\n  expect(actual).toBe(expected);\n}\n\n/**\n * Use the '+' button to zoom in. Hovers first if the toolbar is not visible\n * and waits for the zoom animation to finish afterwards.\n * @param {import('@playwright/test').Page} page\n */\nasync function zoomIntoImageryByButton(page) {\n  // FIXME: There should only be one set of imagery buttons, but there are two?\n  const zoomInBtn = page.getByRole('button', { name: 'Zoom in' });\n  const backgroundImage = page.getByLabel('Focused Image Element');\n  await backgroundImage.hover({ trial: true });\n  await zoomInBtn.click();\n  await waitForZoomAndPanTransitions(page);\n}\n\n/**\n * Use the '-' button to zoom out. Hovers first if the toolbar is not visible\n * and waits for the zoom animation to finish afterwards.\n * @param {import('@playwright/test').Page} page\n */\nasync function zoomOutOfImageryByButton(page) {\n  const zoomOutBtn = page.getByRole('button', { name: 'Zoom out' });\n  const backgroundImage = page.getByLabel('Focused Image Element');\n  await backgroundImage.hover({ trial: true });\n  await zoomOutBtn.click();\n  await waitForZoomAndPanTransitions(page);\n}\n\n/**\n * Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible\n * and waits for the zoom animation to finish afterwards.\n * @param {import('@playwright/test').Page} page\n */\nasync function resetImageryPanAndZoom(page) {\n  const panZoomResetBtn = page.getByRole('button', { name: 'Remove zoom and pan' });\n  await expect(panZoomResetBtn).toBeVisible();\n  await panZoomResetBtn.hover({ trial: true });\n  await panZoomResetBtn.click();\n\n  await waitForZoomAndPanTransitions(page);\n  await expect(page.getByText('Alt drag to pan')).toBeHidden();\n  await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function waitForZoomAndPanTransitions(page) {\n  // Wait for image to stabilize\n  await page.getByLabel('Focused Image Element').hover({ trial: true });\n  // Wait for zoom to end\n  await expect(page.getByLabel('Focused Image Element')).not.toHaveClass(/is-zooming|is-panning/);\n  // Wait for image to stabilize\n  await page.getByLabel('Focused Image Element').hover({ trial: true });\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/imagery/exampleImageryControlledClock.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to testing how imagery functions over time.\nIt only assumes that example imagery is present.\nIt uses https://playwright.dev/docs/clock to have control over time\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithRealTime,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { MISSION_TIME } from '../../../../constants.js';\nimport {\n  createImageryViewWithShortDelay,\n  FIVE_MINUTES,\n  IMAGE_LOAD_DELAY,\n  THIRTY_SECONDS\n} from '../../../../helper/imageryUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Example Imagery Object with Controlled Clock @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    // We mock the clock so that we don't need to wait for time driven events\n    // to verify functionality.\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a default 'Example Imagery' object\n    // Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click text=Example Imagery\n    await page.getByRole('menuitem', { name: 'Example Imagery' }).click();\n\n    // Clear and set Image load delay to minimum value\n    await page.locator('input[type=\"number\"]').clear();\n    await page.locator('input[type=\"number\"]').fill(`${IMAGE_LOAD_DELAY}`);\n\n    await page.getByLabel('Save').click();\n\n    // Verify that the created object is focused\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(\n      'Unnamed Example Imagery'\n    );\n    await page.getByLabel('Focused Image Element').hover({ trial: true });\n\n    // set realtime mode\n    await setRealTimeMode(page);\n    await setStartOffset(page, { startMins: '05' });\n  });\n\n  test('Imagery Time Bounding', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5265'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7825'\n    });\n\n    // verify that old images are discarded\n    const lastImageInBounds = page.getByLabel('Image thumbnail from').first();\n    const lastImageTimestamp = await lastImageInBounds.getAttribute('title');\n    expect(lastImageTimestamp).not.toBeNull();\n\n    // go forward in time to ensure old images are discarded\n    await page.clock.fastForward(IMAGE_LOAD_DELAY);\n    await page.clock.resume();\n    await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();\n\n    // go way forward in time to ensure multiple images are discarded\n    const IMAGES_TO_DISCARD_COUNT = 5;\n\n    const lastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT - 1);\n    const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');\n    expect(lastImageToDiscardTimestamp).not.toBeNull();\n\n    const imageAfterLastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT);\n    const imageAfterLastImageToDiscardTimestamp =\n      await imageAfterLastImageToDiscard.getAttribute('title');\n    expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();\n\n    await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);\n    await page.clock.resume();\n\n    await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();\n    await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();\n  });\n\n  test('Get background-image url from background-image css prop', async ({ page }) => {\n    await assertBackgroundImageUrlFromBackgroundCss(page);\n  });\n});\n\ntest.describe('Example Imagery in Display Layout with Controlled Clock @clock', () => {\n  let displayLayout;\n\n  test.beforeEach(async ({ page }) => {\n    // We mock the clock so that we don't need to wait for time driven events\n    // to verify functionality.\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });\n\n    // Create Example Imagery inside Display Layout\n    await createImageryViewWithShortDelay(page, {\n      name: 'Unnamed Example Imagery',\n      parent: displayLayout.uuid\n    });\n\n    // set realtime mode\n    await navigateToObjectWithRealTime(\n      page,\n      displayLayout.url,\n      `${FIVE_MINUTES}`,\n      `${THIRTY_SECONDS}`\n    );\n  });\n\n  test('Imagery Time Bounding', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5265'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7825'\n    });\n\n    // Edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click on example imagery to expose toolbar\n    await page.locator('.c-so-view__header').click();\n\n    // Adjust object height\n    await page.locator('div[title=\"Resize object height\"] > input').click();\n    await page.locator('div[title=\"Resize object height\"] > input').fill('50');\n\n    // Adjust object width\n    await page.locator('div[title=\"Resize object width\"] > input').click();\n    await page.locator('div[title=\"Resize object width\"] > input').fill('50');\n\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n\n    // verify that old images are discarded\n    const lastImageInBounds = page.getByLabel('Image thumbnail from').first();\n    const lastImageTimestamp = await lastImageInBounds.getAttribute('title');\n    expect(lastImageTimestamp).not.toBeNull();\n\n    // go forward in time to ensure old images are discarded\n    await page.clock.fastForward(IMAGE_LOAD_DELAY);\n    await page.clock.resume();\n    await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();\n\n    // go way forward in time to ensure multiple images are discarded\n    const IMAGES_TO_DISCARD_COUNT = 5;\n\n    const lastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT - 1);\n    const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');\n    expect(lastImageToDiscardTimestamp).not.toBeNull();\n\n    const imageAfterLastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT);\n    const imageAfterLastImageToDiscardTimestamp =\n      await imageAfterLastImageToDiscard.getAttribute('title');\n    expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();\n\n    await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);\n    await page.clock.resume();\n\n    await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();\n    await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();\n  });\n\n  test('Get background-image url from background-image css prop @clock', async ({ page }) => {\n    await assertBackgroundImageUrlFromBackgroundCss(page);\n  });\n});\n\ntest.describe('Example Imagery in Flexible layout with Controlled Clock @clock', () => {\n  let flexibleLayout;\n\n  test.beforeEach(async ({ page }) => {\n    // We mock the clock so that we don't need to wait for time driven events\n    // to verify functionality.\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });\n\n    // Create Example Imagery inside the Flexible Layout\n    await createImageryViewWithShortDelay(page, {\n      name: 'Unnamed Example Imagery',\n      parent: flexibleLayout.uuid\n    });\n\n    // set realtime mode\n    await navigateToObjectWithRealTime(\n      page,\n      flexibleLayout.url,\n      `${FIVE_MINUTES}`,\n      `${THIRTY_SECONDS}`\n    );\n\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n\n  test('Imagery Time Bounding @clock', async ({ page, browserName }) => {\n    test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5326'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7825'\n    });\n\n    // verify that old images are discarded\n    const lastImageInBounds = page.getByLabel('Image thumbnail from').first();\n    const lastImageTimestamp = await lastImageInBounds.getAttribute('title');\n    expect(lastImageTimestamp).not.toBeNull();\n\n    // go forward in time to ensure old images are discarded\n    await page.clock.fastForward(IMAGE_LOAD_DELAY);\n    await page.clock.resume();\n    await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();\n\n    // go way forward in time to ensure multiple images are discarded\n    const IMAGES_TO_DISCARD_COUNT = 5;\n\n    const lastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT - 1);\n    const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');\n    expect(lastImageToDiscardTimestamp).not.toBeNull();\n\n    const imageAfterLastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT);\n    const imageAfterLastImageToDiscardTimestamp =\n      await imageAfterLastImageToDiscard.getAttribute('title');\n    expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();\n\n    await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);\n    await page.clock.resume();\n\n    await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();\n    await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();\n  });\n\n  test('Get background-image url from background-image css prop @clock', async ({ page }) => {\n    await assertBackgroundImageUrlFromBackgroundCss(page);\n  });\n});\n\ntest.describe('Example Imagery in Tabs View with Controlled Clock @clock', () => {\n  let timeStripObject;\n\n  test.beforeEach(async ({ page }) => {\n    // We mock the clock so that we don't need to wait for time driven events\n    // to verify functionality.\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });\n    await page.goto(timeStripObject.url);\n\n    /* Create Example Imagery with minimum Image Load Delay */\n    // Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click text=Example Imagery\n    await page.getByRole('menuitem', { name: 'Example Imagery' }).click();\n\n    // Clear and set Image load delay to minimum value\n    await page.locator('input[type=\"number\"]').clear();\n    await page.locator('input[type=\"number\"]').fill(`${IMAGE_LOAD_DELAY}`);\n\n    await page.getByLabel('Save').click();\n\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(\n      'Unnamed Example Imagery'\n    );\n\n    // set realtime mode\n    await navigateToObjectWithRealTime(\n      page,\n      timeStripObject.url,\n      `${FIVE_MINUTES}`,\n      `${THIRTY_SECONDS}`\n    );\n\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n\n  test('Imagery Time Bounding @clock', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5265'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7825'\n    });\n\n    // verify that old images are discarded\n    const lastImageInBounds = page.getByLabel('Image thumbnail from').first();\n    const lastImageTimestamp = await lastImageInBounds.getAttribute('title');\n    expect(lastImageTimestamp).not.toBeNull();\n\n    // go forward in time to ensure old images are discarded\n    await page.clock.fastForward(IMAGE_LOAD_DELAY);\n    await page.clock.resume();\n    await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();\n\n    // go way forward in time to ensure multiple images are discarded\n    const IMAGES_TO_DISCARD_COUNT = 5;\n\n    const lastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT - 1);\n    const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');\n    expect(lastImageToDiscardTimestamp).not.toBeNull();\n\n    const imageAfterLastImageToDiscard = page\n      .getByLabel('Image thumbnail from')\n      .nth(IMAGES_TO_DISCARD_COUNT);\n    const imageAfterLastImageToDiscardTimestamp =\n      await imageAfterLastImageToDiscard.getAttribute('title');\n    expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();\n\n    await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);\n    await page.clock.resume();\n\n    await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();\n    await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();\n  });\n\n  test('Get background-image url from background-image css prop @clock', async ({ page }) => {\n    await assertBackgroundImageUrlFromBackgroundCss(page);\n  });\n});\n\ntest.describe('Example Imagery in Time Strip with Controlled Clock @clock', () => {\n  let timeStripObject;\n\n  test.beforeEach(async ({ page }) => {\n    // We mock the clock so that we don't need to wait for time driven events\n    // to verify functionality.\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });\n    await page.goto(timeStripObject.url);\n\n    /* Create Example Imagery with minimum Image Load Delay */\n    // Click the Create button\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Click text=Example Imagery\n    await page.getByRole('menuitem', { name: 'Example Imagery' }).click();\n\n    // Clear and set Image load delay to minimum value\n    await page.locator('input[type=\"number\"]').clear();\n    await page.locator('input[type=\"number\"]').fill(`${IMAGE_LOAD_DELAY}`);\n\n    await page.getByLabel('Save').click();\n\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(\n      'Unnamed Example Imagery'\n    );\n\n    // set realtime mode\n    await navigateToObjectWithRealTime(\n      page,\n      timeStripObject.url,\n      `${FIVE_MINUTES}`,\n      `${THIRTY_SECONDS}`\n    );\n\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('wrapper-').last()).toBeInViewport();\n  });\n\n  test('Imagery Time Bounding @clock', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5265'\n    });\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7825'\n    });\n\n    // verify that old images are discarded\n    const lastImageInBounds = page.getByLabel('wrapper-').first();\n    const lastImageTimestamp = await lastImageInBounds.getAttribute('aria-label');\n    expect(lastImageTimestamp).not.toBeNull();\n\n    // go forward in time to ensure old images are discarded\n    await page.clock.fastForward(IMAGE_LOAD_DELAY);\n    await page.clock.resume();\n    await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();\n\n    // go way forward in time to ensure multiple images are discarded\n    const IMAGES_TO_DISCARD_COUNT = 5;\n\n    const lastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT - 1);\n    const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('aria-label');\n    expect(lastImageToDiscardTimestamp).not.toBeNull();\n\n    const imageAfterLastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT);\n    const imageAfterLastImageToDiscardTimestamp =\n      await imageAfterLastImageToDiscard.getAttribute('aria-label');\n    expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();\n\n    await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);\n    await page.clock.resume();\n\n    await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();\n    await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function assertBackgroundImageUrlFromBackgroundCss(page) {\n  const backgroundImage = page.getByLabel('Focused Image Element');\n  const backgroundImageUrl = await backgroundImage.evaluate((el) => {\n    return window\n      .getComputedStyle(el)\n      .getPropertyValue('background-image')\n      .match(/url\\(([^)]+)\\)/)[1];\n  });\n\n  // go forward in time to ensure old images are discarded\n  await page.clock.fastForward(IMAGE_LOAD_DELAY);\n  await page.clock.resume();\n  await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/imagery/exampleImageryFile.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite verifies modifying the image location of the example imagery object.\n */\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Example Imagery Object Custom Images', () => {\n  let exampleImagery;\n  test.beforeEach(async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a default 'Example Imagery' object\n    exampleImagery = await createDomainObjectWithDefaults(page, {\n      name: 'Example Imagery',\n      type: 'Example Imagery'\n    });\n\n    // Verify that the created object is focused\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);\n    await page.getByLabel('Focused Image Element').hover({ trial: true });\n\n    // Wait for image thumbnail auto-scroll to complete\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n  });\n  // this requires CORS to be enabled in some fashion\n  test.fixme('Can right click on image and save it as a file', async ({ page }) => {});\n  test('Can provide a custom image location for the example imagery object', async ({ page }) => {\n    // Modify Example Imagery to create a really stable image which will never let us down\n    await page.getByRole('button', { name: 'More actions' }).click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page\n      .locator('#imageLocation-textarea')\n      .fill(\n        'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'\n      );\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Wait for the thumbnails to finish their scroll animation\n    // (Wait until the rightmost thumbnail is in view)\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n\n    await expect(page.getByLabel('Image Wrapper')).toBeVisible();\n  });\n  test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7903'\n    });\n    await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });\n\n    // Modify Example Imagery to create a really stable image which will never let us down\n    await page.getByRole('button', { name: 'More actions' }).click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page\n      .locator('#imageLocation-textarea')\n      .fill(\n        'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'\n      );\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Wait for the thumbnails to finish their scroll animation\n    // (Wait until the rightmost thumbnail is in view)\n    await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();\n\n    await expect(page.getByLabel('Image Wrapper')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.\n*/\n\nimport fs from 'fs/promises';\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../baseFixtures.js';\nimport { navigateToFaultManagementWithExample } from '../../../../helper/faultUtils.js';\n\ntest.describe('ExportAsJSON', () => {\n  let folder;\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./');\n    // Perform actions to create the domain object\n    folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'e2e folder'\n    });\n  });\n  test('Create a basic object and verify that it can be exported as JSON from Tree', async ({\n    page\n  }) => {\n    // Navigate to the page\n    await page.goto(folder.url);\n\n    // Open context menu and initiate download\n    await page.getByLabel('Show selected item in tree').click();\n    await page.getByRole('treeitem', { name: 'Expand e2e folder folder' }).click({\n      button: 'right'\n    });\n    const [download] = await Promise.all([\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as JSON').click() // Triggers the download\n    ]);\n\n    // Wait for the download process to complete\n    const path = await download.path();\n\n    // Read the contents of the downloaded file using readFile from fs/promises\n    const fileContents = await fs.readFile(path, 'utf8');\n    const jsonData = JSON.parse(fileContents);\n\n    // Use the function to retrieve the key\n    const key = getFirstKeyFromOpenMctJson(jsonData);\n\n    // Verify the contents of the JSON file\n    expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');\n    expect(jsonData.openmct[key]).toHaveProperty('type', 'folder');\n  });\n  test('Create a basic object and verify that it can be exported as JSON from 3 dot menu', async ({\n    page\n  }) => {\n    // Navigate to the page\n    await page.goto(folder.url);\n    //3 dot menu\n    await page.getByLabel('More actions').click();\n    // Open context menu and initiate download\n    const [download] = await Promise.all([\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as JSON').click() // Triggers the download\n    ]);\n\n    // Read the contents of the downloaded file using readFile from fs/promises\n    const fileContents = await fs.readFile(await download.path(), 'utf8');\n    const jsonData = JSON.parse(fileContents);\n\n    // Use the function to retrieve the key\n    const key = getFirstKeyFromOpenMctJson(jsonData);\n\n    // Verify the contents of the JSON file\n    expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');\n    expect(jsonData.openmct[key]).toHaveProperty('type', 'folder');\n  });\n  test('Verify that a nested Object can be exported as JSON', async ({ page }) => {\n    const timer = await createDomainObjectWithDefaults(page, {\n      type: 'Timer',\n      name: 'timer',\n      parent: folder.uuid\n    });\n    // Navigate to the page\n    await page.goto(timer.url);\n\n    //do this against parent folder.url, NOT timer.url child\n    // Open context menu and initiate download\n    await page.getByLabel('Show selected item in tree').click();\n    await page.getByRole('treeitem', { name: 'Collapse e2e folder folder' }).click({\n      button: 'right'\n    });\n\n    // Open context menu and initiate download\n    const [download] = await Promise.all([\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as JSON').click() // Triggers the download\n    ]);\n\n    // Read the contents of the downloaded file\n    const fileContents = await fs.readFile(await download.path(), 'utf8');\n    const jsonData = JSON.parse(fileContents);\n\n    // Retrieve the keys for folder and timer\n    const folderKey = getFirstKeyFromOpenMctJson(jsonData);\n    const timerKey = jsonData.openmct[folderKey].composition[0].key;\n\n    // Verify the folder properties\n    expect(jsonData.openmct[folderKey]).toHaveProperty('name', 'e2e folder');\n    expect(jsonData.openmct[folderKey]).toHaveProperty('type', 'folder');\n\n    // Verify the timer properties\n    expect(jsonData.openmct[timerKey]).toHaveProperty('name', 'timer');\n    expect(jsonData.openmct[timerKey]).toHaveProperty('type', 'timer');\n\n    // Verify the composition of the folder includes the timer\n    expect(jsonData.openmct[folderKey].composition).toEqual(\n      expect.arrayContaining([expect.objectContaining({ key: timerKey })])\n    );\n  });\n});\ntest.describe('ExportAsJSON Disabled Actions', () => {\n  test.beforeEach(async ({ page }) => {\n    //Use a Fault Management Object which is not composable\n    await navigateToFaultManagementWithExample(page);\n  });\n  test('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => {\n    await page.getByLabel('More actions').click();\n    await expect(page.getByLabel('Export as JSON')).toHaveCount(0);\n\n    await page.getByRole('treeitem', { name: 'Fault Management' }).click({\n      button: 'right'\n    });\n    await expect(page.getByLabel('Export as JSON')).toHaveCount(0);\n  });\n});\ntest.describe('ExportAsJSON ProgressBar @couchdb', () => {\n  let folder;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Perform actions to create the domain object\n    folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Timer',\n      parent: folder.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Timer',\n      parent: folder.uuid\n    });\n  });\n  test('Verify that the ExportAsJSON action creates a progressbar', async ({ page }) => {\n    // Navigate to the page\n    await page.goto(folder.url);\n\n    //Export My Items to create a large export\n    await page.getByRole('treeitem', { name: 'My Items' }).click({ button: 'right' });\n    // Open context menu and initiate download\n    await Promise.all([\n      page.getByRole('progressbar'), // This is just a check for the progress bar\n      page.getByText(\n        'Do not navigate away from this page or close this browser tab while this message'\n      ), // This is the text associated with the download\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as JSON').click() // Triggers the download\n    ]);\n  });\n});\n\n/**\n * Retrieves the first key from the 'openmct' property of the provided JSON object.\n *\n * @param {Object} jsonData - The JSON object containing the 'openmct' property.\n * @returns {string} The first key found in the 'openmct' object.\n * @throws {Error} If no keys are found in the 'openmct' object.\n */\nfunction getFirstKeyFromOpenMctJson(jsonData) {\n  if (!jsonData.openmct) {\n    throw new Error(\"The provided JSON object does not have an 'openmct' property.\");\n  }\n\n  const keys = Object.keys(jsonData.openmct);\n  if (keys.length === 0) {\n    throw new Error('No keys found in the openmct object');\n  }\n\n  return keys[0];\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.\n*/\n\n// FIXME: Remove this eslint exception once tests are implemented\n// eslint-disable-next-line no-unused-vars\nimport { expect, test } from '../../../../baseFixtures.js';\n\ntest.describe('ExportAsJSON', () => {\n  test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {\n    //Verify that an testdata JSON file can be imported from Tree\n    //Verify correctness of imported domain object\n  });\n  test.fixme(\n    'Verify that domain object can be importAsJSON from 3 dot menu on folder',\n    async ({ page }) => {\n      //Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object\n      //Verify correctness of imported domain object\n    }\n  );\n  test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => {\n    // Testdata with hierarchy\n    // ImportAsJSON on Tree\n    // Verify Hierarchy\n  });\n  test.fixme(\n    'Verify that the ImportAsJSON dropdown does not appear for the item X',\n    async ({ page }) => {\n      // Other than non-persistable objects\n    }\n  );\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/inspectorDataVisualization/numericData.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { fileURLToPath } from 'url';\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Testing numeric data with inspector data visualization (i.e., data pivoting)', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(\n        new URL('../../../../helper/addInitDataVisualization.js', import.meta.url)\n      )\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {\n    const initStartBounds = page.getByLabel('Start bounds');\n    const initEndBounds = await page.getByLabel('End bounds').textContent();\n    const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {\n      type: 'Example Data Visualization Source'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'First Sine Wave Generator',\n      parent: exampleDataVisualizationSource.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Second Sine Wave Generator',\n      parent: exampleDataVisualizationSource.uuid\n    });\n\n    await page.goto(exampleDataVisualizationSource.url);\n\n    await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();\n    await page.getByRole('tab', { name: 'Data Visualization' }).click();\n    await expect(page.getByText('Numeric Data')).toBeVisible();\n    await expect(\n      page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })\n    ).toBeVisible();\n    await expect(page.locator('.js-series-data-loaded')).toBeVisible();\n\n    await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();\n    await page.getByRole('tab', { name: 'Data Visualization' }).click();\n    await expect(page.getByText('Numeric Data')).toBeVisible();\n    await expect(\n      page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })\n    ).toBeVisible();\n    await expect(page.locator('.js-series-data-loaded')).toBeVisible();\n\n    // test new tab\n    await page.getByLabel('Inspector Views').getByLabel('More actions').click();\n    const pagePromise = context.waitForEvent('page');\n    await page.getByRole('menuitem', { name: /Open In New Tab/ }).click();\n\n    // ensure our new tab's title is correct\n    const newPage = await pagePromise;\n    await newPage.waitForLoadState();\n    await page.getByRole('tab', { name: 'Data Visualization' }).click();\n\n    // expect new tab title to contain 'Second Sine Wave Generator'\n    await expect(newPage).toHaveTitle('Second Sine Wave Generator');\n\n    // Verify that \"Open in New Tab\" preserves the time bounds\n    await expect(initStartBounds).toHaveText(\n      await newPage.getByLabel('Start bounds').textContent()\n    );\n    expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/lad/lad.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  getNextSineValueFromSWG,\n  navigateToObjectWithRealTime,\n  setFixedTimeMode,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Testing LAD table configuration', () => {\n  let ladTable;\n  let sineWaveObject;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create LAD table\n    ladTable = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      name: 'Test LAD Table'\n    });\n\n    // Create Sine Wave Generator\n    sineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Test Sine Wave Generator',\n      parent: ladTable.uuid\n    });\n\n    await page.goto(ladTable.url);\n  });\n  test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // make sure headers are visible initially\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // hide timestamp column\n    await page.getByLabel('Timestamp', { exact: true }).uncheck();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // hide units & type column\n    await page.getByLabel('Units').uncheck();\n    await page.getByLabel('Type', { exact: true }).uncheck();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // hide WATCH column\n    await page.getByLabel('WATCH').uncheck();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // save and reload and verify they columns are still hidden\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await page.reload();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // show timestamp column\n    await page.getByLabel('Timestamp', { exact: true }).check();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // save and reload and make sure timestamp is still visible\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await page.reload();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // show units, type, and WATCH columns\n    await page.getByLabel('Units').check();\n    await page.getByLabel('Type', { exact: true }).check();\n    await page.getByLabel('WATCH').check();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // save and reload and make sure all columns are still visible\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await page.reload();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n  });\n\n  test('When adding something without Units, do not show Units column', async ({ page }) => {\n    // Create Sine Wave Generator\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: ladTable.uuid\n    });\n\n    await page.goto(ladTable.url);\n\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // make sure Sine Wave headers are visible initially too\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();\n\n    // save and reload and verify they columns are still hidden\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Remove Sine Wave Generator\n    openObjectTreeContextMenu(page, sineWaveObject.url);\n    await page.getByRole('menuitem', { name: /Remove/ }).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Ensure Units & Limit columns are gone\n    // as Event Generator don't have them\n    await page.goto(ladTable.url);\n    await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeHidden();\n    await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeHidden();\n  });\n\n  test(\"LAD Tables don't allow selection of rows but does show context click menus\", async ({\n    page\n  }) => {\n    const cell = page.locator('.js-first-data');\n    const userSelectable = await cell.evaluate((el) => {\n      return window.getComputedStyle(el).getPropertyValue('user-select');\n    });\n\n    expect(userSelectable).toBe('none');\n    // Right-click on the LAD table row\n    await cell.click({\n      button: 'right'\n    });\n    const menuOptions = page.locator('.c-menu ul');\n    await expect.soft(menuOptions).toContainText('View Full Datum');\n    await expect.soft(menuOptions).toContainText('View Historical Data');\n    await expect.soft(menuOptions).toContainText('Remove');\n    // await page.locator('li[title=\"Remove this object from its containing object.\"]').click();\n  });\n});\n\ntest.describe('Testing LAD table', () => {\n  let sineWaveObject;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Sine Wave Generator\n    sineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Test Sine Wave Generator'\n    });\n\n    // Switch to real time mode by navigating directly to the URL\n    await navigateToObjectWithRealTime(page, sineWaveObject.url);\n  });\n  test('telemetry value exactly matches latest telemetry value received in realtime mode', async ({\n    page\n  }) => {\n    // Create LAD table\n    await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      name: 'Test LAD Table'\n    });\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.getByLabel('Expand My Items').click();\n    // Add the Sine Wave Generator to the LAD table and save changes\n    await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area'));\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Subscribe to the Sine Wave Generator data\n    // On getting data, check if the value found in the LAD table is the most recent value\n    // from the Sine Wave Generator\n    const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);\n    const subscribeTelemValue = await getTelemValuePromise;\n    await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue);\n    const ladTableValue = await page.getByText(subscribeTelemValue).textContent();\n\n    expect(ladTableValue).toEqual(subscribeTelemValue);\n  });\n  test('telemetry value exactly matches latest telemetry value received in fixed time mode', async ({\n    page\n  }) => {\n    // Create LAD table\n    await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      name: 'Test LAD Table'\n    });\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.getByLabel('Expand My Items').click();\n    // Add the Sine Wave Generator to the LAD table and save changes\n    await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area'));\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Subscribe to the Sine Wave Generator data\n    const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);\n    // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window\n    await setRealTimeMode(page);\n    await setStartOffset(page, { startMins: '01' });\n    await setFixedTimeMode(page);\n\n    // On getting data, check if the value found in the LAD table is the most recent value\n    // from the Sine Wave Generator\n    const subscribeTelemValue = await getTelemValuePromise;\n    await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue);\n  });\n});\n\n/**\n * Open the given `domainObject`'s context menu from the object tree.\n * Expands the path to the object and scrolls to it if necessary.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url the url to the object\n */\nasync function openObjectTreeContextMenu(page, url) {\n  await page.goto(url);\n  await page.getByLabel('Show selected item in tree').click();\n  await page.locator('.is-navigated-object').click({\n    button: 'right'\n  });\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/lad/ladSet.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('LAD Table Sets', () => {\n  test('Ensure we have numbers in cells', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const ladTableSet = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table Set'\n    });\n\n    const firstLadTable = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      parent: ladTableSet.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: firstLadTable.uuid\n    });\n\n    const secondLadTable = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      parent: ladTableSet.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: secondLadTable.uuid\n    });\n\n    await page.goto(ladTableSet.url);\n\n    // Wait for the initial value to show after mount\n    await expect(page.getByLabel('lad value').first()).not.toContainText('---');\n\n    const valueFromFirstSineWave = await page.getByLabel('lad value').first().innerText();\n    const firstSineWaveNumber = parseFloat(valueFromFirstSineWave);\n    // ensure we have a float value in the cell and it's finite\n    expect(Number.isFinite(firstSineWaveNumber)).toBeTruthy();\n\n    const valueFromSecondSineWave = await page.getByLabel('lad value').last().innerText();\n    const secondSineWaveNumber = parseFloat(valueFromSecondSineWave);\n    // ensure we have a float value in the cell and it's finite\n    expect(Number.isFinite(secondSineWaveNumber)).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/lad/ladTable.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('LAD Table', () => {\n  let ladTable;\n  let swg;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    ladTable = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table'\n    });\n\n    swg = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: ladTable.uuid\n    });\n\n    await page.goto(ladTable.url);\n  });\n\n  test('Ensure we have numbers in cells', async ({ page }) => {\n    // Wait for the initial value to show after mount\n    await expect(page.getByLabel('lad value').first()).not.toContainText('---');\n\n    const valueFromFirstSineWave = await page.getByLabel('lad value').first().innerText();\n    const firstSineWaveNumber = parseFloat(valueFromFirstSineWave);\n    // ensure we have a float value in the cell and it's finite\n    expect(Number.isFinite(firstSineWaveNumber)).toBeTruthy();\n\n    const valueFromSecondSineWave = await page.getByLabel('lad value').last().innerText();\n    const secondSineWaveNumber = parseFloat(valueFromSecondSineWave);\n    // ensure we have a float value in the cell and it's finite\n    expect(Number.isFinite(secondSineWaveNumber)).toBeTruthy();\n  });\n\n  test(\n    'Can remove telemetry from composition',\n    {\n      annotation: {\n        type: 'issue',\n        description: 'https://github.com/nasa/openmct/issues/7633'\n      }\n    },\n    async ({ page }) => {\n      // Assert that the table is initially populated\n      await expect(page.getByLabel('lad row')).toHaveCount(1);\n\n      // Expand the tree so the SWG is visible\n      await page.getByLabel('Expand My Items').click();\n      await page.getByLabel('Expand LAD Table').click();\n\n      // Right-click the SWG treeitem context menu and click 'Remove' and confirm\n      await page.getByRole('treeitem', { name: swg.name }).click({ button: 'right' });\n      await page.getByRole('menuitem', { name: 'Remove' }).click();\n      await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n      // Assert that the SWG is no longer in the tree and the table is empty\n      await expect(page.getByRole('treeitem', { name: swg.name })).toBeHidden();\n      await expect(page.getByLabel('lad row')).toHaveCount(0);\n    }\n  );\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding Notebooks.\n*/\n\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  renameCurrentObjectFromBrowseBar\n} from '../../../../appActions.js';\nimport { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';\nimport * as nbUtils from '../../../../helper/notebookUtils.js';\nimport { expect, streamToString, test } from '../../../../pluginFixtures.js';\n\nconst NOTEBOOK_NAME = 'Notebook';\n\ntest.describe('Notebook CRUD Operations', () => {\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n  test('Can create a Notebook Object', async ({ page }) => {\n    //Create domain object\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n    //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'\n    const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name');\n    const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name');\n    await expect(notebookSectionNames).toBeHidden();\n    await expect(notebookPageNames).toBeHidden();\n    await expect(notebookSectionNames).toHaveText('Unnamed Section');\n    await expect(notebookPageNames).toHaveText('Unnamed Page');\n  });\n  test.fixme('Can update a Notebook Object', async ({ page }) => {});\n  test.fixme('Can view a previously created Notebook Object', async ({ page }) => {});\n  test.fixme('Can Delete a Notebook Object', async ({ page }) => {\n    // Other than non-persistable objects\n  });\n});\n\ntest.describe('Default Notebook', () => {\n  // General Default Notebook statements\n  // ## Useful commands:\n  // 1.  - To check default notebook:\n  //     `JSON.parse(localStorage.getItem('notebook-storage'));`\n  // 1.  - Clear default notebook:\n  //     `localStorage.setItem('notebook-storage', null);`\n  test.fixme(\n    'A newly created Notebook is automatically set as the default notebook if no other notebooks exist',\n    async ({ page }) => {\n      //Create new notebook\n      //Verify Default Notebook Characteristics\n    }\n  );\n  test.fixme(\n    'A newly created Notebook is automatically set as the default notebook if at least one other notebook exists',\n    async ({ page }) => {\n      //Create new notebook A\n      //Create second notebook B\n      //Verify Non-Default Notebook A Characteristics\n      //Verify Default Notebook B Characteristics\n    }\n  );\n  test.fixme(\n    'If a default notebook is deleted, the second most recent notebook becomes the default',\n    async ({ page }) => {\n      //Create new notebook A\n      //Create second notebook B\n      //Delete Notebook B\n      //Verify Default Notebook A Characteristics\n    }\n  );\n});\n\ntest.describe('Notebook section tests', () => {\n  //The following test cases are associated with Notebook Sections\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Notebook\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n  test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({\n    page\n  }) => {\n    const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name');\n    const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name');\n    await expect(notebookSectionNames).toBeHidden();\n    await expect(notebookPageNames).toBeHidden();\n    // Expand sidebar\n    await page.locator('.c-notebook__toggle-nav-button').click();\n    // Check that the default section and page are created and the name matches the defaults\n    const defaultSectionName = await notebookSectionNames.innerText();\n    await expect(notebookSectionNames).toBeVisible();\n    expect(defaultSectionName).toBe('Unnamed Section');\n    const defaultPageName = await notebookPageNames.innerText();\n    await expect(notebookPageNames).toBeVisible();\n    expect(defaultPageName).toBe('Unnamed Page');\n\n    // Add a section\n    await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click();\n\n    // Check that new section and page within the new section match the defaults\n    const newSectionName = await notebookSectionNames.nth(1).innerText();\n    await expect(notebookSectionNames.nth(1)).toBeVisible();\n    expect(newSectionName).toBe('Unnamed Section');\n    const newPageName = await notebookPageNames.innerText();\n    await expect(notebookPageNames).toBeVisible();\n    expect(newPageName).toBe('Unnamed Page');\n  });\n  test.fixme('Section selection operations and associated behavior', async ({ page }) => {\n    //Create new notebook A\n    //Add Sections until 6 total with no default section/page\n    //Select 3rd section\n    //Delete 4th section\n    //3rd section is still selected\n    //Delete 3rd section\n    //1st section is selected\n    //Set 3rd section as default\n    //Delete 2nd section\n    //3rd section is still default\n    //Delete 3rd section\n    //1st is selected and there is no default notebook\n  });\n  test.fixme('Section rename operations', async ({ page }) => {\n    // Create a new notebook\n    // Add a section\n    // Rename the section but do not confirm\n    // Keyboard press 'Escape'\n    // Verify that the section name reverts to the default name\n    // Rename the section but do not confirm\n    // Keyboard press 'Enter'\n    // Verify that the section name is updated\n    // Rename the section to \"\" (empty string)\n    // Keyboard press 'Enter' to confirm\n    // Verify that the section name reverts to the default name\n    // Rename the section to something long that overflows the text box\n    // Verify that the section name is not truncated while input is active\n    // Confirm the section name edit\n    // Verify that the section name is truncated now that input is not active\n  });\n});\n\ntest.describe('Notebook page tests', () => {\n  //The following test cases are associated with Notebook Pages\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Notebook\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n  //Test will need to be implemented after a refactor in #5713\n  // eslint-disable-next-line playwright/no-skipped-test\n  test.skip('Delete page popup is removed properly on clicking dropdown again', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5713'\n    });\n    // Expand sidebar and add a second page\n    await page.locator('.c-notebook__toggle-nav-button').click();\n    await page.locator('text=Page Add >> button').click();\n\n    // Click on the 2nd page dropdown button and expect the Delete Page option to appear\n    await page.locator('button[title=\"Open context menu\"]').nth(2).click();\n    await expect(page.locator('text=Delete Page')).toBeEnabled();\n    // Clicking on the same page a second time causes the same Delete Page option to recreate\n    await page.locator('button[title=\"Open context menu\"]').nth(2).click();\n    await expect(page.locator('text=Delete Page')).toBeEnabled();\n    // Clicking on the first page causes the first delete button to detach and recreate on the first page\n    await page.locator('button[title=\"Open context menu\"]').nth(1).click();\n    const numOfDeletePagePopups = await page.locator('li[title=\"Delete Page\"]').count();\n    expect(numOfDeletePagePopups).toBe(1);\n  });\n  test.fixme('Page selection operations and associated behavior', async ({ page }) => {\n    //Create new notebook A\n    //Delete existing Page\n    //New 'Unnamed Page' automatically created\n    //Create 6 total Pages without a default page\n    //Select 3rd\n    //Delete 3rd\n    //First is now selected\n    //Set 3rd as default\n    //Select 2nd page\n    //Delete 2nd page\n    //3rd (default) is now selected\n    //Set 3rd as default page\n    //Select 3rd (default) page\n    //Delete 3rd page\n    //First is now selected and there is no default notebook\n  });\n  test.fixme('Page rename operations', async ({ page }) => {\n    // Create a new notebook\n    // Add a page\n    // Rename the page but do not confirm\n    // Keyboard press 'Escape'\n    // Verify that the page name reverts to the default name\n    // Rename the page but do not confirm\n    // Keyboard press 'Enter'\n    // Verify that the page name is updated\n    // Rename the page to \"\" (empty string)\n    // Keyboard press 'Enter' to confirm\n    // Verify that the page name reverts to the default name\n    // Rename the page to something long that overflows the text box\n    // Verify that the page name is not truncated while input is active\n    // Confirm the page name edit\n    // Verify that the page name is truncated now that input is not active\n  });\n});\n\ntest.describe('Notebook export tests', () => {\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Notebook\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n  test('can export notebook as text', async ({ page }) => {\n    await nbUtils.enterTextEntry(page, `Foo bar entry`);\n    // Click on 3 Dot Menu\n    await page.locator('button[title=\"More actions\"]').click();\n    const downloadPromise = page.waitForEvent('download');\n\n    await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    const download = await downloadPromise;\n    const readStream = await download.createReadStream();\n    const exportedText = await streamToString(readStream);\n    expect(exportedText).toContain('Foo bar entry');\n  });\n  test.fixme('can export multiple notebook entries as text', async ({ page }) => {});\n  test.fixme('can export all notebook entry metdata', async ({ page }) => {});\n  test.fixme('can export all notebook tags', async ({ page }) => {});\n  test.fixme('can export all notebook snapshots', async ({ page }) => {});\n});\n\ntest.describe('Notebook search tests', () => {\n  test.fixme('Can search for a single result', async ({ page }) => {});\n  test.fixme('Can search for many results', async ({ page }) => {});\n  test.fixme('Can search for new and recently modified entries', async ({ page }) => {});\n  test.fixme('Can search for section text', async ({ page }) => {});\n  test.fixme('Can search for page text', async ({ page }) => {});\n  test.fixme('Can search for entry text', async ({ page }) => {});\n});\n\ntest.describe('Notebook entry tests', () => {\n  // Create Notebook with URL Whitelist\n  let notebookObject;\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../../../helper/addInitNotebookWithUrls.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    notebookObject = await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n  test('When a new entry is created, it should be focused and selected', async ({ page }) => {\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Click .c-notebook__drag-area\n    await page.locator('.c-notebook__drag-area').click();\n    await expect(page.getByLabel('Notebook Entry Input')).toBeVisible();\n    await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/);\n  });\n  test('When an object is dropped into a notebook, a new entry is created and it should be focused', async ({\n    page\n  }) => {\n    // Create Overlay Plot\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await page\n      .getByRole('treeitem', { name: overlayPlot.name })\n      .dragTo(page.locator('.c-notebook__drag-area'));\n\n    const embed = page.locator('.c-ne__embed__link');\n    const embedName = await embed.innerText();\n\n    await expect(embed).toHaveClass(/icon-plot-overlay/);\n    expect(embedName).toBe(overlayPlot.name);\n  });\n  test('When an object is dropped into a notebooks existing entry, it should be focused', async ({\n    page\n  }) => {\n    // Create Overlay Plot\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, 'Entry to drop into');\n    await page\n      .getByRole('treeitem', { name: overlayPlot.name })\n      .dragTo(page.locator('text=Entry to drop into'));\n\n    const existingEntry = page.locator('.c-ne__content', {\n      has: page.locator('text=\"Entry to drop into\"')\n    });\n    const embed = existingEntry.locator('.c-ne__embed__link');\n    const embedName = await embed.innerText();\n\n    await expect(embed).toHaveClass(/icon-plot-overlay/);\n    expect(embedName).toBe(overlayPlot.name);\n  });\n  test.fixme('new entries persist through navigation events without save', async ({ page }) => {});\n  test('previous and new entries can be deleted', async ({ page }) => {\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    await nbUtils.enterTextEntry(page, 'First Entry');\n    await page.getByLabel('Notebook Entry', { exact: true }).hover();\n    await page.getByLabel('Delete this entry').click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    await expect(page.getByText('First Entry')).toBeHidden();\n    await nbUtils.enterTextEntry(page, 'Another First Entry');\n    await nbUtils.enterTextEntry(page, 'Second Entry');\n    await nbUtils.enterTextEntry(page, 'Third Entry');\n    await page.getByLabel('Notebook Entry', { exact: true }).nth(2).hover();\n    await page.getByLabel('Delete this entry').nth(2).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    await expect(page.getByText('Third Entry')).toBeHidden();\n    await expect(page.getByText('Another First Entry')).toBeVisible();\n    await expect(page.getByText('Second Entry')).toBeVisible();\n  });\n  test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({\n    page\n  }) => {\n    const TEST_LINK = 'http://www.google.com';\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);\n\n    const validLink = page.locator(`a[href=\"${TEST_LINK}\"]`);\n\n    await expect(validLink).toHaveCount(1);\n\n    // Start waiting for popup before clicking. Note no await.\n    const popupPromise = page.waitForEvent('popup');\n\n    await validLink.click();\n    const popup = await popupPromise;\n\n    // Wait for the popup to load.\n    await popup.waitForLoadState();\n    expect.soft(popup.url()).toContain('www.google.com');\n  });\n  test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({\n    page\n  }) => {\n    const TEST_LINK = 'www.google.com';\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);\n\n    const invalidLink = page.locator(`a[href=\"${TEST_LINK}\"]`);\n\n    await expect(invalidLink).toHaveCount(0);\n  });\n  test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({\n    page\n  }) => {\n    const TEST_LINK = 'http://www.bing.com';\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);\n\n    const invalidLink = page.locator(`a[href=\"${TEST_LINK}\"]`);\n\n    await expect(invalidLink).toHaveCount(0);\n  });\n  test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({\n    page\n  }) => {\n    const INVALID_TEST_LINK = 'http://bing.google.com';\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);\n\n    const validLink = page.locator(`a[href=\"${INVALID_TEST_LINK}\"]`);\n\n    await expect(validLink).toHaveCount(1);\n  });\n  test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({\n    page\n  }) => {\n    const TEST_LINK = 'https://www.google.com';\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);\n\n    const validLink = page.locator(`a[href=\"${TEST_LINK}\"]`);\n\n    await expect(validLink).toHaveCount(1);\n\n    // Start waiting for popup before clicking. Note no await.\n    const popupPromise = page.waitForEvent('popup');\n\n    await validLink.click();\n    const popup = await popupPromise;\n\n    // Wait for the popup to load.\n    await popup.waitForLoadState();\n    expect.soft(popup.url()).toContain('www.google.com');\n  });\n  test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({\n    page\n  }) => {\n    const TEST_LINK = 'http://www.google.com?bad=';\n    const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`;\n\n    // Navigate to the notebook object\n    await page.goto(notebookObject.url);\n\n    // Reveal the notebook in the tree\n    await page.getByLabel('Show selected item in tree').click();\n\n    await nbUtils.enterTextEntry(\n      page,\n      `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`\n    );\n\n    const sanitizedLink = page.locator(`a[href=\"${TEST_LINK}\"]`);\n    const unsanitizedLink = page.locator(`a[href=\"${TEST_LINK_BAD}\"]`);\n\n    expect.soft(await sanitizedLink.count()).toBe(1);\n    await expect(unsanitizedLink).toHaveCount(0);\n  });\n  test('Can add markdown to a notebook entry', async ({ page }) => {\n    await page.goto(notebookObject.url);\n\n    // Headers\n    const headerMarkdown = `# Big Header\\n## Large Header\\n### Medium Header\\n#### Small Header`;\n    await nbUtils.enterTextEntry(page, headerMarkdown);\n    await expect(page.getByRole('heading', { name: 'Big Header' })).toBeVisible();\n\n    // Text markup\n    const markupText =\n      '**This is bold.** _This is italic_. `This is code`. ~This is strikethrough~';\n    await nbUtils.enterTextEntry(page, markupText);\n    await expect(page.locator('strong:has-text(\"This is bold.\")')).toBeVisible();\n\n    // Tables\n    const tablesText = '|Col 1|Col 2|Col3|\\n|-|-|-|\\n |Value 1|Value 2|Value 3|\\n';\n    await nbUtils.enterTextEntry(page, tablesText);\n    await expect(page.getByRole('cell', { name: 'Value 2' })).toBeVisible();\n\n    // Links\n    const linksText =\n      'Raw links https://www.google.com and Markdown links like [Google](https://www.google.com) work';\n    await nbUtils.enterTextEntry(page, linksText);\n    await expect(page.getByRole('link', { name: 'https://www.google.com' })).toBeVisible();\n    await expect(page.getByRole('link', { name: 'Google', exact: true })).toBeVisible();\n\n    // Lists\n    const listsText = '- List item 1\\n   - Item 1A \\n- List Item 2\\n  1. Order 1\\n  1. Order 2\\n';\n    await nbUtils.enterTextEntry(page, listsText);\n    const childItem = page.locator('li:has-text(\"List Item 2\") ol li:has-text(\"Order 2\")');\n    await expect(childItem).toBeVisible();\n\n    // Code Blocks\n    const codeblockTest = '```javascript\\nconst foo = \"bar\";\\nconst bar = \"foo\";\\n```';\n    await nbUtils.enterTextEntry(page, codeblockTest);\n    const codeBlock = page.locator('code.language-javascript:has-text(\"const foo = \\\\\"bar\\\\\";\")');\n    await expect(codeBlock).toBeVisible();\n\n    // Blockquotes\n    const blockquoteTest =\n      'This is a quote by Mark Twain:\\n> \"The man with a new idea is a crank\\n>until the idea succeeds.\"';\n    await nbUtils.enterTextEntry(page, blockquoteTest);\n    const firstLineOfBlockquoteText = page.locator(\n      'blockquote:has-text(\"The man with a new idea is a crank\")'\n    );\n    await expect(firstLineOfBlockquoteText).toBeVisible();\n    const secondLineOfBlockquoteText = page.locator(\n      'blockquote:has-text(\"until the idea succeeds\")'\n    );\n    await expect(secondLineOfBlockquoteText).toBeVisible();\n  });\n\n  /**\n   *  Paste into notebook entry tests\n   */\n  test('Can paste text into a notebook entry', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7686'\n    });\n    const TEST_TEXT = 'This is a test';\n    const iterations = 20;\n    const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);\n\n    await page.goto(notebookObject.url);\n\n    await nbUtils.addNotebookEntry(page);\n    await nbUtils.enterTextInLastEntry(page, TEST_TEXT);\n    await selectAll(page);\n    await copy(page);\n    for (let i = 0; i < iterations; i++) {\n      await paste(page);\n    }\n    await nbUtils.commitEntry(page);\n\n    await expect(page.locator(`text=\"${EXPECTED_TEXT}\"`)).toBeVisible();\n  });\n\n  test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7686'\n    });\n    const TEST_TEXT = 'This is a test';\n\n    await page.goto(notebookObject.url);\n\n    await nbUtils.addNotebookEntry(page);\n    await nbUtils.enterTextInLastEntry(page, TEST_TEXT);\n    await selectAll(page);\n    await copy(page);\n    await paste(page);\n    await nbUtils.commitEntry(page);\n\n    // This should not paste text into the entry\n    await paste(page);\n\n    await expect(await page.locator(`text=\"${TEST_TEXT.repeat(1)}\"`).count()).toEqual(1);\n    await expect(await page.locator(`text=\"${TEST_TEXT.repeat(2)}\"`).count()).toEqual(0);\n  });\n\n  test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({\n    page\n  }) => {\n    const TEST_TEXT = 'Do not lose me!';\n    const FIRST_NEW_NAME = 'New Name';\n    const SECOND_NEW_NAME = 'Second New Name';\n\n    await page.goto(notebookObject.url);\n\n    await page.getByLabel('Expand My Items folder').click();\n\n    await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);\n\n    // verify the name change in tree and browse bar\n    await verifyNameChange(page, FIRST_NEW_NAME);\n\n    // enter one entry\n    await enterAndCommitTextEntry(page, TEST_TEXT);\n\n    // verify the entry is present\n    await expect(await page.locator(`text=\"${TEST_TEXT}\"`).count()).toEqual(1);\n\n    // change the name\n    await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);\n\n    // verify the name change in tree and browse bar\n    await verifyNameChange(page, SECOND_NEW_NAME);\n\n    // verify the entry is still present\n    await expect(await page.locator(`text=\"${TEST_TEXT}\"`).count()).toEqual(1);\n  });\n});\n\n/**\n * Enter text into the last notebook entry and commit it.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} text\n */\nasync function enterAndCommitTextEntry(page, text) {\n  await nbUtils.addNotebookEntry(page);\n  await nbUtils.enterTextInLastEntry(page, text);\n  await nbUtils.commitEntry(page);\n}\n\n/**\n * Verify the name change in the tree and browse bar.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} newName\n */\nasync function verifyNameChange(page, newName) {\n  await expect(\n    page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')\n  ).toHaveText(newName);\n  await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding Notebooks.\n*/\n\nimport fs from 'fs/promises';\nimport { fileURLToPath } from 'url';\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst NOTEBOOK_NAME = 'Notebook';\n\ntest.describe('Snapshot image tests', () => {\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Notebook\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n\n  test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {\n    const imageData = await fs.readFile(\n      fileURLToPath(\n        new URL('../../../../../src/images/favicons/favicon-96x96.png', import.meta.url)\n      )\n    );\n    const imageArray = new Uint8Array(imageData);\n    const fileData = Array.from(imageArray);\n\n    const dropTransfer = await page.evaluateHandle((data) => {\n      const dataTransfer = new DataTransfer();\n      const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });\n      dataTransfer.items.add(file);\n      return dataTransfer;\n    }, fileData);\n\n    await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });\n    await page.locator('.c-ne__save-button > button').click();\n    // be sure that entry was created\n    await expect(page.getByText('favicon-96x96.png')).toBeVisible();\n\n    await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();\n    // expect large image to be displayed\n    await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();\n\n    await page.getByRole('button', { name: 'Close' }).click();\n\n    // drop another image onto the entry\n    await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });\n\n    const secondThumbnail = page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).nth(1);\n    await secondThumbnail.waitFor({ state: 'attached' });\n    // expect two embedded images now\n    await expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(2);\n\n    await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();\n\n    await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    // Ensure that the thumbnail is removed before we assert\n    await secondThumbnail.waitFor({ state: 'detached' });\n\n    // expect one embedded image now as we deleted the other\n    await expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(1);\n  });\n});\n\ntest.describe('Snapshot image failure tests', () => {\n  test.use({ failOnConsoleError: false });\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create Notebook\n    await createDomainObjectWithDefaults(page, {\n      type: NOTEBOOK_NAME\n    });\n  });\n\n  test('Get an error notification when dropping unknown file onto notebook entry', async ({\n    page\n  }) => {\n    // fill Uint8Array array with some garbage data\n    const garbageData = new Uint8Array(100);\n    const fileData = Array.from(garbageData);\n\n    const dropTransfer = await page.evaluateHandle((data) => {\n      const dataTransfer = new DataTransfer();\n      const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' });\n      dataTransfer.items.add(file);\n      return dataTransfer;\n    }, fileData);\n\n    await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });\n\n    // should have gotten a notification from OpenMCT that we couldn't add it\n    await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible();\n  });\n\n  test('Get an error notification when dropping big files onto notebook entry', async ({\n    page\n  }) => {\n    const garbageSize = 15 * 1024 * 1024; // 15 megabytes\n\n    await page.addScriptTag({\n      // make the garbage client side\n      content: `window.bigGarbageData = new Uint8Array(${garbageSize})`\n    });\n\n    const bigDropTransfer = await page.evaluateHandle(() => {\n      const dataTransfer = new DataTransfer();\n      const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' });\n      dataTransfer.items.add(file);\n      return dataTransfer;\n    });\n\n    await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer });\n\n    // should have gotten a notification from OpenMCT that we couldn't add it as it's too big\n    await expect(page.getByText('unable to embed')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding Notebooks.\n*/\n\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Snapshot Menu tests', () => {\n  test.fixme(\n    'When no default notebook is selected, Snapshot Menu dropdown should only have a single option',\n    async ({ page }) => {\n      // There should be no default notebook\n      // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`\n      // refresh page\n      // Click on 'Notebook Snapshot Menu'\n      // 'save to Notebook Snapshots' should be only option there\n    }\n  );\n  test.fixme(\n    'When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option',\n    async ({ page }) => {\n      // Create 2a notebooks\n      // Set Notebook A as Default\n      // Open Snapshot Menu and note that Notebook A is listed\n      // Close Snapshot Menu\n      // Set Default Notebook to Notebook B\n      // Open Snapshot Notebook and note that Notebook B is listed\n      // Select Default Notebook Option and verify that Snapshot is added to Notebook B\n    }\n  );\n  test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {\n    //Note this should be a visual test, too\n    // Create Telemetry object\n    // Create A notebook with many pages and sections.\n    // Set page and section defaults to be between first and last of many. i.e. 3 of 5\n    // Navigate to Telemetry object\n    // Select Default Notebook Option and verify that Snapshot is added to Notebook A\n    // Verify Snapshot Details appear correctly\n  });\n  test.fixme('Snapshots adjust time conductor', async ({ page }) => {\n    // Create Telemetry object\n    // Set Telemetry object's timeconductor to Fixed time with Start and End times are recorded\n    // Embed Telemetry object into notebook\n    // Set Time Conductor to Local clock\n    // Click into embedded telemetry object and verify object appears with same fixed time from record\n  });\n});\n\ntest.describe('Snapshot Container tests', () => {\n  test.beforeEach(async ({ page }) => {\n    //Navigate to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await page.getByLabel('Open the Notebook Snapshot Menu').click();\n    await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();\n    await page.getByLabel('Show Snapshots').click();\n  });\n  test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {\n    await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'Quick View' }).click();\n    await expect(page.getByLabel('Modal Overlay')).toBeVisible();\n    await expect(page.getByLabel('Preview Container')).toBeVisible();\n  });\n  test('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7552'\n    });\n    //Open Snapshot Object View\n    await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'View Snapshot' }).click();\n    await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();\n    await expect(page.locator('#snapshotDescriptor')).toHaveText(\n      /SNAPSHOT \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/\n    );\n    // Open Annotation Editor with Painterro\n    await page.getByLabel('Annotate this snapshot').click();\n    await expect(page.locator('#snap-annotation-canvas')).toBeVisible();\n    // Clear the canvas\n    await page.getByRole('button', { name: 'Put text [T]' }).click();\n    // Click in the Painterro canvas to add a text annotation\n    await page.locator('.ptro-crp-el').click();\n    await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');\n    // When working with Painterro, we need to check that the Apply button is hidden after clicking\n    const painterroApplyButton = page.locator('.ptro-text-tool-buttons').getByTitle('Apply');\n    await painterroApplyButton.click();\n    await expect(painterroApplyButton).toBeHidden();\n\n    // Save and exit annotation window\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('button', { name: 'Done' }).click();\n\n    // Open up annotation again\n    await page.getByRole('img', { name: 'My Items thumbnail' }).click();\n    await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();\n  });\n  test('A snapshot can be Annotated and saved as a JPG and PNG', async ({ page }) => {\n    //Open Snapshot Object View\n    await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'View Snapshot' }).click();\n    await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();\n\n    // Open Annotation Editor with Painterro\n    await page.getByLabel('Annotate this snapshot').click();\n    await expect(page.locator('#snap-annotation-canvas')).toBeVisible();\n    // Clear the canvas\n    await page.getByRole('button', { name: 'Put text [T]' }).click();\n    // Click in the Painterro canvas to add a text annotation\n    await page.locator('.ptro-crp-el').click();\n    await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');\n    // When working with Painterro, we need to check that the Apply button is hidden after clicking\n    const painterroApplyButton = page.locator('.ptro-text-tool-buttons').getByTitle('Apply');\n    await painterroApplyButton.click();\n    await expect(painterroApplyButton).toBeHidden();\n\n    // Save and exit annotation window\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('button', { name: 'Done' }).click();\n\n    // Open up annotation again\n    await page.getByRole('img', { name: 'My Items thumbnail' }).click();\n    await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();\n\n    // Save as JPG\n    await Promise.all([\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as JPG').click() // Triggers the download\n    ]);\n\n    // Save as PNG\n    await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();\n    await Promise.all([\n      page.waitForEvent('download'), // Waits for the download event\n      page.getByLabel('Export as PNG').click() // Triggers the download\n    ]);\n  });\n  test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});\n  test.fixme(\n    '5 Snapshots can be added to a container and Deleted with Delete All action',\n    async ({ page }) => {}\n  );\n  test.fixme(\n    'A snapshot can be Deleted from Container with 3 dot action menu',\n    async ({ page }) => {}\n  );\n  test.fixme(\n    'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',\n    async ({ page }) => {}\n  );\n  test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});\n  test.fixme(\n    'Can add object to Snapshot container and pull into notebook and create a new entry',\n    async ({ page }) => {\n      //Create Notebook\n      //Create Telemetry Object\n      //From Telemetry Object, use 'save to Notebook Snapshots'\n      //Snapshots indicator should blink, click on it to view snapshots\n      //Navigate to Notebook\n      //Drag and Drop onto droppable area for new entry\n      //New Entry created with given snapshot added\n      //Snapshot removed from container?\n    }\n  );\n  test.fixme(\n    'Can add object to Snapshot container and pull into notebook and existing entry',\n    async ({ page }) => {\n      //Create Notebook\n      //Create Telemetry Object\n      //From Telemetry Object, use 'save to Notebook Snapshots'\n      //Snapshots indicator should blink, click on it to view snapshots\n      //Navigate to Notebook\n      //Drag and Drop into exiting entry\n      //Existing Entry updated with given snapshot\n      //Snapshot removed from container?\n    }\n  );\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/notebookTags.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify notebook tag functionality.\n*/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport {\n  createNotebookAndEntry,\n  createNotebookEntryAndTags,\n  enterTextEntry\n} from '../../../../helper/notebookUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Tagging in Notebooks @addInit', () => {\n  test.beforeEach(async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n  test('Can load tags', async ({ page }) => {\n    await createNotebookAndEntry(page);\n\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    await page.locator('button:has-text(\"Add Tag\")').click();\n\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).toContainText('Science');\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).toContainText('Drilling');\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).toContainText('Driving');\n  });\n  test('Can add tags', async ({ page }) => {\n    await createNotebookEntryAndTags(page);\n\n    await expect(page.locator('[aria-label=\"Notebook Entry\"]')).toContainText('Science');\n    await expect(page.locator('[aria-label=\"Notebook Entry\"]')).toContainText('Driving');\n\n    await page.locator('button:has-text(\"Add Tag\")').click();\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).not.toContainText('Science');\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).not.toContainText('Driving');\n    await expect(page.locator('[aria-label=\"Autocomplete Options\"]')).toContainText('Drilling');\n  });\n  test('Can add tags with blank entry', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, { type: 'Notebook' });\n\n    await enterTextEntry(page, '');\n\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    await page.hover(`button:has-text(\"Add Tag\")`);\n    await page.locator(`button:has-text(\"Add Tag\")`).click();\n\n    // Click inside the tag search input\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n    // Select the \"Driving\" tag\n    await page.locator('[aria-label=\"Autocomplete Options\"] >> text=Driving').click();\n\n    await expect(page.locator('[aria-label=\"Notebook Entry\"]')).toContainText('Driving');\n  });\n  test('Can cancel adding tags', async ({ page }) => {\n    await createNotebookAndEntry(page);\n\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    // Test canceling adding a tag after we click \"Type to select tag\"\n    await page.locator('button:has-text(\"Add Tag\")').click();\n\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n\n    await page.getByRole('search').getByLabel('Search Input').click();\n\n    await expect(page.locator('button:has-text(\"Add Tag\")')).toBeVisible();\n\n    // Test canceling adding a tag after we just click \"Add Tag\"\n    await page.locator('button:has-text(\"Add Tag\")').click();\n\n    await page.getByRole('search').getByLabel('Search Input').click();\n\n    await expect(page.locator('button:has-text(\"Add Tag\")')).toBeVisible();\n  });\n  test('Can search for tags and preview works properly', async ({ page }) => {\n    await createNotebookEntryAndTags(page);\n    await page.getByRole('search').getByLabel('Search Input').click();\n    await page.getByRole('search').getByLabel('Search Input').fill('sc');\n    await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(\n      'Science'\n    );\n    await expect(\n      page.getByRole('listitem', { name: 'Annotation Search Result' })\n    ).not.toContainText('Driving');\n\n    await page.getByRole('search').getByLabel('Search Input').click();\n    await page.getByRole('search').getByLabel('Search Input').fill('Sc');\n    await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(\n      'Science'\n    );\n    await expect(\n      page.getByRole('listitem', { name: 'Annotation Search Result' })\n    ).not.toContainText('Driving');\n\n    await page.getByRole('search').getByLabel('Search Input').click();\n    await page.getByRole('search').getByLabel('Search Input').fill('Xq');\n    await expect(page.getByText('No results found')).toBeVisible();\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n\n    // Go back into edit mode for the display layout\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    await page.getByRole('search').getByLabel('Search Input').click();\n    await page.getByRole('search').getByLabel('Search Input').fill('Sc');\n    await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(\n      'Science'\n    );\n    await page.getByText('Entry 0').click();\n    await expect(page.locator('.js-preview-window')).toBeVisible();\n  });\n\n  test('Can delete tags', async ({ page }) => {\n    await createNotebookEntryAndTags(page);\n    // Delete Driving\n    await page.hover('[aria-label=\"Tag\"]:has-text(\"Driving\")');\n    await page.locator('[aria-label=\"Remove tag Driving\"]').click();\n\n    await expect(page.locator('[aria-label=\"Tags Inspector\"]')).toContainText('Science');\n    await expect(page.locator('[aria-label=\"Tags Inspector\"]')).not.toContainText('Driving');\n\n    await page.getByRole('search').getByLabel('Search Input').fill('sc');\n    await expect(\n      page.getByRole('listitem', { name: 'Annotation Search Result' })\n    ).not.toContainText('Driving');\n  });\n\n  test('Can delete entries without tags', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5823'\n    });\n    await createNotebookEntryAndTags(page);\n\n    await page.locator('text=To start a new entry, click here or drag and drop any object').click();\n    await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`);\n    await page.locator('.c-ne__save-button > button').click();\n\n    await page.hover('[aria-label=\"Notebook Entry Display\"] >> nth=1');\n    await page.locator('button[title=\"Delete this entry\"]').last().click();\n    await expect(\n      page.locator('text=This action will permanently delete this entry. Do you wish to continue?')\n    ).toBeVisible();\n    await page.locator('button:has-text(\"Ok\")').click();\n    await expect(\n      page.locator('text=This action will permanently delete this entry. Do you wish to continue?')\n    ).toBeHidden();\n  });\n\n  test('Can delete objects with tags and neither return in search', async ({ page }) => {\n    await createNotebookEntryAndTags(page);\n    // Delete Notebook\n    await page.locator('button[title=\"More actions\"]').click();\n    await page.locator('li[title=\"Remove this object from its containing object.\"]').click();\n    await page.locator('button:has-text(\"OK\")').click();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page.getByRole('search').getByLabel('Search Input').fill('Unnamed');\n    await expect(page.getByText('No results found')).toBeVisible();\n    await page.getByRole('search').getByLabel('Search Input').fill('sci');\n    await expect(page.getByText('No results found')).toBeVisible();\n    await page.getByRole('search').getByLabel('Search Input').fill('dri');\n    await expect(page.getByText('No results found')).toBeVisible();\n  });\n  test('Tags persist across reload', async ({ page }) => {\n    //Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const ITERATIONS = 4;\n    const notebook = await createNotebookEntryAndTags(page, ITERATIONS);\n    await page.goto(notebook.url);\n\n    // Verify tags are present\n    for (let iteration = 0; iteration < ITERATIONS; iteration++) {\n      const entryLocator = `[aria-label=\"Notebook Entry\"] >> nth = ${iteration}`;\n      await expect(page.locator(entryLocator)).toContainText('Science');\n      await expect(page.locator(entryLocator)).toContainText('Driving');\n    }\n\n    //Reload Page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Verify tags persist across reload\n    for (let iteration = 0; iteration < ITERATIONS; iteration++) {\n      const entryLocator = `[aria-label=\"Notebook Entry\"] >> nth = ${iteration}`;\n      await expect(page.locator(entryLocator)).toContainText('Science');\n      await expect(page.locator(entryLocator)).toContainText('Driving');\n    }\n  });\n  test('Can cancel adding a tag', async ({ page }) => {\n    await createNotebookAndEntry(page);\n\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    // Click on the \"Add Tag\" button\n    await page.locator('button:has-text(\"Add Tag\")').click();\n\n    // Click inside the AutoComplete field\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n\n    // Click on the \"Tags\" header (simulating a click outside the autocomplete)\n    await page.locator('div.c-inspect-properties__header:has-text(\"Tags\")').click();\n\n    // Verify there is a button with text \"Add Tag\"\n    await expect(page.locator('button:has-text(\"Add Tag\")')).toBeVisible();\n\n    // Verify the AutoComplete field is hidden\n    await expect(page.locator('[placeholder=\"Type to select tag\"]')).toBeHidden();\n  });\n  test('Can start to add a tag, click away, and add a tag', async ({ page }) => {\n    await createNotebookEntryAndTags(page);\n\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    // Click on the body simulating a click outside the autocomplete)\n    await page.locator('body').click();\n    await page.locator(`[aria-label=\"Notebook Entry\"]`).click();\n\n    await page.hover(`button:has-text(\"Add Tag\")`);\n    await page.locator(`button:has-text(\"Add Tag\")`).click();\n\n    // Click inside the tag search input\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n    // Select the \"Driving\" tag\n    await page.locator('[aria-label=\"Autocomplete Options\"] >> text=Drilling').click();\n    await expect(page.getByLabel('Notebook Entries').getByText('Drilling')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.\n*/\n/* eslint-disable playwright/no-networkidle */\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport * as nbUtils from '../../../../helper/notebookUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Notebook Tests with CouchDB @couchdb @network', () => {\n  let testNotebook;\n\n  test.beforeEach(async ({ page }) => {\n    // Navigate to baseURL\n    await page.goto('./', { waitUntil: 'networkidle' });\n\n    // Create Notebook\n    testNotebook = await createDomainObjectWithDefaults(page, {\n      type: 'Notebook',\n      name: 'Test Notebook'\n    });\n    await page.goto(testNotebook.url);\n    await expect(page.getByLabel('Browse bar object name')).toHaveText(testNotebook.name);\n  });\n\n  test('Search tests', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/akhenry/openmct-yamcs/issues/69'\n    });\n    await nbUtils.enterTextEntry(page, 'First Entry');\n    await page.getByText('Annotations').click();\n\n    // Add three tags\n    await addTagAndAwaitNetwork(page, 'Science');\n    await addTagAndAwaitNetwork(page, 'Drilling');\n    await addTagAndAwaitNetwork(page, 'Driving');\n\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').click();\n    //Partial match for \"Science\" should only return Science\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').fill('Sc');\n    await expect(page.locator('[aria-label=\"Annotation Search Result\"]').first()).toContainText(\n      'Science'\n    );\n    await expect(page.locator('[aria-label=\"Annotation Search Result\"]').first()).not.toContainText(\n      'Driving'\n    );\n    await expect(page.locator('[aria-label=\"Annotation Search Result\"]').first()).not.toContainText(\n      'Drilling'\n    );\n\n    //Searching for a tag which does not exist should return an empty result\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').click();\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').fill('Xq');\n    await expect(page.getByText('No results found')).toBeVisible();\n  });\n});\n\n/**\n * Add a tag to a notebook entry by providing a tagName.\n * Reduces indeterminism by waiting until all necessary requests are completed.\n * @param {import('@playwright/test').Page} page\n * @param {string} tagName\n */\nasync function addTagAndAwaitNetwork(page, tagName) {\n  await page.hover(`button:has-text(\"Add Tag\")`);\n  await page.locator(`button:has-text(\"Add Tag\")`).click();\n  await page.locator('[placeholder=\"Type to select tag\"]').click();\n  await Promise.all([\n    // Waits for the next request with the specified url\n    page.waitForRequest('**/openmct/_all_docs?include_docs=true'),\n    // Triggers the request\n    page.locator(`[aria-label=\"Autocomplete Options\"] >> text=${tagName}`).click(),\n    expect(page.locator(`[aria-label=\"Tag\"]:has-text(\"${tagName}\")`)).toBeVisible()\n  ]);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  dragAndDropEmbed,\n  enterTextEntry,\n  lockPage,\n  startAndAddRestrictedNotebookObject\n} from '../../../../helper/notebookUtils.js';\nimport { expect, streamToString, test } from '../../../../pluginFixtures.js';\n\nconst TEST_TEXT = 'Testing text for entries.';\nconst TEST_TEXT_NAME = 'Test Page';\n\ntest.describe('Restricted Notebook', () => {\n  let notebook;\n  test.beforeEach(async ({ page }) => {\n    notebook = await startAndAddRestrictedNotebookObject(page);\n  });\n\n  test('Can be renamed @addInit', async ({ page }) => {\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);\n  });\n\n  test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {\n    await openObjectTreeContextMenu(page, notebook.url);\n\n    const menuOptions = page.locator('.c-menu ul');\n    await expect.soft(menuOptions).toContainText('Remove');\n\n    const restrictedNotebookTreeObject = page.locator(`a:has-text(\"${notebook.name}\")`);\n\n    // notebook tree object exists\n    await expect(restrictedNotebookTreeObject).toHaveCount(1);\n\n    // Click Remove Text\n    await page.locator('li[role=\"menuitem\"]:has-text(\"Remove\")').click();\n\n    // Click 'Ok' on confirmation window\n    await page.locator('button:has-text(\"OK\")').click();\n\n    // has been deleted\n    await expect(restrictedNotebookTreeObject).toHaveCount(0);\n  });\n\n  test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {\n    await enterTextEntry(page, TEST_TEXT);\n\n    await expect(page.getByLabel('Commit Entries')).toHaveCount(1);\n  });\n});\n\ntest.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {\n  let notebook;\n  test.beforeEach(async ({ page }) => {\n    notebook = await startAndAddRestrictedNotebookObject(page);\n    await enterTextEntry(page, TEST_TEXT);\n    await lockPage(page);\n\n    // open sidebar\n    await page.locator('button.c-notebook__toggle-nav-button').click();\n  });\n\n  test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => {\n    // eslint-disable-next-line playwright/no-skipped-test\n    test.skip(testInfo.project === 'chrome-beta', 'Test is unreliable on chrome-beta');\n    // main lock message on page\n    const lockMessage = page.locator(\n      'text=This page has been committed and cannot be modified or removed'\n    );\n    await expect(lockMessage).toHaveCount(1);\n\n    // lock icon on page in sidebar\n    const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');\n    await expect(pageLockIcon).toHaveCount(1);\n\n    // no way to remove a restricted notebook with a locked page\n    await openObjectTreeContextMenu(page, notebook.url);\n    const menuOptions = page.locator('.c-menu ul');\n\n    await expect(menuOptions).not.toContainText('Remove');\n  });\n\n  test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({\n    page\n  }) => {\n    // Add a new page to the section\n    await page.getByRole('button', { name: 'Add Page' }).click();\n    // Focus the new page by clicking it\n    await page.getByText('Unnamed Page').nth(1).click();\n    // Rename the new page\n    await page.getByText('Unnamed Page').nth(1).fill(TEST_TEXT_NAME);\n\n    // expect to be able to rename unlocked pages\n    await page.getByText(TEST_TEXT_NAME).press('Enter'); // exit contenteditable state\n    await expect(page.locator('div').filter({ hasText: /^Test Page$/ })).toHaveCount(1);\n\n    // enter test text\n    await enterTextEntry(page, TEST_TEXT);\n\n    // expect new page to be lockable\n    await expect(page.getByLabel('Commit Entries')).toHaveCount(1);\n\n    // Click the context menu button for the new page\n    await page.getByTitle('Open context menu').click();\n    // Delete the page\n    await page.getByRole('menuitem', { name: 'Delete Page' }).click();\n    // Click OK button\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // deleted page, should no longer exist\n    const deletedPageElement = page.getByText(TEST_TEXT_NAME);\n    await expect(deletedPageElement).toHaveCount(0);\n  });\n});\n\ntest.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {\n  test.beforeEach(async ({ page }) => {\n    const notebook = await startAndAddRestrictedNotebookObject(page);\n    await dragAndDropEmbed(page, notebook);\n  });\n\n  test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {\n    // Click embed popup menu\n    await page.getByLabel('Notebook Entry').getByLabel('More actions').click();\n\n    const embedMenu = page.getByLabel('Super Menu');\n    await expect(embedMenu).toContainText('Remove This Embed');\n  });\n\n  test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {\n    await lockPage(page);\n    // Click embed popup menu\n    await page.getByLabel('Notebook Entry').getByLabel('More actions').click();\n\n    const embedMenu = page.getByLabel('Super Menu');\n    await expect(embedMenu).not.toContainText('Remove This Embed');\n  });\n});\n\ntest.describe('can export restricted notebook as text', () => {\n  test.beforeEach(async ({ page }) => {\n    await startAndAddRestrictedNotebookObject(page);\n  });\n\n  test('basic functionality', async ({ page }) => {\n    await enterTextEntry(page, `Foo bar entry`);\n    // Click on 3 Dot Menu\n    await page.locator('button[title=\"More actions\"]').click();\n    const downloadPromise = page.waitForEvent('download');\n\n    await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();\n\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    //Verify exported text as a stream of text instead of a file read from the filesystem\n    const download = await downloadPromise;\n    const readStream = await download.createReadStream();\n    const exportedText = await streamToString(readStream);\n    expect(exportedText).toContain('Foo bar entry');\n  });\n\n  test.fixme('can export multiple notebook entries as text', async ({ page }) => {});\n  test.fixme('can export all notebook entry metdata', async ({ page }) => {});\n  test.fixme('can export all notebook tags', async ({ page }) => {});\n  test.fixme('can export all notebook snapshots', async ({ page }) => {});\n});\n\n/**\n * Open the given `domainObject`'s context menu from the object tree.\n * Expands the path to the object and scrolls to it if necessary.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url the url to the object\n */\nasync function openObjectTreeContextMenu(page, url) {\n  await page.goto(url);\n  await page.getByLabel('Show selected item in tree').click();\n  await page.locator('.is-navigated-object').click({\n    button: 'right'\n  });\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*\n * This test suite is dedicated to testing the operator status plugin.\n */\n\nimport { fileURLToPath } from 'url';\n\nimport { expect, test } from '../../../../pluginFixtures.js';\n\n/*\n\nPrecondition: Inject Example User, Operator Status Plugins\nVerify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)\n\nClear Role Status of single user test\nSTUB (test.fixme) Rolling through each\n\n*/\n\ntest.describe('Operator Status', () => {\n  test.beforeEach(async ({ page }) => {\n    // FIXME: determine if plugins will be added to index.html or need to be injected\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../../../helper/addInitExampleUser.js', import.meta.url))\n    });\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../../../helper/addInitOperatorStatus.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await expect(page.getByText('Select Role')).toBeVisible();\n    // Description should be empty https://github.com/nasa/openmct/issues/6978\n    await expect(page.locator('.c-message__action-text')).toBeHidden();\n    // set role\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    // dismiss role confirmation popup\n    await page.getByRole('button', { name: 'Dismiss' }).click();\n  });\n\n  // verify that operator status is visible\n  test('operator status is visible and expands when clicked', async ({ page }) => {\n    await expect(page.locator('div[title=\"Set my operator status\"]')).toBeVisible();\n    await page.locator('div[title=\"Set my operator status\"]').click();\n\n    // expect default status to be 'GO'\n    await expect(page.locator('.c-status-poll-panel')).toBeVisible();\n  });\n\n  test('poll question indicator remains when blank poll set', async ({ page }) => {\n    await expect(page.locator('div[title=\"Set the current poll question\"]')).toBeVisible();\n    await page.locator('div[title=\"Set the current poll question\"]').click();\n    // set to blank\n    await page.getByRole('button', { name: 'Update' }).click();\n\n    // should still be visible\n    await expect(page.locator('div[title=\"Set the current poll question\"]')).toBeVisible();\n  });\n\n  // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)\n  test('operator status table reflects answered values', async ({ page }) => {\n    // user navigates to operator status poll\n    const statusPollIndicator = page.locator('div[title=\"Set my operator status\"]');\n    await statusPollIndicator.click();\n\n    // get user role value\n    const userRole = page.locator('.c-status-poll-panel__user-role');\n    const userRoleText = await userRole.innerText();\n\n    // get selected status value\n    const selectStatus = page.locator('select[name=\"setStatus\"]');\n    await selectStatus.selectOption({ index: 1 });\n    const initialStatusValue = await selectStatus.inputValue();\n\n    // open manage status poll\n    const manageStatusPollIndicator = page.locator('div[title=\"Set the current poll question\"]');\n    await manageStatusPollIndicator.click();\n    // parse the table row values\n    const row = page.locator(`tr:has-text(\"${userRoleText}\")`);\n    const rowValues = await row.innerText();\n    const rowValuesArr = rowValues.split('\\t');\n    const COLUMN_STATUS_INDEX = 1;\n    // check initial set value matches status table\n    expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual(\n      initialStatusValue.toLowerCase()\n    );\n\n    // change user status\n    await statusPollIndicator.click();\n    // FIXME: might want to grab a dynamic option instead of arbitrary\n    await page.locator('select[name=\"setStatus\"]').selectOption({ index: 2 });\n    const updatedStatusValue = await selectStatus.inputValue();\n    // verify user status is reflected in table\n    await manageStatusPollIndicator.click();\n\n    const updatedRow = page.locator(`tr:has-text(\"${userRoleText}\")`);\n    const updatedRowValues = await updatedRow.innerText();\n    const updatedRowValuesArr = updatedRowValues.split('\\t');\n\n    expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual(\n      updatedStatusValue.toLowerCase()\n    );\n  });\n\n  test('clear poll button removes poll responses', async ({ page }) => {\n    // user navigates to operator status poll\n    const statusPollIndicator = page.locator('div[title=\"Set my operator status\"]');\n    await statusPollIndicator.click();\n\n    // get user role value\n    const userRole = page.locator('.c-status-poll-panel__user-role');\n    const userRoleText = await userRole.innerText();\n\n    // get selected status value\n    const selectStatus = page.locator('select[name=\"setStatus\"]');\n    // FIXME: might want to grab a dynamic option instead of arbitrary\n    await selectStatus.selectOption({ index: 1 });\n    const initialStatusValue = await selectStatus.inputValue();\n\n    // open manage status poll\n    const manageStatusPollIndicator = page.locator('div[title=\"Set the current poll question\"]');\n    await manageStatusPollIndicator.click();\n    // parse the table row values\n    const row = page.locator(`tr:has-text(\"${userRoleText}\")`);\n    const rowValues = await row.innerText();\n    const rowValuesArr = rowValues.split('\\t');\n    const COLUMN_STATUS_INDEX = 1;\n    // check initial set value matches status table\n    expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual(\n      initialStatusValue.toLowerCase()\n    );\n\n    // clear the poll\n    await page.locator('button[title=\"Clear the previous poll question\"]').click();\n\n    const updatedRow = page.locator(`tr:has-text(\"${userRoleText}\")`);\n    const updatedRowValues = await updatedRow.innerText();\n    const updatedRowValuesArr = updatedRowValues.split('\\t');\n    const UNSET_VALUE_LABEL = 'Not set';\n    expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]).toEqual(UNSET_VALUE_LABEL);\n  });\n\n  test('Poll indicator is visible when window is really small', async ({ page }) => {\n    const pollIndicator = page.locator('div[title=\"Set my operator status\"]');\n    //Make window narrow\n    await page.setViewportSize({ width: 640, height: 480 });\n    await page.getByLabel('Display as single line').click();\n    const indicatorsCount = await page.locator('.c-indicator').count();\n    //Assert that multiple indicators are active\n    expect(indicatorsCount).toBeGreaterThanOrEqual(3);\n    //Assert that indicators are expanded\n    await expect(page.locator('.l-shell__head')).toContainClass('l-shell__head--expanded');\n    //Expect poll indicator to be visible\n    await expect(pollIndicator).toBeInViewport({ ratio: 1 });\n  });\n\n  test.fixme('iterate through all possible response values', async ({ page }) => {\n    // test all possible response values for the poll\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/performanceIndicator/performanceIndicator.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('The performance indicator', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await page.evaluate(() => {\n      const openmct = window.openmct;\n      openmct.install(openmct.plugins.PerformanceIndicator());\n    });\n  });\n\n  test('can be installed', ({ page }) => {\n    const performanceIndicator = page.getByTitle('Performance Indicator');\n    expect(performanceIndicator).toBeDefined();\n  });\n\n  test('Shows a numerical FPS value', async ({ page }) => {\n    // Frames Per Second. We need to wait at least 1 second to get a value.\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(1000);\n    await expect(page.getByTitle('Performance Indicator')).toHaveText(/\\d\\d? fps/);\n  });\n\n  test('Supports showing optional extended performance information in an overlay for debugging', async ({\n    page\n  }) => {\n    const performanceMeasurementLabel = 'Some measurement';\n    const performanceMeasurementValue = 'Some value';\n\n    await page.evaluate(\n      ({ performanceMeasurementLabel: label, performanceMeasurementValue: value }) => {\n        const openmct = window.openmct;\n        openmct.performance.measurements.set(label, value);\n      },\n      { performanceMeasurementLabel, performanceMeasurementValue }\n    );\n    const performanceIndicator = page.getByTitle('Performance Indicator');\n    await performanceIndicator.click();\n    //Performance overlay is a crude debugging tool, it's evaluated once per second.\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(1000);\n    const performanceOverlay = page.getByTitle('Performance Overlay');\n    await expect(performanceOverlay).toBeVisible();\n    await expect(performanceOverlay).toHaveText(new RegExp(`${performanceMeasurementLabel}.*`));\n    await expect(performanceOverlay).toHaveText(new RegExp(`.*${performanceMeasurementValue}`));\n\n    //Confirm that it disappears if we click on it again.\n    await performanceIndicator.click();\n    await expect(performanceOverlay).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTestsuite for plot autoscale.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithFixedTimeBounds\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\ntest.use({\n  viewport: {\n    width: 1280,\n    height: 720\n  }\n});\n\ntest.describe('Autoscale', () => {\n  test('User can set autoscale with a valid range @snapshot', async ({ page }) => {\n    //This is necessary due to the size of the test suite.\n    test.slow();\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      name: 'Test Overlay Plot',\n      type: 'Overlay Plot'\n    });\n    await createDomainObjectWithDefaults(page, {\n      name: 'Test Sine Wave Generator',\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    // Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC\n    await navigateToObjectWithFixedTimeBounds(page, overlayPlot.url, 1648591200000, 1648591230000);\n\n    await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);\n\n    // enter edit mode\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // turn off autoscale\n    await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();\n\n    await page.getByLabel('Y Axis 1 Minimum value').fill('-2');\n    await page.getByLabel('Y Axis 1 Maximum value').fill('2');\n\n    // save\n    await page.getByLabel('Save').click();\n    await Promise.all([\n      page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),\n      //Wait for Save Banner to appear\n      page.locator('.c-message-banner__message').hover({ trial: true })\n    ]);\n    //Wait until Save Banner is gone\n    await page.locator('.c-message-banner__close-button').click();\n    await page.locator('.c-message-banner__message').waitFor({ state: 'detached' });\n\n    // Make sure that after turning off autoscale, the user entered range values are reflected in the ticks.\n    await testYTicks(page, [\n      '-2.00',\n      '-1.50',\n      '-1.00',\n      '-0.50',\n      '0.00',\n      '0.50',\n      '1.00',\n      '1.50',\n      '2.00'\n    ]);\n\n    const canvas = page.locator('canvas').nth(1);\n\n    await canvas.hover({ trial: true });\n    await expect(page.locator('.js-series-data-loaded')).toBeVisible();\n\n    expect\n      .soft(await canvas.screenshot())\n      .toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });\n\n    //Alt Drag Start\n    await page.keyboard.down('Alt');\n\n    await canvas.dragTo(canvas, {\n      sourcePosition: {\n        x: 200,\n        y: 200\n      },\n      targetPosition: {\n        x: 400,\n        y: 400\n      }\n    });\n\n    //Alt Drag End\n    await page.keyboard.up('Alt');\n\n    // Ensure the drag worked.\n    await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);\n\n    //Wait for canvas to stabilize.\n    await canvas.hover({ trial: true });\n\n    expect\n      .soft(await canvas.screenshot())\n      .toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' });\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function testYTicks(page, values) {\n  const yTicks = page.locator('.gl-plot-y-tick-label');\n  await page.locator('canvas >> nth=1').hover();\n  let promises = [yTicks.count().then((c) => expect(c).toBe(values.length))];\n\n  for (let i = 0, l = values.length; i < l; i += 1) {\n    promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line\n  }\n\n  await Promise.all(promises);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify log plot functionality. Note this test suite if very much under active development and should not\nnecessarily be used for reference when writing new tests in this area.\n*/\n\nimport { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Log plot tests', () => {\n  test.beforeEach(async ({ page }) => {\n    // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Set a specific time range for consistency, otherwise it will change\n    // on every test to a range based on the current time.\n    const startDate = '2022-03-29';\n    const startTime = '22:00:00';\n    const endDate = '2022-03-29';\n    const endTime = '22:00:30';\n\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Unnamed Overlay Plot'\n    });\n\n    // create a sinewave generator\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Unnamed Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n\n    // set amplitude to 6, offset 4, data rate 2 hz\n    await page.getByLabel('Amplitude', { exact: true }).fill('6');\n    await page.getByLabel('Offset', { exact: true }).fill('4');\n    await page.getByLabel('Data Rate (hz)', { exact: true }).fill('2');\n\n    await page.getByLabel('Save').click();\n\n    await page.goto(overlayPlot.url);\n  });\n  test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({\n    page\n  }) => {\n    await testRegularTicks(page);\n    await enableEditMode(page);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await enableLogMode(page);\n    await testLogTicks(page);\n    await disableLogMode(page);\n    await testRegularTicks(page);\n    await enableLogMode(page);\n    await testLogTicks(page);\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await testLogTicks(page);\n  });\n\n  // Leaving test as 'TODO' for now.\n  // NOTE: Not eligible for community contributions.\n  test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {\n    await enableEditMode(page);\n    await enableLogMode(page);\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // TODO ...export, delete the overlay, then import it...\n\n    //await testLogTicks(page);\n\n    // TODO, the plot is slightly at different position that in the other test, so this fails.\n    // ...We can fix it by copying all steps from the first test...\n    // await testLogPlotPixels(page);\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function testRegularTicks(page) {\n  const yTicks = page.locator('.gl-plot-y-tick-label');\n  await expect(yTicks).toHaveCount(7);\n  await expect(yTicks.nth(0)).toHaveText('-2');\n  await expect(yTicks.nth(1)).toHaveText('0');\n  await expect(yTicks.nth(2)).toHaveText('2');\n  await expect(yTicks.nth(3)).toHaveText('4');\n  await expect(yTicks.nth(4)).toHaveText('6');\n  await expect(yTicks.nth(5)).toHaveText('8');\n  await expect(yTicks.nth(6)).toHaveText('10');\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function testLogTicks(page) {\n  const yTicks = page.locator('.gl-plot-y-tick-label');\n  await expect(yTicks).toHaveCount(9);\n  await expect(yTicks.nth(0)).toHaveText('-2.98');\n  await expect(yTicks.nth(1)).toHaveText('-1.51');\n  await expect(yTicks.nth(2)).toHaveText('-0.58');\n  await expect(yTicks.nth(3)).toHaveText('-0.00');\n  await expect(yTicks.nth(4)).toHaveText('0.58');\n  await expect(yTicks.nth(5)).toHaveText('1.51');\n  await expect(yTicks.nth(6)).toHaveText('2.98');\n  await expect(yTicks.nth(7)).toHaveText('5.31');\n  await expect(yTicks.nth(8)).toHaveText('9.00');\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function enableEditMode(page) {\n  // turn on edit mode\n  await page.getByRole('button', { name: 'Edit Object' }).click();\n  await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function enableLogMode(page) {\n  await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked();\n  await page.getByRole('checkbox', { name: 'Log mode' }).check();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function disableLogMode(page) {\n  await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked();\n  await page.getByRole('checkbox', { name: 'Log mode' }).uncheck();\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n */\n// FIXME: Remove this eslint exception once implemented\n// eslint-disable-next-line no-unused-vars\nasync function testLogPlotPixels(page) {\n  const pixelsMatch = await page.evaluate(async () => {\n    // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.\n\n    await new Promise((r) => setTimeout(r, 5 * 1000));\n\n    // These are some pixels that should be blue points in the log plot.\n    // If the plot changes shape to an unexpected shape, this will\n    // likely fail, which is what we want.\n    //\n    // I found these pixels by pausing playwright in debug mode at this\n    // point, and using similar code as below to output the pixel data, then\n    // I logged those pixels here.\n    const expectedBluePixels = [\n      // TODO these pixel sets only work with the first test, but not the second test.\n\n      // [60, 35],\n      // [121, 125],\n      // [156, 377],\n      // [264, 73],\n      // [372, 186],\n      // [576, 73],\n      // [659, 439],\n      // [675, 423]\n\n      [60, 35],\n      [120, 125],\n      [156, 375],\n      [264, 73],\n      [372, 185],\n      [575, 72],\n      [659, 437],\n      [675, 421]\n    ];\n\n    // The first canvas in the DOM is the one that has the plot point\n    // icons (canvas 2d), which is the one we are testing. The second\n    // one in the DOM is the WebGL canvas with the line. (Why aren't\n    // they both WebGL?)\n    const canvas = document.querySelector('canvas');\n\n    const ctx = canvas.getContext('2d');\n\n    for (const pixel of expectedBluePixels) {\n      // XXX Possible optimization: call getImageData only once with\n      // area including all pixels to be tested.\n      const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;\n\n      // #43b0ffff <-- openmct cyanish-blue with 100% opacity\n      // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {\n      if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {\n        // If any pixel is empty, it means we didn't hit a plot point.\n        return false;\n      }\n    }\n\n    return true;\n  });\n\n  expect(pixelsMatch).toBe(true);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify log plot functionality when objects are missing\n*/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Handle missing object for plots', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n  test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {\n    // eslint-disable-next-line playwright/no-skipped-test\n    test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');\n\n    let warningReceived = false;\n\n    page.on('console', (message) => {\n      if (message.type() === 'warning' && message.text().includes('Missing domain object')) {\n        warningReceived = true;\n      }\n    });\n\n    const stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: stackedPlot.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: stackedPlot.uuid\n    });\n\n    //Gets local storage and deletes the last sine wave generator in the stacked plot\n    const mct = await page.evaluate(() => window.localStorage.getItem('mct'));\n    const parsedData = JSON.parse(mct);\n    const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];\n\n    delete parsedData[key];\n\n    //Sets local storage with missing object\n    const jsonData = JSON.stringify(parsedData);\n    await page.evaluate((data) => {\n      window.localStorage.setItem('mct', data);\n    }, jsonData);\n\n    //Reloads page and clicks on stacked plot\n    await page.reload({ waitUntil: 'domcontentloaded' });\n    await page.goto(stackedPlot.url);\n\n    //Verify Main section is there on load\n    await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);\n\n    //Check that there is only one stacked item plot with a plot, the missing one will be empty\n    await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);\n    //Verify that console.warn was thrown\n    expect(warningReceived).toBe(true);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify log plot functionality. Note this test suite if very much under active development and should not\nnecessarily be used for reference when writing new tests in this area.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  getCanvasPixels,\n  waitForPlotsToRender\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Overlay Plot', () => {\n  let overlayPlot;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n  });\n\n  test('Plot legend color is in sync with plot series color', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // navigate to plot series color palette\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Expand Sine Wave Generator:').click();\n    await page.locator('.c-click-swatch--menu').click();\n    await page.locator('.c-palette__item[style=\"background: rgb(255, 166, 61);\"]').click();\n    // gets color for swatch located in legend\n    const seriesColorSwatch = page.locator(\n      '.gl-plot-y-label-swatch-container > .plot-series-color-swatch'\n    );\n    await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');\n  });\n\n  test('Plot legend expands by default', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7403'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Assert that the legend is collapsed by default\n    await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();\n    await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();\n    await expect(page.getByLabel('Expand by Default')).toHaveText(/No/);\n\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);\n\n    // Change the legend to expand by default\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Expand By Default').check();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Assert that the legend is now open\n    await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();\n    await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Value' })).toBeVisible();\n    await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);\n\n    // Assert that the legend is expanded on page load\n    await page.reload();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();\n    await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Timestamp' })).toBeVisible();\n    await expect(page.getByRole('columnheader', { name: 'Value' })).toBeVisible();\n    await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);\n  });\n\n  test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6338'\n    });\n\n    const swgA = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    // Assert that no limit lines are shown by default\n    await page.locator('.js-limit-area').waitFor({ state: 'attached' });\n    await expect(page.locator('.c-plot-limit-line')).toHaveCount(0);\n\n    // Enter edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the \"Sine Wave Generator\" plot series options and enable limit lines\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Expand Sine Wave Generator:').click();\n    await page.getByLabel('Limit lines').check();\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    // Save (exit edit mode)\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    await page.reload();\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    // Enter edit mode\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2\n    await page\n      .locator(`#inspector-elements-tree >> text=${swgA.name}`)\n      .dragTo(page.locator('[aria-label=\"Element Item Group Y Axis 2\"]'));\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    // Save (exit edit mode)\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    await page.reload();\n\n    await assertLimitLinesExistAndAreVisible(page);\n  });\n\n  test('Limit lines adjust when series is resized', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6987'\n    });\n    // Create an Overlay Plot with a default SWG\n    overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    // Assert that no limit lines are shown by default\n    await expect(page.locator('.js-limit-area')).toBeAttached();\n    await expect(page.locator('.c-plot-limit-line')).toHaveCount(0);\n\n    // Enter edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the \"Sine Wave Generator\" plot series options and enable limit lines\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Expand Sine Wave Generator:').click();\n    await page.getByLabel('Limit lines').check();\n\n    await assertLimitLinesExistAndAreVisible(page);\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    const initialCoords = await assertLimitLinesExistAndAreVisible(page);\n    // Resize the chart container by showing the snapshot pane.\n    await page.getByLabel('Show Snapshots').click();\n\n    const newCoords = await assertLimitLinesExistAndAreVisible(page);\n    // We just need to know that the first limit line redrew somewhere lower than the initial y position.\n    expect(newCoords.y).toBeGreaterThan(initialCoords.y);\n  });\n\n  test('The elements pool supports dragging series into multiple y-axis buckets', async ({\n    page\n  }) => {\n    const swgA = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    const swgB = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    const swgC = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    const swgD = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    const swgE = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    // Drag swg a, c, e into Y Axis 2\n    await page\n      .locator(`#inspector-elements-tree >> text=${swgA.name}`)\n      .dragTo(page.locator('[aria-label=\"Element Item Group Y Axis 2\"]'));\n    await page\n      .locator(`#inspector-elements-tree >> text=${swgC.name}`)\n      .dragTo(page.locator('[aria-label=\"Element Item Group Y Axis 2\"]'));\n    await page\n      .locator(`#inspector-elements-tree >> text=${swgE.name}`)\n      .dragTo(page.locator('[aria-label=\"Element Item Group Y Axis 2\"]'));\n\n    // Assert that Y Axis 1 and Y Axis 2 property groups are visible only\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    const yAxis1PropertyGroup = page.locator('[aria-label=\"Y Axis Properties\"]');\n    const yAxis2PropertyGroup = page.locator('[aria-label=\"Y Axis 2 Properties\"]');\n    const yAxis3PropertyGroup = page.locator('[aria-label=\"Y Axis 3 Properties\"]');\n\n    await expect(yAxis1PropertyGroup).toBeVisible();\n    await expect(yAxis2PropertyGroup).toBeVisible();\n    await expect(yAxis3PropertyGroup).toBeHidden();\n\n    const yAxis1Group = page.getByLabel('Y Axis 1');\n    const yAxis2Group = page.getByLabel('Y Axis 2');\n    const yAxis3Group = page.getByLabel('Y Axis 3');\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    // Drag swg b into Y Axis 3\n    await page\n      .locator(`#inspector-elements-tree >> text=${swgB.name}`)\n      .dragTo(page.locator('[aria-label=\"Element Item Group Y Axis 3\"]'));\n\n    // Assert that all Y Axis property groups are visible\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    await expect(yAxis1PropertyGroup).toBeVisible();\n    await expect(yAxis2PropertyGroup).toBeVisible();\n    await expect(yAxis3PropertyGroup).toBeVisible();\n\n    // Verify that the elements are in the correct buckets and in the correct order\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();\n    expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy();\n    expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy();\n    expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy();\n    expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();\n  });\n\n  test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({\n    page\n  }) => {\n    const swgA = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n    // Wait for plot series data to load and be drawn\n    await waitForPlotsToRender(page);\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();\n    const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');\n    const plotPixelSize = plotPixels.length;\n    expect(plotPixelSize).toBeGreaterThan(0);\n  });\n\n  test('Can remove an item via the elements pool action menu', async ({ page }) => {\n    const swgA = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    const swgB = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n    // Wait for plot series data to load and be drawn\n    await waitForPlotsToRender(page);\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);\n    await expect(swgAElementsPoolItem).toBeVisible();\n    await swgAElementsPoolItem.click({ button: 'right' });\n    await page.getByRole('menuitem', { name: 'Remove' }).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    await expect(swgAElementsPoolItem).toBeHidden();\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7530'\n    });\n    await test.step('Verify that the legend is correct after removing a series', async () => {\n      await page.getByLabel('Plot Canvas').hover();\n      await page.mouse.move(50, 0, {\n        steps: 10\n      });\n      await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);\n      await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();\n      await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();\n    });\n  });\n});\n\n/**\n * Asserts that limit lines exist and are visible\n * @param {import('@playwright/test').Page} page\n */\nasync function assertLimitLinesExistAndAreVisible(page) {\n  // Wait for plot series data to load\n  await waitForPlotsToRender(page);\n  // Wait for limit lines to be created\n  await page.locator('.js-limit-area').waitFor({ state: 'attached' });\n  // There should be 10 limit lines created by default\n  await expect(page.locator('.c-plot-limit-line')).toHaveCount(10);\n  const limitLineCount = await page.locator('.c-plot-limit-line').count();\n  for (let i = 0; i < limitLineCount; i++) {\n    await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();\n  }\n\n  const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();\n  return firstLimitLineCoords;\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is dedicated to testing the rendering and interaction of plots.\n *\n */\n\nimport {\n  createDomainObjectWithDefaults,\n  getCanvasPixels,\n  setEndOffset,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Plot Controls', () => {\n  let overlayPlot;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    // Create an overlay plot with a sine wave generator\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    await page.goto(`${overlayPlot.url}`);\n  });\n\n  test(\"Plots don't purge data when paused\", async ({ page }) => {\n    // Set realtime mode with 2 second window\n    const startOffset = {\n      startMins: '00',\n      startSecs: '01'\n    };\n\n    const endOffset = {\n      endMins: '00',\n      endSecs: '01'\n    };\n\n    // Switch to real-time mode\n    await setRealTimeMode(page);\n\n    // Set start time offset\n    await setStartOffset(page, startOffset);\n\n    // Set end time offset\n    await setEndOffset(page, endOffset);\n    // Edit the overlay plot and turn off auto scale, setting the min and max to -1 and 1\n    // enter edit mode\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // turn off autoscale\n    await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();\n\n    await page.getByLabel('Y Axis 1 Minimum value').fill('-1');\n    await page.getByLabel('Y Axis 1 Maximum value').fill('1');\n\n    // save\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // click on pause control\n    await page.getByTitle('Pause incoming real-time data').click();\n    // expect plot to be paused\n    await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();\n    // Wait for 2 seconds to stabilize plot data - future timestamp\n    // eslint-disable-next-line\n    await page.waitForTimeout(2000);\n    // Capture the # of plot points\n    const plotPixels = await getCanvasPixels(page, 'canvas');\n    const plotPixelSizeAtPause = plotPixels.length;\n    // Wait 2 seconds\n    // eslint-disable-next-line\n    await page.waitForTimeout(2000);\n    // Capture the # of plot points\n    const plotPixelsAfterWait = await getCanvasPixels(page, 'canvas');\n    const plotPixelSizeAfterWait = plotPixelsAfterWait.length;\n    // Expect before and after plot points to match\n    await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);\n  });\n\n  /*\n  Test to verify that switching a plot's time context from global to\n  its own independent time context and then back to global context works correctly.\n\n  After switching from fixed time mode (ITC) to real time mode (global context),\n  the pause control for the plot should be available, indicating that it is following the right context.\n  */\n  test('Plots follow the right time context', async ({ page }) => {\n    // Set global time conductor to real-time mode\n    await setRealTimeMode(page);\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // Ensure pause control is visible since global time conductor is in Real time mode.\n    await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();\n\n    // Toggle independent time conductor ON\n    await page.getByLabel('Enable Independent Time Conductor').click();\n\n    // Bring up the independent time conductor popup and switch to fixed time mode\n    await page.getByLabel('Independent Time Conductor Panel').click();\n    await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();\n    await page.getByLabel('Independent Time Conductor Mode Menu').click();\n    await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // Ensure pause control is no longer visible since the plot is following the independent time context\n    await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();\n\n    // Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode\n    await page.getByLabel('Disable Independent Time Conductor').click();\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // Ensure pause control is visible since the global time conductor is in real time mode\n    await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/plotControlsCompactMode.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is dedicated to testing the rendering and interaction of plots.\n *\n */\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Plot Controls in compact mode', () => {\n  let timeStrip;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    timeStrip = await createDomainObjectWithDefaults(page, {\n      type: 'Time Strip'\n    });\n\n    // Create an overlay plot with a sine wave generator\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: timeStrip.uuid\n    });\n    await page.goto(`${timeStrip.url}`);\n  });\n\n  test('Plots show cursor guides', async ({ page }) => {\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // click on cursor guides control\n    await page.getByTitle('Toggle cursor guides').click();\n    await page.getByLabel('Plot Canvas').hover();\n    await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();\n    await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is dedicated to testing the rendering and interaction of plots.\n *\n */\n\nimport {\n  createDomainObjectWithDefaults,\n  createOutOfOrderStateTelemetry,\n  getCanvasPixels,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport { VISUAL_REALTIME_URL } from '../../../../constants.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Plot Rendering', () => {\n  let sineWaveGeneratorObject;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n  });\n\n  test('Plots do not re-request data when a plot is clicked', async ({ page }) => {\n    // Navigate to Sine Wave Generator\n    await page.goto(sineWaveGeneratorObject.url);\n    // Click on the plot canvas\n    await page.locator('canvas').nth(1).click();\n    // No request was made to get historical data\n    const createMineFolderRequests = [];\n    page.on('request', (req) => {\n      createMineFolderRequests.push(req);\n    });\n    expect(createMineFolderRequests.length).toEqual(0);\n    await page.getByLabel('Plot Canvas').hover();\n  });\n\n  test('Time conductor synchronizes with plot time range when that plot control is clicked', async ({\n    page\n  }) => {\n    // Navigate to Sine Wave Generator\n    await page.goto(sineWaveGeneratorObject.url);\n    // Switch to real-time mode\n    await setRealTimeMode(page);\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // click on pause control\n    await page.getByTitle('Pause incoming real-time data').click();\n\n    // expect plot to be paused\n    await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // click on synchronize with time conductor\n    await page.getByTitle('Synchronize Time Conductor').click();\n\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    //confirm that you're now in fixed mode with the correct range\n    await expect(page.getByLabel('Time Conductor Mode')).toHaveText('Fixed Timespan');\n  });\n\n  test('Plot is rendered when infinity values exist', async ({ page }) => {\n    // Edit Plot\n    await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);\n\n    //Get pixel data from Canvas\n    const plotPixels = await getCanvasPixels(page, 'canvas');\n    const plotPixelSize = plotPixels.length;\n    expect(plotPixelSize).toBeGreaterThan(0);\n  });\n});\n\ntest.describe.skip('Plot rendering with out of order data', () => {\n  let telemetry;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_REALTIME_URL, { waitUntil: 'domcontentloaded' });\n\n    telemetry = await createOutOfOrderStateTelemetry(page);\n  });\n\n  test('Out of Order Plot Paused', async ({ page, theme }) => {\n    await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });\n\n    // hover over plot for plot controls\n    await page.getByLabel('Plot Canvas').hover();\n    // click on pause control\n    await page.getByTitle('Pause incoming real-time data').click();\n\n    // there should be no out of order data in the plot. This is verified by checking that the out of order y-axis label is not present in the plot. If the out of order data is present, the y-axis label will be present in the plot.\n    await expect(page.getByText('OUT OF ORDER', { exact: true })).toHaveCount(0);\n  });\n});\n\n/**\n * This function edits a sine wave generator with the default options and enables the infinity values option.\n *\n * @param {import('@playwright/test').Page} page\n * @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject\n * @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.\n */\nasync function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {\n  await page.goto(sineWaveGeneratorObject.url);\n  // Edit SWG properties to include infinity values\n  await page.locator('[title=\"More actions\"]').click();\n  await page.locator('[title=\"Edit properties of this object.\"]').click();\n  await page\n    .getByRole('switch', {\n      name: 'Include Infinity Values'\n    })\n    .check();\n\n  await page\n    .getByRole('button', {\n      name: 'Save'\n    })\n    .click();\n\n  // FIXME: Changes to SWG properties should be reflected on save, but they're not?\n  // Thus, navigate away and back to the object.\n  await page.goto('./#/browse/mine');\n  await page.goto(sineWaveGeneratorObject.url);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify log plot functionality when objects are missing\n*/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst SWG_NAME = 'Sine Wave Generator';\nconst OVERLAY_PLOT_NAME = 'Overlay Plot';\nconst STACKED_PLOT_NAME = 'Stacked Plot';\n\ntest.describe('For a default Plot View, Plot View Action:', () => {\n  let download;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const plot = await createDomainObjectWithDefaults(page, {\n      type: SWG_NAME,\n      name: SWG_NAME\n    });\n\n    await page.goto(plot.url);\n\n    // Set up dialog handler before clicking the export button\n    await page.getByLabel('More actions').click();\n  });\n\n  test.afterEach(async ({ page }) => {\n    if (download) {\n      await download.cancel();\n    }\n  });\n\n  test('Export as PNG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as PNG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);\n  });\n\n  test('Export as JPG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as JPG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);\n  });\n});\n\ntest.describe('For an Overlay Plot View, Plot View Action:', () => {\n  let download;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: OVERLAY_PLOT_NAME,\n      name: OVERLAY_PLOT_NAME\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: SWG_NAME,\n      name: SWG_NAME,\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    // Set up dialog handler before clicking the export button\n    await page.getByLabel('More actions').click();\n  });\n\n  test.afterEach(async ({ page }) => {\n    if (download) {\n      await download.cancel();\n    }\n  });\n\n  test('Export as PNG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as PNG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);\n  });\n\n  test('Export as JPG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as JPG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);\n  });\n});\n\ntest.describe('For a Stacked Plot View, Plot View Action:', () => {\n  let download;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: STACKED_PLOT_NAME,\n      name: STACKED_PLOT_NAME\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: SWG_NAME,\n      name: SWG_NAME,\n      parent: stackedPlot.uuid\n    });\n\n    await page.goto(stackedPlot.url);\n\n    // Set up dialog handler before clicking the export button\n    await page.getByLabel('More actions').click();\n  });\n\n  test.afterEach(async ({ page }) => {\n    if (download) {\n      await download.cancel();\n    }\n  });\n\n  test('Export as PNG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as PNG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);\n  });\n\n  test('Export as JPG, will suggest the correct default filename', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as JPG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);\n  });\n});\n\ntest.describe('Plot View Action:', () => {\n  let download;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const plot = await createDomainObjectWithDefaults(page, {\n      type: SWG_NAME,\n      name: `!@#${SWG_NAME}!@#><`\n    });\n\n    await page.goto(plot.url);\n\n    // Set up dialog handler before clicking the export button\n    await page.getByLabel('More actions').click();\n  });\n\n  test.afterEach(async ({ page }) => {\n    if (download) {\n      await download.cancel();\n    }\n  });\n\n  test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as PNG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);\n  });\n\n  test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {\n    // Start waiting for download before clicking. Note no await.\n    const downloadPromise = page.waitForEvent('download');\n\n    // trigger the download\n    await page.getByLabel('Export as JPG').click();\n\n    download = await downloadPromise;\n\n    // Verify the filename contains the expected pattern\n    expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/previews.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Plots work in Previews', () => {\n  test('We can preview plot in display layouts', async ({ page, openmctConfig }) => {\n    const { myItemsFolderName } = openmctConfig;\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create a Sinewave Generator\n    const sineWaveObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n    // Create a Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Expand the 'My Items' folder in the left tree\n    await page.getByLabel(`Expand ${myItemsFolderName} folder`).click();\n    // Add the Sine Wave Generator to the Display Layout and save changes\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(sineWaveObject.name)\n    });\n    const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');\n    await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // right click on the plot and select view large\n    await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({\n      button: 'right'\n    });\n    await page.getByLabel('View Historical Data').click();\n    await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();\n    await page.getByRole('button', { name: 'Close' }).click();\n    await page.getByLabel('Expand Test Display Layout layout').click();\n\n    // change to a plot and ensure embiggen works\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Move Sub-object Frame').click();\n    await page.getByText('View type').click();\n    await page.getByText('Overlay Plot').click();\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await expect(\n      page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')\n    ).toBeVisible();\n    await expect(page.getByLabel('Preview Container')).toBeHidden();\n    await page.getByLabel('Large View').click();\n    await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();\n    await page.getByRole('button', { name: 'Close' }).click();\n\n    // get last sinewave tree item (in the display layout)\n    await page\n      .getByRole('treeitem', { name: /Sine Wave Generator/ })\n      .locator('a')\n      .last()\n      .click({ button: 'right' });\n    await page.getByLabel('View', { exact: true }).click();\n    await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();\n    await page.getByRole('button', { name: 'Close' }).click();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\n * This test suite is dedicated to testing the Scatter Plot component.\n */\n\nimport { v4 as uuid } from 'uuid';\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Scatter Plot', () => {\n  let scatterPlot;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create the Scatter Plot\n    scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });\n  });\n\n  test('Can add and remove telemetry sources', async ({ page }) => {\n    // Create a sine wave generator within the scatter plot\n    const swg1 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: `swg-${uuid()}`,\n      parent: scatterPlot.uuid\n    });\n\n    // Navigate to the scatter plot and verify that\n    // the SWG appears in the elements pool\n    await page.goto(scatterPlot.url);\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeVisible();\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Create another sine wave generator within the scatter plot\n    const swg2 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: `swg-${uuid()}`,\n      parent: scatterPlot.uuid\n    });\n\n    // Verify that the 'Replace telemetry source' modal appears and accept it\n    await expect(\n      page.getByText(\n        'This action will replace the current telemetry source. Do you want to continue?'\n      )\n    ).toBeVisible();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Navigate to the scatter plot and verify that the new SWG\n    // appears in the elements pool and the old one is gone\n    await page.goto(scatterPlot.url);\n    await page.getByLabel('Edit Object').click();\n\n    // Click the \"Elements\" tab\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden();\n    await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible();\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Right click on the new SWG in the elements pool and delete it\n    await page.getByLabel(`Preview ${swg2.name}`).click({\n      button: 'right'\n    });\n    await page.getByLabel('Remove').click();\n\n    // Verify that the 'Remove object' confirmation modal appears and accept it\n    await expect(\n      page.getByText(\n        'Warning! This action will remove this object. Are you sure you want to continue?'\n      )\n    ).toBeVisible();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Verify that the elements pool shows no elements\n    await expect(page.getByText('No contained elements')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify log plot functionality. Note this test suite if very much under active development and should not\nnecessarily be used for reference when writing new tests in this area.\n*/\n\nimport { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Stacked Plot', () => {\n  let stackedPlot;\n  let swgA;\n  let swgB;\n  let swgC;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'Stacked Plot'\n    });\n\n    swgA = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator A',\n      parent: stackedPlot.uuid\n    });\n    swgB = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator B',\n      parent: stackedPlot.uuid\n    });\n    swgC = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator C',\n      parent: stackedPlot.uuid\n    });\n  });\n\n  test('Using the remove action removes the correct plot', async ({ page }) => {\n    const swgAElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgA.name });\n    const swgBElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgB.name });\n    const swgCElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgC.name });\n\n    await page.goto(stackedPlot.url);\n\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    await swgBElementsPoolItem.click({ button: 'right' });\n    await page\n      .getByRole('menuitem')\n      .filter({ hasText: /Remove/ })\n      .click();\n    await page.getByRole('button').filter({ hasText: 'Ok' }).click();\n\n    await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2);\n\n    // Confirm that the elements pool contains the items we expect\n    await expect(swgAElementsPoolItem).toHaveCount(1);\n    await expect(swgBElementsPoolItem).toHaveCount(0);\n    await expect(swgCElementsPoolItem).toHaveCount(1);\n  });\n\n  test('Can reorder Stacked Plot items', async ({ page }) => {\n    const swgAElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgA.name });\n    const swgBElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgB.name });\n    const swgCElementsPoolItem = page\n      .locator('#inspector-elements-tree')\n      .locator('.c-object-label', { hasText: swgC.name });\n\n    await page.goto(stackedPlot.url);\n\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Elements' }).click();\n\n    const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);\n    const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);\n    const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2);\n\n    // assert initial plot order - [swgA, swgB, swgC]\n    await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);\n    await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);\n    await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);\n\n    // Drag and drop to reorder - [swgB, swgA, swgC]\n    await swgBElementsPoolItem.dragTo(swgAElementsPoolItem);\n\n    // assert plot order after reorder - [swgB, swgA, swgC]\n    await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);\n    await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);\n    await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);\n\n    // Drag and drop to reorder - [swgB, swgC, swgA]\n    await swgCElementsPoolItem.dragTo(swgAElementsPoolItem);\n\n    // assert plot order after second reorder - [swgB, swgC, swgA]\n    await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);\n    await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);\n    await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);\n\n    // collapse inspector\n    await page.locator('.l-shell__pane-inspector .l-pane__collapse-button').click();\n\n    // Save (exit edit mode)\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // assert plot order persists after save - [swgB, swgC, swgA]\n    await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);\n    await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);\n    await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);\n  });\n\n  test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({\n    page\n  }) => {\n    await page.goto(stackedPlot.url);\n\n    // Click on the 1st plot\n    await page\n      .getByLabel('Stacked Plot Item Sine Wave Generator A')\n      .getByLabel('Plot Canvas')\n      .click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Assert that the inspector shows the Y Axis properties for swgA\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })\n    ).toBeVisible();\n\n    // Click on the 2nd plot\n    await page\n      .getByLabel('Stacked Plot Item Sine Wave Generator B')\n      .getByLabel('Plot Canvas')\n      .click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Assert that the inspector shows the Y Axis properties for swgB\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })\n    ).toBeVisible();\n\n    // Click on the 3rd plot\n    await page\n      .getByLabel('Stacked Plot Item Sine Wave Generator C')\n      .getByLabel('Plot Canvas')\n      .click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Assert that the inspector shows the Y Axis properties for swgB\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })\n    ).toBeVisible();\n\n    // Go into edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Click on the 1st plot\n    await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();\n\n    // Assert that the inspector shows the Y Axis properties for swgA\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })\n    ).toBeVisible();\n\n    // Click on the 2nd plot\n    await page.getByLabel('Stacked Plot Item Sine Wave Generator B').click();\n\n    // Assert that the inspector shows the Y Axis properties for swgB\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })\n    ).toBeVisible();\n\n    // Click on the 3rd plot\n    await page.getByLabel('Stacked Plot Item Sine Wave Generator C').click();\n\n    // Assert that the inspector shows the Y Axis properties for swgC\n    await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();\n    await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();\n    await expect(\n      page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })\n    ).toBeVisible();\n  });\n\n  test('Changing properties of an immutable child plot are applied correctly', async ({ page }) => {\n    await page.goto(stackedPlot.url);\n\n    // Go into edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // Click on canvas for the 1st plot\n    await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Expand config for the series\n    await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();\n\n    // turn off alarm markers\n    await page.getByLabel('Alarm Markers').uncheck();\n\n    // save\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // reload page and waitForPlotsToRender\n    await page.reload();\n    await waitForPlotsToRender(page);\n\n    // Click on canvas for the 1st plot\n    await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    // Expand config for the series\n    await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();\n\n    // Assert that alarm markers are still turned off\n    await expect(\n      page\n        .getByTitle('Display markers visually denoting points in alarm.')\n        .getByRole('cell', { name: 'Disabled' })\n    ).toBeVisible();\n  });\n\n  test('the legend toggles between aggregate and per child', async ({ page }) => {\n    await page.goto(stackedPlot.url);\n\n    await waitForPlotsToRender(page);\n\n    // Go into edit mode\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Config' }).click();\n\n    const legendProperties = page.getByLabel('Legend Properties');\n    await legendProperties.locator('[title=\"Display legends per sub plot.\"]~div input').uncheck();\n\n    await assertAggregateLegendIsVisible(page);\n\n    // Save (exit edit mode)\n    await page.locator('button[title=\"Save\"]').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await waitForPlotsToRender(page);\n\n    await assertAggregateLegendIsVisible(page);\n\n    await page.reload();\n\n    await waitForPlotsToRender(page);\n\n    await assertAggregateLegendIsVisible(page);\n  });\n\n  test('can toggle between aggregate and per child legends', async ({ page }) => {\n    // make some an overlay plot\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      parent: stackedPlot.uuid\n    });\n\n    // make some SWGs for the overlay plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(stackedPlot.url);\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Inspector Views').getByRole('checkbox').uncheck();\n    await page.getByLabel('Expand By Default').check();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);\n\n    // reload and ensure the legend is still expanded\n    await page.reload();\n    await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);\n\n    // change to collapsed by default\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Expand By Default').uncheck();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(1);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);\n\n    // change it to individual legends\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Show Legends For Children').check();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(4);\n    await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);\n  });\n});\n\n/**\n * Asserts that aggregate stacked plot legend is visible\n * @param {import('@playwright/test').Page} page\n */\nasync function assertAggregateLegendIsVisible(page) {\n  // Wait for plot series data to load\n  await waitForPlotsToRender(page);\n  // Wait for plot legend to be shown\n  await expect(page.locator('.js-stacked-plot-legend')).toBeVisible();\n  // There should be 3 legend items\n  await expect(\n    page.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')\n  ).toHaveCount(3);\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/tagging.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify plot tagging functionality.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  setFixedTimeMode,\n  setRealTimeMode,\n  waitForPlotsToRender\n} from '../../../../appActions.js';\nimport { basicTagsTests, createTags, testTelemetryItem } from '../../../../helper/plotTagsUtils.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Plot Tagging', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Tags work with Overlay Plots', async ({ page }) => {\n    test.slow();\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6822'\n    });\n\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    const alphaSineWave = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave',\n      parent: overlayPlot.uuid,\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.01'\n      }\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave',\n      parent: overlayPlot.uuid,\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.02'\n      }\n    });\n\n    await page.goto(overlayPlot.url);\n\n    let canvas = page.locator('canvas').nth(1);\n\n    // Switch to real-time mode\n    // Adding tags should pause the plot\n    await setRealTimeMode(page);\n\n    await createTags({\n      page,\n      canvas\n    });\n\n    await setFixedTimeMode(page);\n\n    await basicTagsTests(page);\n    await testTelemetryItem(page, alphaSineWave);\n\n    // set to real time mode\n    await setRealTimeMode(page);\n\n    // Search for Science Tag\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');\n\n    // Click on the search object result\n    await page.getByLabel('OpenMCT Search').getByText('Alpha Sine Wave').first().click();\n\n    await waitForPlotsToRender(page);\n\n    // expect plot to be paused\n    await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();\n\n    await setFixedTimeMode(page);\n  });\n\n  test('Tags work with Plot View of telemetry items', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.01'\n      }\n    });\n    const canvas = page.locator('canvas').nth(1);\n    await createTags({\n      page,\n      canvas\n    });\n    await basicTagsTests(page);\n  });\n\n  test('Plots use index to retrieve tags @couchdb @network', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/8184'\n    });\n    // Switch to real-time mode\n    await setRealTimeMode(page);\n\n    const tagsRequestPromise = new Promise((resolve) => {\n      page.on('request', async (request) => {\n        const isTagsRequest = request.url().endsWith('by_keystring');\n        if (isTagsRequest) {\n          const response = await request.response();\n          resolve(response.status() === 200);\n        }\n      });\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n\n    const pauseButton = page.getByLabel('Pause incoming real-time data');\n    pauseButton.click();\n\n    const didUseIndexForTagsRequest = await tagsRequestPromise;\n\n    expect(didUseIndexForTagsRequest).toBe(true);\n  });\n\n  test('Tags work with Stacked Plots', async ({ page }) => {\n    const stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot'\n    });\n\n    const alphaSineWave = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave',\n      parent: stackedPlot.uuid,\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.01'\n      }\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave',\n      parent: stackedPlot.uuid,\n      customParameters: {\n        '[aria-label=\"Data Rate (hz)\"]': '0.02'\n      }\n    });\n\n    await page.goto(stackedPlot.url);\n\n    const canvas = page.locator('canvas').nth(1);\n\n    await createTags({\n      page,\n      canvas,\n      xEnd: 700,\n      yEnd: 240\n    });\n    await basicTagsTests(page);\n    await testTelemetryItem(page, alphaSineWave);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/plot/timeTicks.e2e.spec.js",
    "content": "import { expect, test } from '@playwright/test';\n\nimport { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';\n\ntest.describe('Time Tick Generation', () => {\n  // Test cases will go here\n  let sineWaveGeneratorObject;\n\n  test.beforeEach(async ({ page }) => {\n    // Open a browser, navigate to the main page, and wait until all networkevents to resolve\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n\n    // Navigate to Sine Wave Generator\n    await page.goto(sineWaveGeneratorObject.url);\n  });\n\n  test('Plot time-series ticks are functionally correct over a period of 6 months, between two years', async ({\n    page\n  }) => {\n    const startDate = '2022-09-01';\n    const startTime = '22:00:00';\n    const endDate = '2023-03-01';\n    const endTime = '22:00:30';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    await testYearTimeSeriesTicks(page);\n  });\n\n  test('Plot time-series ticks are functionally correct over a period of days', async ({\n    page\n  }) => {\n    const startDate = '2023-03-22';\n    const startTime = '00:00:00';\n    const endDate = '2023-04-20';\n    const endTime = '12:00:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    await testDaysTimeSeriesTicks(page);\n  });\n\n  test('Plot time-series ticks are functionally correct over a period of hours', async ({\n    page\n  }) => {\n    const startDate = '2023-03-22';\n    const startTime = '01:15:00';\n    const endDate = '2023-03-22';\n    const endTime = '09:15:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    await testHoursTimeSeriesTicks(page);\n  });\n\n  test('Plot time-series ticks are functionally correct over a period of minutes', async ({\n    page\n  }) => {\n    const startDate = '2023-03-22';\n    const startTime = '01:15:00';\n    const endDate = '2023-03-22';\n    const endTime = '01:35:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    await testMinutesTimeSeriesTicks(page);\n  });\n\n  test('Plot time-series ticks are functionally correct over a period of seconds', async ({\n    page\n  }) => {\n    const startDate = '2023-03-22';\n    const startTime = '01:22:00';\n    const endDate = '2023-03-22';\n    const endTime = '01:23:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    await testSecondsTimeSeriesTicks(page);\n  });\n\n  test('Plot snaps to 2-minute intervals for an irregular 17-minute range', async ({ page }) => {\n    // Range: 01:15:00 to 01:32:00 (17 minutes)\n    const startDate = '2023-03-22';\n    const startTime = '01:15:00';\n    const endDate = '2023-03-22';\n    const endTime = '01:32:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    const xTicks = page.locator('.gl-plot-x-tick-label');\n\n    // 2-minute increment\n    await expect(xTicks.nth(0)).toHaveText('01:16:00');\n    await expect(xTicks.nth(1)).toHaveText('01:18:00');\n    await expect(xTicks.nth(2)).toHaveText('01:20:00');\n    await expect(xTicks.nth(3)).toHaveText('01:22:00');\n    await expect(xTicks.nth(4)).toHaveText('01:24:00');\n    await expect(xTicks.nth(5)).toHaveText('01:26:00');\n    await expect(xTicks.nth(6)).toHaveText('01:28:00');\n    await expect(xTicks.nth(7)).toHaveText('01:30:00');\n  });\n\n  test('Ticks align to round hour boundaries despite off-grid start time', async ({ page }) => {\n    // Range starts at 01:13:42\n    const startDate = '2023-03-22';\n    const startTime = '01:13:42';\n    const endDate = '2023-03-22';\n    const endTime = '05:13:42';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    const xTicks = page.locator('.gl-plot-x-tick-label');\n\n    await expect(xTicks.nth(0)).toHaveText('01:20:00');\n    await expect(xTicks.nth(1)).toHaveText('02:10:00');\n    await expect(xTicks.nth(2)).toHaveText('03:00:00');\n    await expect(xTicks.nth(3)).toHaveText('03:50:00');\n    await expect(xTicks.nth(4)).toHaveText('04:40:00');\n  });\n\n  test('Plot utilizes 12-hour snapping for a multi-day range', async ({ page }) => {\n    // 48 hour range\n    const startDate = '2023-03-22';\n    const startTime = '00:00:00';\n    const endDate = '2023-03-24';\n    const endTime = '00:00:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    const xTicks = page.locator('.gl-plot-x-tick-label');\n    // 12-hour increments\n    await expect(xTicks.nth(0)).toHaveText('2023-03-22 00:00:00');\n    await expect(xTicks.nth(1)).toHaveText('2023-03-22 12:00:00');\n    await expect(xTicks.nth(2)).toHaveText('2023-03-23 00:00:00');\n    await expect(xTicks.nth(3)).toHaveText('2023-03-23 12:00:00');\n    await expect(xTicks.nth(4)).toHaveText('2023-03-24 00:00:00');\n  });\n\n  test('Plot reduces tick count when the container is resized to be smaller', async ({ page }) => {\n    // 8-hour range\n    const startDate = '2023-03-22';\n    const startTime = '01:00:00';\n    const endDate = '2023-03-22';\n    const endTime = '09:00:00';\n    await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });\n\n    const xTicks = page.locator('.gl-plot-x-tick-label');\n\n    const initialCount = await xTicks.count();\n    expect(initialCount).toBeGreaterThan(3);\n\n    await page.setViewportSize({ width: 600, height: 600 });\n\n    // Verify that the tick count has decreased to avoid overlap\n    await expect\n      .poll(async () => {\n        const newCount = await xTicks.count();\n        return newCount;\n      })\n      .toBeLessThan(initialCount);\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n */\nasync function testYearTimeSeriesTicks(page) {\n  const xTicks = page.locator('.gl-plot-x-tick-label');\n  await expect(xTicks).toHaveCount(4);\n  await expect(xTicks.nth(0)).toHaveText('2022-09-01');\n  await expect(xTicks.nth(1)).toHaveText('2022-11-01');\n  await expect(xTicks.nth(2)).toHaveText('2023-01-01');\n  await expect(xTicks.nth(3)).toHaveText('2023-03-01');\n}\n\nasync function testDaysTimeSeriesTicks(page) {\n  const xTicks = page.locator('.gl-plot-x-tick-label');\n  await expect(xTicks).toHaveCount(5);\n  await expect(xTicks.nth(0)).toHaveText('2023-03-23');\n  await expect(xTicks.nth(1)).toHaveText('2023-03-30');\n  await expect(xTicks.nth(2)).toHaveText('2023-04-06');\n  await expect(xTicks.nth(3)).toHaveText('2023-04-13');\n  await expect(xTicks.nth(4)).toHaveText('2023-04-20');\n}\n\nasync function testHoursTimeSeriesTicks(page) {\n  const xTicks = page.locator('.gl-plot-x-tick-label');\n  await expect(xTicks).toHaveCount(8);\n  await expect(xTicks.nth(0)).toHaveText('02:00:00');\n  await expect(xTicks.nth(1)).toHaveText('03:00:00');\n  await expect(xTicks.nth(2)).toHaveText('04:00:00');\n  await expect(xTicks.nth(3)).toHaveText('05:00:00');\n  await expect(xTicks.nth(4)).toHaveText('06:00:00');\n  await expect(xTicks.nth(5)).toHaveText('07:00:00');\n  await expect(xTicks.nth(6)).toHaveText('08:00:00');\n  await expect(xTicks.nth(7)).toHaveText('09:00:00');\n}\n\nasync function testMinutesTimeSeriesTicks(page) {\n  const xTicks = page.locator('.gl-plot-x-tick-label');\n\n  await expect(xTicks).toHaveCount(10);\n  await expect(xTicks.nth(0)).toHaveText('01:16:00');\n  await expect(xTicks.nth(1)).toHaveText('01:18:00');\n  await expect(xTicks.nth(2)).toHaveText('01:20:00');\n  await expect(xTicks.nth(3)).toHaveText('01:22:00');\n  await expect(xTicks.nth(4)).toHaveText('01:24:00');\n  await expect(xTicks.nth(5)).toHaveText('01:26:00');\n  await expect(xTicks.nth(6)).toHaveText('01:28:00');\n  await expect(xTicks.nth(7)).toHaveText('01:30:00');\n  await expect(xTicks.nth(8)).toHaveText('01:32:00');\n  await expect(xTicks.nth(9)).toHaveText('01:34:00');\n}\n\nasync function testSecondsTimeSeriesTicks(page) {\n  const xTicks = page.locator('.gl-plot-x-tick-label');\n  await expect(xTicks).toHaveCount(7);\n  await expect(xTicks.nth(0)).toHaveText('01:22:00');\n  await expect(xTicks.nth(1)).toHaveText('01:22:10');\n  await expect(xTicks.nth(2)).toHaveText('01:22:20');\n  await expect(xTicks.nth(3)).toHaveText('01:22:30');\n  await expect(xTicks.nth(4)).toHaveText('01:22:40');\n  await expect(xTicks.nth(5)).toHaveText('01:22:50');\n  await expect(xTicks.nth(6)).toHaveText('01:23:00');\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Reload action', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n\n    const alphaTable = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      name: 'Alpha Table'\n    });\n\n    const betaTable = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      name: 'Beta Table'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: alphaTable.uuid\n    });\n    await page.getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: /Edit Properties/ }).click();\n    await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001');\n    await page.getByLabel('Save').click();\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: betaTable.uuid\n    });\n    await page.getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: /Edit Properties/ }).click();\n    await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001');\n    await page.getByLabel('Save').click();\n\n    await page.goto(displayLayout.url);\n\n    // Expand all folders\n    await expandEntireTree(page);\n\n    await page.getByLabel('Edit Object', { exact: true }).click();\n\n    await page\n      .getByLabel('Main Tree')\n      .getByLabel(`Preview ${alphaTable.name}`)\n      .dragTo(page.getByLabel('Layout Grid'), {\n        targetPosition: { x: 0, y: 0 }\n      });\n\n    await page\n      .getByLabel('Main Tree')\n      .getByLabel(`Preview ${betaTable.name}`)\n      .dragTo(page.getByLabel('Layout Grid'), {\n        targetPosition: { x: 0, y: 250 }\n      });\n\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n  });\n\n  test('can reload display layout and its children', async ({ page }) => {\n    const beforeReloadAlphaTelemetryValue = page\n      .getByLabel('Alpha Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n    const beforeReloadBetaTelemetryValue = await page\n      .getByLabel('Beta Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n    // reload alpha\n    await page.getByTitle('View menu items').first().click();\n    await page.getByRole('menuitem', { name: /Reload/ }).click();\n\n    const afterReloadAlphaTelemetryValue = await page\n      .getByLabel('Alpha Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n    const afterReloadBetaTelemetryValue = await page\n      .getByLabel('Beta Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n\n    expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);\n    expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue);\n\n    // now reload parent\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: /Reload/ }).click();\n\n    const fullReloadAlphaTelemetryValue = await page\n      .getByLabel('Alpha Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n    const fullReloadBetaTelemetryValue = await page\n      .getByLabel('Beta Table table content')\n      .getByLabel('wavelengths table cell')\n      .first()\n      .getAttribute('title');\n\n    expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);\n    expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue);\n  });\n\n  test('is disabled in Previews', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7638'\n    });\n    await page.getByLabel('Alpha Table Frame Controls').getByLabel('Large View').click();\n    await page.getByLabel('Modal Overlay').getByLabel('More actions').click();\n    await expect(page.getByLabel('Reload')).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*\nThis test suite is dedicated to tests which verify the basic operations surrounding conditionSets and styling\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  linkParameterToObject,\n  setRealTimeMode\n} from '../../../../appActions.js';\nimport { MISSION_TIME } from '../../../../constants.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Conditionally Styling, using a Condition Set', () => {\n  let stateGenerator;\n  let conditionSet;\n  let displayLayout;\n  const STATE_CHANGE_INTERVAL = '1';\n\n  test.beforeEach(async ({ page }) => {\n    // Install the clock and set the time to the mission time such that the state generator will be controllable\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create Condition Set, State Generator, and Display Layout\n    conditionSet = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Test Condition Set'\n    });\n    stateGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'State Generator',\n      name: 'One Second State Generator'\n    });\n    // edit the state generator to have a 1 second update rate\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page.getByLabel('State Duration (seconds)', { exact: true }).fill(STATE_CHANGE_INTERVAL);\n    await page.getByLabel('Save').click();\n\n    displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n  });\n\n  test('Conditional styling, using a Condition Set, will style correctly based on the output @clock', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7840'\n    });\n\n    // set up the condition set to use the state generator\n    await page.goto(conditionSet.url, { waitUntil: 'domcontentloaded' });\n\n    // Add the State Generator to the Condition Set by dragging from the main tree\n    await page.getByLabel('Show selected item in tree').click();\n    await page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: stateGenerator.name\n      })\n      .dragTo(page.locator('#conditionCollection'));\n\n    // Add the state generator to the first criterion such that there is a condition named 'OFF' when the state generator is off\n    await page.getByLabel('Add Condition').click();\n    await page\n      .getByLabel('Criterion Telemetry Selection')\n      .selectOption({ label: stateGenerator.name });\n    await page.getByLabel('Criterion Metadata Selection').selectOption({ label: 'State' });\n    await page.getByLabel('Criterion Comparison Selection').selectOption({ label: 'is' });\n    await page.getByLabel('Condition Name Input').first().fill('OFF');\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await linkParameterToObject(page, stateGenerator.name, displayLayout.name);\n\n    //Add a box to the display layout\n    await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' });\n    await page.getByLabel('Edit Object').click();\n\n    //Add a box to the display layout and move it to the right\n    //TEMP: Click the layout such that the state generator is deselected\n    await page.getByLabel('Test Display Layout Layout Grid').locator('div').nth(1).click();\n    await page.getByLabel('Add Drawing Object').click();\n    await page.getByText('Box').click();\n    await page.getByLabel('X:').click();\n    await page.getByLabel('X:').fill('10');\n    await page.getByLabel('X:').press('Enter');\n\n    // set up conditional styling such that the box is red when the state generator condition is 'OFF'\n    await page.getByRole('tab', { name: 'Styles' }).click();\n    await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();\n    await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();\n    await page.getByLabel('Modal Overlay').getByLabel(`Preview ${conditionSet.name}`).click();\n    await page.getByText('Ok').click();\n    await page.getByLabel('Set background color').first().click();\n    await page.getByLabel('#ff0000').click();\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await setRealTimeMode(page);\n\n    //Pause at a time when the state generator is 'OFF' which is 20 minutes in the future\n    await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));\n\n    const redBG = 'background-color: rgb(255, 0, 0);';\n    const defaultBG = 'background-color: rgb(102, 102, 102);';\n    const textElement = page.getByLabel('Alpha-numeric telemetry value').locator('div:first-child');\n    const styledElement = page.getByLabel('Box', { exact: true });\n\n    await page.clock.resume();\n\n    // Check if the style is red when text is 'OFF'\n    await expect(textElement).toHaveText('OFF');\n    await waitForStyleChange(styledElement, redBG);\n\n    // Fast forward to the next state change\n    await page.clock.fastForward(STATE_CHANGE_INTERVAL * 1000);\n\n    // Check if the style is not red when text is 'ON'\n    await expect(textElement).toHaveText('ON');\n    await waitForStyleChange(styledElement, defaultBG);\n  });\n});\n\n/**\n * Wait for the style of an element to change to the expected style.\n * @param {import('@playwright/test').Locator} element - The element to check.\n * @param {string} expectedStyle - The expected style to wait for.\n * @param {number} timeout - The timeout in milliseconds.\n */\nasync function waitForStyleChange(element, expectedStyle, timeout = 0) {\n  await expect(async () => {\n    const style = await element.getAttribute('style');\n\n    // eslint-disable-next-line playwright/prefer-web-first-assertions\n    expect(style).toBe(expectedStyle);\n  }).toPass({ timeout: 1000 }); // timeout allows for the style to be applied\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/conditional/displayLayoutConditionalStyling.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { fileURLToPath } from 'url';\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithRealTime\n} from '../../../../../appActions.js';\nimport { expect, test } from '../../../../../pluginFixtures.js';\n\nconst TINY_IMAGE_BASE64 =\n  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';\n\ntest.describe('Display Layout Conditional Styling', () => {\n  test.use({\n    storageState: fileURLToPath(\n      new URL('../../../../../test-data/condition_set_storage.json', import.meta.url)\n    )\n  });\n\n  let displayLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n  });\n\n  test('Image Drawing Object can have visibility toggled conditionally', async ({ page }) => {\n    await page.getByLabel('Edit Object').click();\n\n    // Add Image Drawing Object to the layout\n    await page.getByLabel('Add Drawing Object').click();\n    await page.getByLabel('Image').click();\n    await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);\n    await page.getByText('Ok').click();\n\n    // Use the \"Test Condition Set\" for conditional styling on the image\n    await page.getByRole('tab', { name: 'Styles' }).click();\n    await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();\n    await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();\n    await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();\n    await page.getByText('Ok').click();\n\n    // Set the image to be hidden when the condition is met\n    await page.getByTitle('Visible').first().click();\n    await page.getByLabel('Save Style').first().click();\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Switch to real-time mode and verify that the image toggles visibility\n    await navigateToObjectWithRealTime(page, displayLayout.url);\n    await expect(page.getByLabel('Image View')).toBeVisible();\n    await expect(page.getByLabel('Image View')).toBeHidden();\n\n    // Reload the page and verify that the image toggles visibility\n    await page.reload({ waitUntil: 'domcontentloaded' });\n    await expect(page.getByLabel('Image View')).toBeVisible();\n    await expect(page.getByLabel('Image View')).toBeHidden();\n  });\n\n  test('Alphanumeric object can have visibility toggled conditionally', async ({ page }) => {\n    await page.getByLabel('Edit Object').click();\n\n    // Add Alphanumeric Object to the layout\n    await page.getByLabel('Expand My Items folder').click();\n    await page.getByLabel('Expand Test Condition Set').click();\n    await page.getByLabel('Preview VIPER Rover Heading').dragTo(page.getByLabel('Layout Grid'));\n\n    // Use the \"Test Condition Set\" for conditional styling on the alphanumeric\n    await page.getByRole('tab', { name: 'Styles' }).click();\n    await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();\n    await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();\n    await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();\n    await page.getByText('Ok').click();\n\n    // Set the alphanumeric to be hidden when the condition is met\n    await page.getByTitle('Visible').first().click();\n    await page.getByLabel('Save Style').first().click();\n    await page.getByLabel('Save', { exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Switch to real-time mode and verify that the image toggles visibility\n    await navigateToObjectWithRealTime(page, displayLayout.url);\n    await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();\n    await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();\n\n    // Reload the page and verify that the alphanumeric toggles visibility\n    await page.reload({ waitUntil: 'domcontentloaded' });\n    await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();\n    await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/conditionalStyling.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test conditional styling\n */\n\nimport { test } from '../../../../pluginFixtures.js';\n\ntest.describe('Conditional Styling', () => {\n  test.fixme(\n    'Conditional Styling can be applied to Flex Layout and its children',\n    async ({ page }) => {\n      //test\n    }\n  );\n  test.fixme(\n    'Conditional Styling can be applied to Overlay Plot and its children',\n    async ({ page }) => {\n      //test\n    }\n  );\n  test.fixme(\n    'Conditional Styling changes the styling of the object the condition changes state',\n    async ({ page }) => {\n      //test\n    }\n  );\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/flexLayoutStyling.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test styling of flex layouts\n */\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { checkStyles, hexToRGB, setStyles } from '../../../../helper/stylingUtils.js';\nimport { test } from '../../../../pluginFixtures.js';\n\nconst setBorderColor = '#ff00ff';\nconst setBackgroundColor = '#5b0f00';\nconst setTextColor = '#e6b8af';\nconst defaultFrameBorderColor = '#e6b8af'; //default border color\nconst defaultBorderTargetColor = '#acacac';\nconst defaultTextColor = '#acacac'; // default text color\nconst inheritedColor = '#acacac'; // inherited from the body style\nconst pukeGreen = '#6aa84f'; //Ugliest green known to man 🤮\nconst NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value\n\ntest.describe('Flexible Layout styling', () => {\n  let stackedPlot;\n  let flexibleLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Flexible Layout and attach to the Stacked Plot\n    flexibleLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout',\n      name: 'Flexible Layout'\n    });\n\n    // Create a Stacked Plot and attach to the Flexible Layout\n    stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot1',\n      parent: flexibleLayout.uuid\n    });\n\n    // Create a Stacked Plot and attach to the Flexible Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot2',\n      parent: flexibleLayout.uuid\n    });\n  });\n\n  test('styling the flexible layout properly applies the styles to flex layout', async ({\n    page\n  }) => {\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Set styles using setStyles function\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    // Flex Layout Column matches set styles\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Check styles of overall Flex Layout\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    // Check styles on StackedPlot1. Note: https://github.com/nasa/openmct/issues/7337\n    await checkStyles(\n      hexToRGB(defaultFrameBorderColor),\n      NO_STYLE_RGBA,\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337\n    await checkStyles(\n      hexToRGB(defaultFrameBorderColor),\n      NO_STYLE_RGBA,\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n  });\n\n  test('styling a child object of the flexible layout properly applies that style to only that child', async ({\n    page\n  }) => {\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Set styles using setStyles function on StackedPlot1 but not StackedPlot2\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('group', { name: 'StackedPlot1 Frame' })\n    );\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page and verify that styles persist\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n  });\n\n  test('if an overall style has been applied to the parent layout or plot, the individual styling should be able to coexist with that', async ({\n    page\n  }) => {\n    //Navigate to stackedPlot\n    await page.goto(stackedPlot.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit stackedPlot\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Set styles using setStyles function on StackedPlot1 but not StackedPlot2\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Stacked Plot Style Target').locator('div')\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Check styles on StackedPlot1 to match the set colors\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2 to verify they are the default\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Set styles using setStyles function on StackedPlot2\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('group', { name: 'StackedPlot2 Frame' })\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page and verify that styles persist\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Set Flex Layout Column to puke green\n    await setStyles(\n      page,\n      pukeGreen,\n      pukeGreen,\n      pukeGreen,\n      page.getByLabel('Flexible Layout Column')\n    );\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Flex Layout Column matches set styles\n    await checkStyles(\n      hexToRGB(pukeGreen),\n      hexToRGB(pukeGreen),\n      hexToRGB(pukeGreen),\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    // Check styles on StackedPlot1 matches previously set colors\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Check styles on StackedPlot2 matches previous set colors\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot2 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n  });\n\n  test('when the \"no style\" option is selected, background and text should be reset to inherited styles', async ({\n    page\n  }) => {\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    // Set styles using setStyles function\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('group', { name: 'StackedPlot1 Frame' })\n    );\n\n    // Check styles using checkStyles function\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page and set Styles to 'None'\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    //Select the 'No Style' option\n    await setStyles(\n      page,\n      'No Style',\n      'No Style',\n      'No Style',\n      page.getByRole('group', { name: 'StackedPlot1 Frame' })\n    );\n\n    // Check styles using checkStyles function\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(inheritedColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page and verify that styles persist\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Check styles using checkStyles function\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(inheritedColor),\n      page\n        .getByRole('group', { name: 'StackedPlot1 Frame' })\n        .getByLabel('Stacked Plot Style Target')\n    );\n  });\n\n  test('Styling, and then canceling reverts to previous style', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7233'\n    });\n\n    await page.goto(flexibleLayout.url);\n\n    await page.getByLabel('Edit Object').click();\n    await page.getByRole('tab', { name: 'Styles' }).click();\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Flexible Layout Column')\n    );\n    await page.getByLabel('Cancel Editing').click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(inheritedColor),\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    await page.reload();\n    await checkStyles(\n      hexToRGB(defaultBorderTargetColor),\n      NO_STYLE_RGBA,\n      hexToRGB(inheritedColor),\n      page.getByLabel('Flexible Layout Column')\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/stackedPlotStyling.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test styling of stacked plots\n */\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport {\n  checkFontStyles,\n  checkStyles,\n  hexToRGB,\n  setStyles\n} from '../../../../helper/stylingUtils.js';\nimport { test } from '../../../../pluginFixtures.js';\n\nconst setBorderColor = '#ff00ff';\nconst setBackgroundColor = '#5b0f00';\nconst setTextColor = '#e6b8af';\nconst defaultTextColor = '#acacac'; // default text color\nconst NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value\nconst DEFAULT_PLOT_VIEW_BORDER_COLOR = '#acacac';\nconst setFontSize = '72px';\nconst setFontWeight = '700'; //bold for monospace bold\nconst setFontFamily = '\"Andale Mono\", sans-serif';\n\ntest.describe('Stacked Plot styling', () => {\n  let stackedPlot;\n  let overlayPlot1;\n  let overlayPlot2;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Stacked Plot\n    stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot1'\n    });\n\n    // create two overlay plots\n    overlayPlot1 = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Overlay Plot 1',\n      parent: stackedPlot.uuid\n    });\n\n    overlayPlot2 = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Overlay Plot 2',\n      parent: stackedPlot.uuid\n    });\n\n    // Create two SWGs and attach them to the Stacked Plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 1',\n      parent: overlayPlot1.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 2',\n      parent: overlayPlot2.uuid\n    });\n  });\n\n  test('styling the overlay plot properly applies the styles to all containers', async ({\n    page\n  }) => {\n    // Directly navigate to the stacked plot\n    await page.goto(stackedPlot.url, { waitUntil: 'domcontentloaded' });\n\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    //Set styles on overall stacked plot\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('tab', { name: 'Styles' }) //Workaround for https://github.com/nasa/openmct/issues/7229\n    );\n\n    //Set Font Size to 72\n    await page.getByLabel('Set Font Size').click();\n    await page.getByRole('menuitem', { name: '72px' }).click();\n\n    //Set Font Type to Monospace Bold. See setFontWeight and setFontFamily variables\n    await page.getByLabel('Set Font Type').click();\n    await page.getByRole('menuitem', { name: 'Monospace Bold' }).click();\n\n    //Check styles of stacked plot\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Stacked Plot Style Target')\n    );\n\n    //Check font styles of stacked plot\n    await checkFontStyles(\n      setFontSize,\n      setFontWeight,\n      setFontFamily,\n      page.getByLabel('Stacked Plot Style Target')\n    );\n\n    //Save\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Reload page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    //Verify styles are correct after reload\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Stacked Plot Style Target')\n    );\n\n    await checkFontStyles(\n      setFontSize,\n      setFontWeight,\n      setFontFamily,\n      page.getByLabel('Stacked Plot Style Target')\n    );\n\n    //Verify that stacked Plot Items inherit only text properties\n    await checkStyles(\n      NO_STYLE_RGBA,\n      NO_STYLE_RGBA,\n      hexToRGB(setTextColor),\n      page.getByLabel('Stacked Plot Item Overlay Plot 1')\n    );\n\n    await checkStyles(\n      NO_STYLE_RGBA,\n      NO_STYLE_RGBA,\n      hexToRGB(setTextColor),\n      page.getByLabel('Stacked Plot Item Overlay Plot 2')\n    );\n\n    await checkFontStyles(\n      setFontSize,\n      setFontWeight,\n      setFontFamily,\n      page.getByLabel('Stacked Plot Item Overlay Plot 1')\n    );\n  });\n\n  test('styling a child object of the flexible layout properly applies that style to only that child', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7338'\n    });\n    await page.goto(stackedPlot.url, { waitUntil: 'domcontentloaded' });\n\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    //Check default styles for overlayPlot1 and overlayPlot2\n    await checkStyles(\n      NO_STYLE_RGBA,\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page.getByLabel('Stacked Plot Item Overlay Plot 1')\n    );\n\n    await checkStyles(\n      NO_STYLE_RGBA,\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page.getByLabel('Stacked Plot Item Overlay Plot 2')\n    );\n\n    // Set styles using setStyles function on StackedPlot1 but not StackedPlot2\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Stacked Plot Item Overlay Plot 1')\n    );\n\n    //Set Font Styles on SWG1 but not SWG2\n    await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();\n    //Set Font Size to 72\n    await page.getByLabel('Set Font Size').click();\n    await page.getByRole('menuitem', { name: '72px' }).click();\n\n    //Set Font Type to Monospace Bold. See setFontWeight and setFontFamily variables\n    await page.getByLabel('Set Font Type').click();\n    await page.getByRole('menuitem', { name: 'Monospace Bold' }).click();\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Plot Container Style Target').first()\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(DEFAULT_PLOT_VIEW_BORDER_COLOR),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page.getByLabel('Plot Container Style Target').nth(1)\n    );\n\n    // Reload page and verify that styles persist\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // Check styles on StackedPlot1\n    await checkStyles(\n      hexToRGB(setBorderColor),\n      hexToRGB(setBackgroundColor),\n      hexToRGB(setTextColor),\n      page.getByLabel('Plot Container Style Target').first()\n    );\n\n    // Check styles on StackedPlot2\n    await checkStyles(\n      hexToRGB(DEFAULT_PLOT_VIEW_BORDER_COLOR),\n      NO_STYLE_RGBA,\n      hexToRGB(defaultTextColor),\n      page.getByLabel('Plot Container Style Target').nth(1)\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/styling/styleInspectorOptions.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test styling changes in the inspector tool\n */\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Style Inspector Options', () => {\n  let flexibleLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Flexible Layout and attach to the Stacked Plot\n    flexibleLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout',\n      name: 'Flexible Layout'\n    });\n    // Create a Stacked Plot and attach to the Flexible Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'Stacked Plot',\n      parent: flexibleLayout.uuid\n    });\n  });\n\n  test('styles button only appears when appropriate component selected', async ({ page }) => {\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // The overall Flex Layout or Stacked Plot itself MUST be style-able.\n    await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();\n\n    // Select flexible layout column\n    await page.getByLabel('Container Handle 1').click();\n\n    // Flex Layout containers should NOT be style-able.\n    await expect(page.getByRole('tab', { name: 'Styles' })).toBeHidden();\n\n    // Select Flex Layout Column\n    await page.getByLabel('Flexible Layout Column').click();\n\n    // The overall Flex Layout or Stacked Plot itself MUST be style-able.\n    await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();\n\n    // Select Stacked Layout Column\n    await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();\n\n    // The overall Flex Layout or Stacked Plot itself MUST be style-able.\n    await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();\n  });\n});\n\ntest.describe('Saved Styles', () => {\n  test.fixme('historical styles appear as an option once used', async ({ page }) => {\n    //test\n  });\n  test.fixme('at least 5 saved styles appear in the saved styles list', async ({ page }) => {\n    //test\n  });\n  test.fixme('Saved Styles can be deleted once used', async ({ page }) => {\n    //test\n  });\n  test.fixme('can apply a saved style to the currently selected target', async ({ page }) => {\n    //test\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Tabs View', () => {\n  let tabsView;\n  let table;\n  let notebook;\n  let sineWaveGenerator;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    tabsView = await createDomainObjectWithDefaults(page, {\n      type: 'Tabs View'\n    });\n    table = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      parent: tabsView.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: table.uuid\n    });\n    notebook = await createDomainObjectWithDefaults(page, {\n      type: 'Notebook',\n      parent: tabsView.uuid\n    });\n    sineWaveGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: tabsView.uuid\n    });\n  });\n\n  test('Renders tabbed elements', async ({ page }) => {\n    await page.goto(tabsView.url);\n\n    // select first tab\n    await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();\n    // ensure table header visible\n    await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();\n\n    // no canvas (i.e., sine wave generator) in the document should be visible\n    await expect(page.locator('canvas[id=webglContext]')).toBeHidden();\n\n    // select second tab\n    await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();\n\n    // ensure notebook visible\n    await expect(page.locator('.c-notebook__drag-area')).toBeVisible();\n\n    // no canvas (i.e., sine wave generator) in the document should be visible\n    await expect(page.locator('canvas[id=webglContext]')).toBeHidden();\n\n    // select third tab\n    await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();\n\n    // expect sine wave generator visible\n    await expect(page.locator('.c-plot')).toBeVisible();\n\n    // expect two canvases (i.e., overlay & main canvas for sine wave generator) to be visible\n    await expect(page.locator('canvas')).toHaveCount(2);\n    await expect(page.locator('canvas').nth(0)).toBeVisible();\n    await expect(page.locator('canvas').nth(1)).toBeVisible();\n\n    // now try to select the first tab again\n    await page.getByLabel(`${table.name} tab`, { exact: true }).click();\n    // ensure table header visible\n    await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();\n\n    // no canvas (i.e., sine wave generator) in the document should be visible\n    await expect(page.locator('canvas[id=webglContext]')).toBeHidden();\n  });\n\n  test('Changing the displayed tab should not be persisted if the view is locked', async ({\n    page\n  }) => {\n    await page.goto(tabsView.url);\n    const lockButton = page.getByLabel('Unlocked for editing, click to lock.', { exact: true });\n    await expect(lockButton).toBeVisible();\n    //lock the view\n    await lockButton.click();\n    // get the initial tab index\n    const initialTab = page.getByLabel(/- selected/);\n    // switch to a different tab in the view\n    const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });\n    const swgTabText = await swgTab.textContent();\n    await swgTab.click();\n    await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();\n    // navigate away from the tabbed view and back\n    await page.getByRole('treeitem', { name: 'My Items' }).click();\n    await expect(page).toHaveURL(/mine\\?/);\n    await page.goto(tabsView.url);\n    // check that the initial tab is displayed\n    const lockedSelectedTab = page.getByLabel(/- selected/);\n    await expect(lockedSelectedTab).toBeVisible();\n    await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());\n\n    //unlock the view\n    await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();\n    // switch to a different tab in the view\n    await swgTab.click();\n    await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();\n    // navigate away from the tabbed view and back\n    await page.getByRole('treeitem', { name: 'My Items' }).click();\n    await expect(page).toHaveURL(/mine\\?/);\n    await page.goto(tabsView.url);\n    // check that the newly selected tab is displayed\n    const unlockedSelectedTab = page.getByLabel(/- selected/);\n    await expect(unlockedSelectedTab).toBeVisible();\n    await expect(unlockedSelectedTab).toHaveText(swgTabText);\n  });\n});\n\ntest.describe('Tabs View CRUD', () => {\n  let tabsView;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    tabsView = await createDomainObjectWithDefaults(page, {\n      type: 'Tabs View'\n    });\n  });\n\n  test('Eager Load Tabs is the default and then can be toggled off', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7198'\n    });\n    await page.goto(tabsView.url);\n\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n    await expect(page.getByLabel('Eager Load Tabs')).not.toBeChecked();\n    await page.getByLabel('Eager Load Tabs').setChecked(true);\n    await expect(page.getByLabel('Eager Load Tabs')).toBeChecked();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/telemetryTable/preview.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*\n * This test suite is dedicated to testing the preview plugin.\n */\n\nimport { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Preview mode', () => {\n  test('all context menu items are available for a telemetry table', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    // Create a Display Layout\n    const displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout'\n    });\n    // Create a Telemetry Table\n    const telemetryTable = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      parent: displayLayout.uuid\n    });\n    // Create a Sinewave Generator\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: telemetryTable.uuid\n    });\n\n    await page.goto(displayLayout.url);\n    await page.getByLabel('View menu items').click();\n    await expect(page.getByLabel('Export Marked Rows')).toBeVisible();\n\n    await page.getByRole('menuitem', { name: 'Large View' }).click();\n    await page.getByLabel('Overlay').getByLabel('More actions').click();\n    await expect(page.getByLabel('Export Table Data')).toBeVisible();\n    await expect(page.getByLabel('Export Marked Rows')).toBeVisible();\n    await page.getByRole('menuitem', { name: 'Pause' }).click();\n    await page.getByRole('button', { name: 'Close' }).click();\n\n    await expandEntireTree(page);\n\n    await page.getByLabel('Edit Object').click();\n\n    const treePane = page.getByRole('tree', {\n      name: 'Main Tree'\n    });\n    const telemetryTableTreeItem = treePane.getByRole('treeitem', {\n      name: new RegExp(telemetryTable.name)\n    });\n    await telemetryTableTreeItem.locator('a').click();\n    await page.getByLabel('Overlay').getByLabel('More actions').click();\n    await expect(page.getByLabel('Export Table Data')).toBeVisible();\n    await expect(page.getByLabel('Export Marked Rows')).toBeVisible();\n    await expect(page.getByLabel('Export Marked Rows')).toBeDisabled();\n    await page.getByLabel('Pause').click();\n    const tableWrapper = page.getByLabel('Preview Container').locator('div.c-table-wrapper');\n    await expect(tableWrapper).toHaveClass(/is-paused/);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithRealTime,\n  setTimeConductorBounds\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Telemetry Table', () => {\n  let table;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });\n  });\n\n  test('Limits to 50 rows by default', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: table.uuid\n    });\n    await navigateToObjectWithRealTime(page, table.url);\n    const rows = page.getByLabel('table content').getByLabel('Table Row');\n    await expect(rows).toHaveCount(50);\n  });\n\n  test('on load, auto scrolls to top for descending, and to bottom for ascending', async ({\n    page\n  }) => {\n    const sineWaveGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: table.uuid\n    });\n\n    // verify in telemetry table object view\n    await navigateToObjectWithRealTime(page, table.url);\n\n    expect(await getScrollPosition(page)).toBe(0);\n\n    // verify in telemetry table view\n    await page.goto(sineWaveGenerator.url);\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByText('Telemetry Table', { exact: true }).click();\n\n    expect(await getScrollPosition(page)).toBe(0);\n\n    // navigate back to table\n    await page.goto(table.url);\n\n    // go into edit mode\n    await page.getByLabel('Edit Object').click();\n\n    // change sort direction\n    await page.locator('thead div').filter({ hasText: 'Time' }).click();\n\n    // save view\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // navigate away and back\n    await page.goto(sineWaveGenerator.url);\n    await page.goto(table.url);\n\n    // verify scroll position\n    expect(await getScrollPosition(page, false)).toBeLessThan(1);\n  });\n\n  test('unpauses and filters data when paused by button and user changes bounds', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5113'\n    });\n\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: table.uuid\n    });\n\n    // focus the Telemetry Table\n    page.goto(table.url);\n\n    // Click pause button\n    const pauseButton = page.locator('button.c-button.icon-pause');\n    await pauseButton.click();\n\n    const tableWrapper = page.locator('div.c-table-wrapper');\n    await expect(tableWrapper).toHaveClass(/is-paused/);\n\n    // Subtract 5 minutes from the current end bound datetime and set it\n    // Bring up the time conductor popup\n    let endTimeStamp = await page.getByLabel('End bounds').textContent();\n    endTimeStamp = new Date(endTimeStamp);\n\n    endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);\n    const endDate = endTimeStamp.toISOString().split('T')[0];\n    const endTime = endTimeStamp.toISOString().split('T')[1].slice(0, -5);\n\n    await setTimeConductorBounds(page, { endDate, endTime });\n\n    await expect(tableWrapper).not.toHaveClass(/is-paused/);\n\n    // Get the most recent telemetry date\n    const latestTelemetryDate = await page\n      .getByLabel('table content')\n      .getByLabel('utc table cell')\n      .last()\n      .getAttribute('title');\n\n    // Verify that it is <= our new end bound\n    const latestMilliseconds = Date.parse(latestTelemetryDate);\n    const endBoundMilliseconds = Date.parse(endTimeStamp);\n    expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);\n  });\n\n  test('Supports filtering telemetry by regular text search', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: table.uuid\n    });\n\n    // focus the Telemetry Table\n    await page.goto(table.url);\n\n    await page.getByRole('searchbox', { name: 'message filter input' }).click();\n    await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');\n\n    let cells = await page.getByRole('cell').getByText(/Roger/).all();\n    // ensure we've got more than one cell\n    expect(cells.length).toBeGreaterThan(1);\n    // ensure the text content of each cell contains the search term\n    for (const cell of cells) {\n      await expect(cell).toHaveText(/Roger/);\n    }\n\n    await page.getByRole('searchbox', { name: 'message filter input' }).click();\n    await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');\n\n    cells = await page\n      .getByRole('cell')\n      .getByText(/Dodger/)\n      .all();\n    // ensure we've got more than one cell\n    expect(cells).toHaveLength(0);\n    // ensure the text content of each cell contains the search term\n    for (const cell of cells) {\n      await expect(cell).not.toHaveText(/Dodger/);\n    }\n\n    // Click pause button\n    await page.getByLabel('Pause').click();\n  });\n\n  test('Supports filtering using Regex', async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: table.uuid\n    });\n\n    // focus the Telemetry Table\n    page.goto(table.url);\n    await page.getByRole('searchbox', { name: 'message filter input' }).hover();\n    await page\n      .getByLabel('Message filter header')\n      .getByLabel('Click to enable regex: enter a string with slashes, like this: /regex_exp/')\n      .click();\n    await page.getByRole('searchbox', { name: 'message filter input' }).click();\n    await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');\n\n    let cells = await page.getByRole('cell').getByText(/Roger/).all();\n    // ensure we've got more than one cell\n    expect(cells.length).toBeGreaterThan(1);\n    // ensure the text content of each cell contains the search term\n    for (const cell of cells) {\n      await expect(cell).toHaveText(/Roger/);\n    }\n\n    await page.getByRole('searchbox', { name: 'message filter input' }).click();\n    await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');\n\n    cells = await page\n      .getByRole('cell')\n      .getByText(/Dodger/)\n      .all();\n    // ensure we've got more than one cell\n    expect(cells).toHaveLength(0);\n    // ensure the text content of each cell contains the search term\n    for (const cell of cells) {\n      await expect(cell).not.toHaveText(/Dodger/);\n    }\n\n    // Click pause button\n    await page.getByLabel('Pause').click();\n  });\n});\n\nasync function getScrollPosition(page, top = true) {\n  const tableBody = page.locator('.c-table__body-w');\n\n  // Wait for the scrollbar to appear\n  await tableBody.evaluate((node) => {\n    return new Promise((resolve) => {\n      function checkScroll() {\n        if (node.scrollHeight > node.clientHeight) {\n          resolve();\n        } else {\n          setTimeout(checkScroll, 100);\n        }\n      }\n      checkScroll();\n    });\n  });\n\n  // make sure there are rows\n  const rows = page.getByLabel('table content').getByLabel('Table Row');\n  await rows.first().waitFor();\n\n  // Using this to allow for rows to come and go, so we can truly test the scroll position\n  // eslint-disable-next-line playwright/no-wait-for-timeout\n  await page.waitForTimeout(1000);\n\n  const { scrollTop, clientHeight, scrollHeight } = await tableBody.evaluate((node) => ({\n    scrollTop: node.scrollTop,\n    clientHeight: node.clientHeight,\n    scrollHeight: node.scrollHeight\n  }));\n\n  if (top) {\n    return scrollTop;\n  } else {\n    return Math.abs(scrollHeight - (scrollTop + clientHeight));\n  }\n}\n"
  },
  {
    "path": "e2e/tests/functional/plugins/timeConductor/datepicker.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createDomainObjectWithDefaults,\n  navigateToObjectWithFixedTimeBounds,\n  setFixedIndependentTimeConductorBounds\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\nconst FIXED_TIME_URL = './#/browse/mine';\n\ntest.describe('Datepicker operations', () => {\n  test.beforeEach(async ({ page }) => {\n    await navigateToObjectWithFixedTimeBounds(page, FIXED_TIME_URL, 1693592063607, 1693593893607);\n  });\n\n  test('Verify that user can use the datepicker in the TC', async ({ page }) => {\n    await page.getByLabel('Time Conductor Mode').click();\n    // Click on the date picker that is left-most on the screen\n    await page.getByLabel('Global Time Conductor').locator('a').first().click();\n    await expect(page.getByRole('dialog')).toBeVisible();\n    // Click on the first cell\n    await page.getByText('27 239').click();\n    // Expect datepicker to close and time conductor date setting to be changed\n    await expect(page.getByRole('dialog')).toHaveCount(0);\n  });\n\n  test('Verify that user can use the datepicker in the ITC', async ({ page }) => {\n    const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });\n\n    await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });\n\n    await setFixedIndependentTimeConductorBounds(page, {\n      start: '2024-11-12 19:11:11.000Z',\n      end: '2024-11-12 20:11:11.000Z'\n    });\n    // Open ITC\n    await page.getByLabel('Start bounds').nth(0).click();\n    // Click on the datepicker icon\n    await page.locator('form a').first().click();\n    await expect(page.getByRole('dialog')).toBeVisible();\n    // Click on the first cell\n    await page.getByText('7 342').click();\n    // Expect datepicker to close and time conductor date setting to be changed\n    await expect(page.getByRole('dialog')).toHaveCount(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  setEndOffset,\n  setFixedTimeMode,\n  setRealTimeMode,\n  setStartOffset\n} from '../../../../appActions.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Time conductor operations', () => {\n  const DAY = '2024-01-01';\n  const DAY_AFTER = '2024-01-02';\n  const ONE_O_CLOCK = '01:00:00';\n  const TWO_O_CLOCK = '02:00:00';\n\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('validate date and time inputs are validated on input event', async ({ page }) => {\n    const submitButtonLocator = page.getByLabel('Submit time bounds');\n\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    await test.step('invalid start date disables submit button', async () => {\n      const initialStartDate = await page.getByLabel('Start date').inputValue();\n      const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;\n\n      await page.getByLabel('Start date').fill(invalidStartDate);\n      await expect(submitButtonLocator).toBeDisabled();\n      await page.getByLabel('Start date').fill(initialStartDate);\n      await expect(submitButtonLocator).toBeEnabled();\n    });\n\n    await test.step('invalid start time disables submit button', async () => {\n      const initialStartTime = await page.getByLabel('Start time').inputValue();\n      const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;\n\n      await page.getByLabel('Start time').fill(invalidStartTime);\n      await expect(submitButtonLocator).toBeDisabled();\n      await page.getByLabel('Start time').fill(initialStartTime);\n      await expect(submitButtonLocator).toBeEnabled();\n    });\n\n    await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {\n      const initialEndDate = await page.getByLabel('End date').inputValue();\n      const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;\n      const initialStartTime = await page.getByLabel('Start time').inputValue();\n      const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;\n\n      await page.getByLabel('Start time').fill(invalidStartTime);\n      await expect(submitButtonLocator).toBeDisabled();\n      await page.getByLabel('End date').fill(invalidEndDate);\n      await expect(submitButtonLocator).toBeDisabled();\n      await page.getByLabel('End date').fill(initialEndDate);\n      await expect(submitButtonLocator).toBeDisabled();\n      await page.getByLabel('Start time').fill(initialStartTime);\n      await expect(submitButtonLocator).toBeEnabled();\n    });\n  });\n\n  test('validate date and time inputs validation is reported on change event', async ({ page }) => {\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    await test.step('invalid start date is reported on change event, not on input event', async () => {\n      const initialStartDate = await page.getByLabel('Start date').inputValue();\n      const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;\n\n      await page.getByLabel('Start date').fill(invalidStartDate);\n      await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');\n      await page.getByLabel('Start date').press('Tab');\n      await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');\n      await page.getByLabel('Start date').fill(initialStartDate);\n      await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');\n    });\n\n    await test.step('invalid start time is reported on change event, not on input event', async () => {\n      const initialStartTime = await page.getByLabel('Start time').inputValue();\n      const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;\n\n      await page.getByLabel('Start time').fill(invalidStartTime);\n      await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');\n      await page.getByLabel('Start time').press('Tab');\n      await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');\n      await page.getByLabel('Start time').fill(initialStartTime);\n      await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');\n    });\n\n    await test.step('invalid end date is reported on change event, not on input event', async () => {\n      const initialEndDate = await page.getByLabel('End date').inputValue();\n      const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;\n\n      await page.getByLabel('End date').fill(invalidEndDate);\n      await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');\n      await page.getByLabel('End date').press('Tab');\n      await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');\n      await page.getByLabel('End date').fill(initialEndDate);\n      await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');\n    });\n\n    await test.step('invalid end time is reported on change event, not on input event', async () => {\n      const initialEndTime = await page.getByLabel('End time').inputValue();\n      const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;\n\n      await page.getByLabel('End time').fill(invalidEndTime);\n      await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');\n      await page.getByLabel('End time').press('Tab');\n      await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');\n      await page.getByLabel('End time').fill(initialEndTime);\n      await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');\n    });\n  });\n\n  test('validate start time does not exceed end time on submit', async ({ page }) => {\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    // FIXME: https://github.com/nasa/openmct/pull/7818\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(500);\n\n    await page.getByLabel('Start date').fill(DAY);\n    await page.getByLabel('Start time').fill(TWO_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY);\n    await page.getByLabel('End time').fill(ONE_O_CLOCK);\n    await page.getByLabel('Submit time bounds').click();\n\n    await expect(page.getByLabel('Start date')).toHaveAttribute(\n      'title',\n      'Start bound must be less than end bound'\n    );\n    await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);\n    await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n\n    await page.getByLabel('Start date').fill(DAY);\n    await page.getByLabel('Start time').fill(ONE_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY);\n    await page.getByLabel('End time').fill(TWO_O_CLOCK);\n    await page.getByLabel('Submit time bounds').click();\n\n    await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n    await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);\n  });\n\n  test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    await page.getByLabel('Start date').fill(DAY_AFTER);\n    await page.getByLabel('Start time').fill(ONE_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY);\n    await page.getByLabel('End time').fill(ONE_O_CLOCK);\n    await page.getByLabel('Submit time bounds').click();\n\n    await expect(page.getByLabel('Start date')).toHaveAttribute(\n      'title',\n      'Start bound must be less than end bound'\n    );\n    await expect(page.getByLabel('Start bounds')).not.toHaveText(\n      `${DAY_AFTER} ${ONE_O_CLOCK}.000Z`\n    );\n    await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n\n    await page.getByLabel('Start date').fill(DAY);\n    await page.getByLabel('Start time').fill(ONE_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY_AFTER);\n    await page.getByLabel('End time').fill(ONE_O_CLOCK);\n    await page.getByLabel('Submit time bounds').click();\n\n    await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n    await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);\n  });\n\n  test('cancelling form does not set bounds', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7791'\n    });\n\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    await page.getByLabel('Start date').fill(DAY);\n    await page.getByLabel('Start time').fill(ONE_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY_AFTER);\n    await page.getByLabel('End time').fill(ONE_O_CLOCK);\n    await page.getByLabel('Discard changes and close time popup').click();\n\n    await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n    await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);\n\n    // Open the time conductor popup\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n\n    await page.getByLabel('Start date').fill(DAY);\n    await page.getByLabel('Start time').fill(ONE_O_CLOCK);\n    await page.getByLabel('End date').fill(DAY_AFTER);\n    await page.getByLabel('End time').fill(ONE_O_CLOCK);\n    await page.getByLabel('Submit time bounds').click();\n\n    await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);\n    await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);\n  });\n});\n\ntest.describe('Global Time Conductor', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Input field validation: real-time mode', async ({ page }) => {\n    const startOffset = {\n      startHours: '01',\n      startMins: '29',\n      startSecs: '23'\n    };\n\n    const endOffset = {\n      endHours: '01',\n      endMins: '30',\n      endSecs: '31'\n    };\n\n    // Switch to real-time mode\n    await setRealTimeMode(page);\n\n    // Set start time offset\n    await setStartOffset(page, startOffset);\n\n    // Verify time was updated on time offset button\n    await expect(page.getByLabel('Start offset: 01:29:23')).toBeVisible();\n\n    // Set end time offset\n    await setEndOffset(page, endOffset);\n\n    // Verify time was updated on preceding time offset button\n    await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();\n\n    // Discard changes and verify that offsets remain unchanged\n    await setStartOffset(page, {\n      startHours: '00',\n      startMins: '30',\n      startSecs: '00',\n      submitChanges: false\n    });\n\n    await expect(page.getByLabel('Start offset: 01:29:23')).toBeVisible();\n    await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();\n  });\n\n  /**\n   * Verify that offsets and url params are preserved when switching\n   * between fixed timespan and real-time mode.\n   */\n  test('preserve offsets and url params when switching between fixed and real-time mode', async ({\n    page\n  }) => {\n    const startOffset = {\n      startMins: '30',\n      startSecs: '23'\n    };\n\n    const endOffset = {\n      endSecs: '01'\n    };\n\n    // Convert offsets to milliseconds\n    const startDelta = 30 * 60 * 1000 + 23 * 1000;\n    const endDelta = 1 * 1000;\n\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Switch to real-time mode\n    await setRealTimeMode(page);\n\n    // Set start time offset\n    await setStartOffset(page, startOffset);\n\n    // Set end time offset\n    await setEndOffset(page, endOffset);\n\n    // Switch to fixed timespan mode\n    await setFixedTimeMode(page);\n\n    // Switch back to real-time mode\n    await setRealTimeMode(page);\n\n    // Verify updated start time offset persists after mode switch\n    await expect(page.getByLabel('Start offset: 00:30:23')).toBeVisible();\n\n    // Verify updated end time offset persists after mode switch\n    await expect(page.getByLabel('End offset: 00:00:01')).toBeVisible();\n\n    // Verify url parameters persist after mode switch\n    // eslint-disable-next-line no-useless-escape\n    const urlRegex = new RegExp(`.*tc\\.startDelta=${startDelta}&tc\\.endDelta=${endDelta}.*`);\n    await page.waitForURL(urlRegex);\n  });\n\n  test.fixme(\n    'time conductor history in fixed time mode will track changing start and end times',\n    async ({ page }) => {\n      // change start time, verify it's tracked in history\n      // change end time, verify it's tracked in history\n    }\n  );\n\n  test.fixme(\n    'time conductor history in realtime mode will track changing start and end times',\n    async ({ page }) => {\n      // change start offset, verify it's tracked in history\n      // change end offset, verify it's tracked in history\n    }\n  );\n\n  test.fixme(\n    'time conductor history allows you to set a historical timeframe',\n    async ({ page }) => {\n      // make sure there are historical history options\n      // select an option and make sure the time conductor start and end bounds are updated correctly\n    }\n  );\n\n  test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {\n    // make sure there are realtime history options\n    // select an option and verify the offsets are updated correctly\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/plugins/timer/timer.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults, setRealTimeMode } from '../../../../appActions.js';\nimport { MISSION_TIME } from '../../../../constants.js';\nimport { expect, test } from '../../../../pluginFixtures.js';\n\ntest.describe('Timer', () => {\n  let timer;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    timer = await createDomainObjectWithDefaults(page, { type: 'timer' });\n  });\n\n  test('Can perform actions on the Timer', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/4313'\n    });\n\n    await test.step('From the tree context menu', async () => {\n      await triggerTimerContextMenuAction(page, timer.url, 'Start');\n      await triggerTimerContextMenuAction(page, timer.url, 'Pause');\n      await triggerTimerContextMenuAction(page, timer.url, 'Restart at 0');\n      await triggerTimerContextMenuAction(page, timer.url, 'Stop');\n    });\n\n    await test.step('From the 3dot menu', async () => {\n      await triggerTimer3dotMenuAction(page, 'Start');\n      await triggerTimer3dotMenuAction(page, 'Pause');\n      await triggerTimer3dotMenuAction(page, 'Restart at 0');\n      await triggerTimer3dotMenuAction(page, 'Stop');\n    });\n\n    await test.step('From the object view', async () => {\n      await triggerTimerViewAction(page, 'Start');\n      await triggerTimerViewAction(page, 'Pause');\n      await triggerTimerViewAction(page, 'Restart at 0');\n    });\n  });\n});\n\ntest.describe('Timer with target date @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await setRealTimeMode(page);\n    await createDomainObjectWithDefaults(page, { type: 'timer' });\n  });\n\n  test('Can count down to a target date', async ({ page }) => {\n    // Set the target date to 2024-11-24 03:30:00\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();\n\n    await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');\n    await page.locator('input[name=\"hour\"]').fill('3');\n    await page.locator('input[name=\"min\"]').fill('30');\n    await page.locator('input[name=\"sec\"]').fill('00');\n    await page.getByLabel('Save').click();\n    await page.locator('.c-timer__direction').hover();\n    await page.getByLabel('Start', { exact: true }).click();\n\n    // Get the current timer seconds value\n    const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);\n    await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);\n\n    // Wait for the timer to count down and assert\n    await expect\n      .poll(async () => {\n        const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);\n        return Number(newTimerValue);\n      })\n      .toBeLessThan(Number(timerSecValue));\n  });\n\n  test('Can count up from a target date', async ({ page }) => {\n    // Set the target date to 2020-11-23 03:30:00\n    await page.getByTitle('More actions').click();\n    await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();\n    await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');\n    await page.locator('input[name=\"hour\"]').fill('3');\n    await page.locator('input[name=\"min\"]').fill('30');\n    await page.locator('input[name=\"sec\"]').fill('00');\n    await page.getByLabel('Save').click();\n    await page.locator('.c-timer__direction').hover();\n    await page.getByLabel('Start', { exact: true }).click();\n\n    // Get the current timer seconds value\n    const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);\n    await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);\n\n    // Wait for the timer to count up and assert\n    await expect\n      .poll(async () => {\n        const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);\n        return Number(newTimerValue);\n      })\n      .toBeGreaterThan(Number(timerSecValue));\n  });\n});\n\n/**\n * Actions that can be performed on a timer from context menus.\n * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction\n */\n\n/**\n * Actions that can be performed on a timer from the object view.\n * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction\n */\n\n/**\n * Trigger a timer action from the tree context menu\n * @param {import('@playwright/test').Page} page\n * @param {TimerAction} action\n */\nasync function triggerTimerContextMenuAction(page, timerUrl, action) {\n  const menuAction = `.c-menu ul li >> text=\"${action}\"`;\n  await openObjectTreeContextMenu(page, timerUrl);\n  await page.locator(menuAction).click();\n  assertTimerStateAfterAction(page, action);\n}\n\n/**\n * Trigger a timer action from the 3dot menu\n * @param {import('@playwright/test').Page} page\n * @param {TimerAction} action\n */\nasync function triggerTimer3dotMenuAction(page, action) {\n  const menuAction = `.c-menu ul li >> text=\"${action}\"`;\n  let isActionAvailable = false;\n  let iterations = 0;\n  // Dismiss/open the 3dot menu until the action is available\n  // or a maximum number of iterations is reached\n  while (!isActionAvailable && iterations <= 20) {\n    await page.getByLabel('Object View').click();\n    await page.getByLabel('More actions').click();\n    isActionAvailable = await page.locator(menuAction).isVisible();\n    iterations++;\n  }\n\n  await page.locator(menuAction).click();\n  assertTimerStateAfterAction(page, action);\n}\n\n/**\n * Trigger a timer action from the object view\n * @param {import('@playwright/test').Page} page\n * @param {TimerViewAction} action\n */\nasync function triggerTimerViewAction(page, action) {\n  await page.locator('.c-timer').hover({ trial: true });\n  const buttonTitle = buttonTitleFromAction(action);\n  await page.getByLabel(buttonTitle, { exact: true }).click();\n  assertTimerStateAfterAction(page, action);\n}\n\n/**\n * Takes in a TimerViewAction and returns the button title\n * @param {TimerViewAction} action\n */\nfunction buttonTitleFromAction(action) {\n  switch (action) {\n    case 'Start':\n      return 'Start';\n    case 'Pause':\n      return 'Pause';\n    case 'Restart at 0':\n      return 'Reset';\n  }\n}\n\n/**\n * Verify the timer state after a timer action has been performed.\n * @param {import('@playwright/test').Page} page\n * @param {TimerAction} action\n */\nasync function assertTimerStateAfterAction(page, action) {\n  const timerValue = page.locator('.c-timer__value');\n  let timerStateClass;\n  switch (action) {\n    case 'Start':\n    case 'Restart at 0':\n      timerStateClass = 'is-started';\n      await expect(timerValue).toHaveText('0D 00:00:00');\n      break;\n    case 'Stop':\n      timerStateClass = 'is-stopped';\n      await expect(timerValue).toHaveText('--:--:--');\n      break;\n    case 'Pause':\n      timerStateClass = 'is-paused';\n      break;\n  }\n\n  await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));\n}\n\n/**\n * Open the given `domainObject`'s context menu from the object tree.\n * Expands the path to the object and scrolls to it if necessary.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url the url to the object\n */\nasync function openObjectTreeContextMenu(page, url) {\n  await page.goto(url);\n  await page.getByLabel('Show selected item in tree').click();\n  await page.locator('.is-navigated-object').click({\n    button: 'right'\n  });\n}\n"
  },
  {
    "path": "e2e/tests/functional/recentObjects.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { waitForAnimations } from '../../baseFixtures.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Recent Objects', () => {\n  /** @type {import('@playwright/test').Locator} */\n  let recentObjectsList;\n  /** @type {import('@playwright/test').Locator} */\n  let clock;\n  /** @type {import('@playwright/test').Locator} */\n  let folderA;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Set Recent Objects List locator for subsequent tests\n    recentObjectsList = page.getByRole('list', {\n      name: 'Recent Objects'\n    });\n\n    // Create a folder and nest a Clock within it\n    folderA = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n    clock = await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      parent: folderA.uuid\n    });\n\n    // Drag the Recent Objects panel up a bit\n    await page\n      .locator('.l-pane.l-pane--vertical-handle-before', {\n        hasText: 'Recently Viewed'\n      })\n      .locator('.l-pane__handle')\n      .hover();\n    await page.mouse.down();\n    await page.mouse.move(0, 100);\n    await page.mouse.up();\n  });\n  test('Navigated objects show up in recents, object renames and deletions are reflected', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6818'\n    });\n\n    // Verify that both created objects appear in the list and are in the correct order\n    await assertInitialRecentObjectsListState();\n\n    // Navigate to the folder by clicking on the main object name in the recent objects list item\n    await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();\n    await page.waitForURL(`**/${folderA.uuid}?*`);\n    expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();\n\n    // Rename\n    folderA.name = `${folderA.name}-NEW!`;\n    await page.locator('.l-browse-bar__object-name').fill('');\n    await page.locator('.l-browse-bar__object-name').fill(folderA.name);\n    await page.keyboard.press('Enter');\n\n    // Verify rename has been applied in recent objects list item and objects paths\n    expect(\n      await page\n        .getByRole('navigation', {\n          name: clock.name\n        })\n        .locator('a')\n        .filter({\n          hasText: folderA.name\n        })\n        .count()\n    ).toBeGreaterThan(0);\n    expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();\n\n    await page.getByLabel('Show selected item in tree').click();\n    // Delete the folder via the left tree pane treeitem context menu\n    await page\n      .getByRole('treeitem', { name: new RegExp(folderA.name) })\n      .locator('a')\n      .click({\n        button: 'right'\n      });\n    await page.getByRole('menuitem', { name: /Remove/ }).click();\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Verify that the folder and clock are no longer in the recent objects list\n    await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();\n    await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();\n  });\n\n  test('Clicking on an object in the path of a recent object navigates to the object', async ({\n    page,\n    openmctConfig\n  }) => {\n    const { myItemsFolderName } = openmctConfig;\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6151'\n    });\n    await page.goto('./#/browse/mine');\n\n    // Navigate to the folder by clicking on its entry in the Clock's breadcrumb\n    const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`);\n    await page\n      .getByRole('navigation', {\n        name: clock.name\n      })\n      .locator('a')\n      .filter({\n        hasText: folderA.name\n      })\n      .click();\n\n    // Verify that the hash URL updates correctly\n    await waitForFolderNavigation;\n    expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`));\n\n    // Navigate to My Items by clicking on its entry in the Clock's breadcrumb\n    const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`);\n    await page\n      .getByRole('navigation', {\n        name: clock.name\n      })\n      .locator('a')\n      .filter({\n        hasText: myItemsFolderName\n      })\n      .click();\n\n    // Verify that the hash URL updates correctly\n    await waitForMyItemsNavigation;\n    expect(page.url()).toMatch(new RegExp(`.*mine?.*`));\n  });\n  test(\"Clicking on the 'target button' scrolls the object into view in the tree and highlights it\", async ({\n    page\n  }) => {\n    const clockTreeItem = page\n      .getByRole('tree', { name: 'Main Tree' })\n      .getByRole('treeitem', { name: clock.name });\n    const folderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', {\n      name: folderA.name,\n      expanded: true\n    });\n\n    // Click the \"Target\" button for the Clock which is nested in a folder\n    await page.getByRole('button', { name: `Open and scroll to ${clock.name}` }).click();\n\n    // Assert that the Clock parent folder has expanded and the Clock is visible)\n    await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);\n    await expect(clockTreeItem).toBeVisible();\n\n    // Assert that the Clock treeitem is highlighted\n    await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);\n\n    // Wait for highlight animation to end\n    await waitForAnimations(clockTreeItem.locator('.c-tree__item'));\n\n    // Assert that the Clock treeitem is no longer highlighted\n    await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);\n  });\n  test('Persists on refresh', async ({ page }) => {\n    await assertInitialRecentObjectsListState();\n    await page.reload();\n    await assertInitialRecentObjectsListState();\n  });\n  test('Displays objects and aliases uniquely', async ({ page }) => {\n    const mainTree = page.getByRole('tree', { name: 'Main Tree' });\n\n    // Navigate to the clock and reveal it in the tree\n    await page.goto(clock.url);\n    await page.getByLabel('Show selected item in tree').click();\n\n    // Right click the clock and create an alias using the \"link\" context menu action\n    const clockTreeItem = page\n      .getByRole('tree', {\n        name: 'Main Tree'\n      })\n      .getByRole('treeitem', {\n        name: clock.name\n      });\n    await clockTreeItem.click({\n      button: 'right'\n    });\n    await page\n      .getByRole('menuitem', {\n        name: /Create Link/\n      })\n      .click();\n    await page\n      .getByRole('tree', { name: 'Create Modal Tree' })\n      .getByRole('treeitem')\n      .first()\n      .click();\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Click the newly created object alias in the tree\n    await mainTree\n      .getByRole('treeitem', {\n        name: new RegExp(clock.name)\n      })\n      .filter({\n        has: page.locator('.is-alias')\n      })\n      .click();\n\n    // Assert that two recent objects are displayed and one of them is an alias\n    await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toHaveCount(2);\n    await expect(recentObjectsList.locator('.is-alias')).toHaveCount(1);\n\n    // Assert that the alias and the original's breadcrumbs are different\n    const clockBreadcrumbs = recentObjectsList\n      .getByRole('listitem', { name: clock.name })\n      .getByRole('navigation');\n    await expect(clockBreadcrumbs).toHaveCount(2);\n    await expect(clockBreadcrumbs.nth(0)).not.toHaveText(await clockBreadcrumbs.nth(1).innerText());\n  });\n  test('Enforces a limit of 20 recent objects and clears the recent objects', async ({ page }) => {\n    // Creating 21 objects takes a while, so increase the timeout\n    test.slow();\n\n    // Assert that the list initially contains 3 objects (clock, folder, my items)\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(3);\n\n    let lastFolder;\n    let lastClock;\n    // Create 19 more objects (3 in beforeEach() + 18 new = 21 total)\n    for (let i = 0; i < 9; i++) {\n      lastFolder = await createDomainObjectWithDefaults(page, {\n        type: 'Folder',\n        parent: lastFolder?.uuid\n      });\n      lastClock = await createDomainObjectWithDefaults(page, {\n        type: 'Clock',\n        parent: lastFolder?.uuid\n      });\n    }\n\n    // Assert that the list contains 20 objects\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(20);\n\n    // Collapse the tree\n    await page.getByTitle('Collapse all tree items').click();\n    const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', {\n      name: lastFolder.name,\n      expanded: true\n    });\n    const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', {\n      name: lastClock.name\n    });\n\n    // Test \"Open and Scroll To\" in a deeply nested tree, while we're here\n    await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}` }).click();\n\n    // Assert that the Clock parent folder has expanded and the Clock is visible)\n    await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);\n    await expect(lastClockTreeItem).toBeVisible();\n\n    // Assert that the Clock treeitem is highlighted\n    await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);\n\n    // Wait for highlight animation to end\n    await waitForAnimations(lastClockTreeItem.locator('.c-tree__item'));\n\n    // Assert that the Clock treeitem is no longer highlighted\n    await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);\n\n    // Click the aria-label=\"Clear Recently Viewed\" button\n    await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();\n\n    // Click on the \"OK\" button in the confirmation dialog\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Assert that the list is empty\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(0);\n  });\n  test('Verify functionality of \"clear\" and \"collapse pane\" buttons', async ({ page }) => {\n    // Assert that the list initially contains 3 objects (clock, folder, my items)\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(3);\n\n    // Assert that the button is enabled\n    await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeEnabled();\n\n    // Click the aria-label=\"Clear Recently Viewed\" button\n    await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();\n\n    // Click on the \"OK\" button in the confirmation dialog\n    await page.getByRole('button', { name: 'Ok', exact: true }).click();\n\n    // Assert that the list is empty\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(0);\n\n    // Assert that the button is disabled\n    await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeDisabled();\n\n    // Navigate to folder object\n    await page.goto(folderA.url);\n\n    // Assert that the list contains 1 object\n    await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(1);\n\n    // Assert that the button is enabled\n    await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeEnabled();\n\n    // Assert initial state of pane and collapse the Recent Objects panel\n    await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();\n    await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();\n    await page.getByLabel('Collapse Recently Viewed Pane').click();\n\n    // Assert that the \"Expand Recently Viewed Pane\" button is visible\n    // and that the \"Collapse Recently Viewed Pane\" button is hidden\n    await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();\n    await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();\n\n    // Expand the Recent Objects panel by clicking on the \"Expand Recently Viewed Pane\" button\n    await page.getByLabel('Expand Recently Viewed Pane').click();\n\n    // Assert that the \"Expand Recently Viewed Pane\" button is hidden\n    // and that the \"Collapse Recently Viewed Pane\" button is visible\n    await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();\n    await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();\n  });\n\n  function assertInitialRecentObjectsListState() {\n    return Promise.all([\n      expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(),\n      expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(),\n      expect(\n        recentObjectsList\n          .getByRole('listitem', { name: clock.name })\n          .locator('a')\n          .getByText(folderA.name)\n      ).toBeVisible(),\n      expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(),\n      expect(\n        recentObjectsList\n          .getByRole('listitem', { name: clock.name })\n          .locator('a')\n          .getByText(folderA.name)\n      ).toBeVisible(),\n      expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible()\n    ]);\n  }\n});\n"
  },
  {
    "path": "e2e/tests/functional/renaming.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests for renaming objects, and their global application effects.\n*/\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../baseFixtures.js';\n\ntest.describe('Renaming objects', () => {\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('When renaming objects, the browse bar and various components all update', async ({\n    page\n  }) => {\n    const folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n    // Create a new 'Clock' object with default settings\n    const clock = await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      parent: folder.uuid\n    });\n\n    // Rename\n    clock.name = `${clock.name}-NEW!`;\n    await renameObjectFromContextMenu(page, clock.url, clock.name);\n    // check inspector for new name\n    const titleValue = await page\n      .getByLabel('Title inspector properties')\n      .getByLabel('inspector property value')\n      .textContent();\n    expect(titleValue).toBe(clock.name);\n    // check browse bar for new name\n    await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();\n    // check tree item for new name\n    await expect(\n      page.getByRole('listitem', {\n        name: clock.name\n      })\n    ).toBeVisible();\n    // check recent objects for new name\n    await expect(\n      page.getByRole('navigation', {\n        name: clock.name\n      })\n    ).toBeVisible();\n    // check title for new name\n    const title = await page.title();\n    expect(title).toBe(clock.name);\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {string} myItemsFolderName\n * @param {string} url\n * @param {string} newName\n */\nasync function renameObjectFromContextMenu(page, url, newName) {\n  await openObjectTreeContextMenu(page, url);\n  await page.locator('li:text(\"Edit Properties\")').click();\n  const nameInput = page.getByLabel('Title', { exact: true });\n  await nameInput.fill('');\n  await nameInput.fill(newName);\n  await page.locator('[aria-label=\"Save\"]').click();\n}\n\n/**\n * Open the given `domainObject`'s context menu from the object tree.\n * Expands the path to the object and scrolls to it if necessary.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url the url to the object\n */\nasync function openObjectTreeContextMenu(page, url) {\n  await page.goto(url);\n  await page.getByLabel('Show selected item in tree').click();\n  await page.locator('.is-navigated-object').click({\n    button: 'right'\n  });\n}\n"
  },
  {
    "path": "e2e/tests/functional/search.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/**\n * This test suite is dedicated to tests which verify search functionalities.\n */\n\nimport { v4 as uuid } from 'uuid';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Grand Search', () => {\n  let grandSearchInput;\n\n  test.use({ ignore404s: [/_design\\/object_names\\/_view\\/object_names$/] });\n\n  test.beforeEach(async ({ page }) => {\n    grandSearchInput = page\n      .getByLabel('OpenMCT Search')\n      .getByRole('searchbox', { name: 'Search Input' });\n\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Can search for objects, and subsequent search dropdown behaves properly', async ({\n    page,\n    openmctConfig\n  }) => {\n    const { myItemsFolderName } = openmctConfig;\n\n    const createdObjects = await createObjectsForSearch(page);\n\n    // Go back into edit mode for the display layout\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    await grandSearchInput.click();\n    await grandSearchInput.fill('Cl');\n\n    await expect(page.getByLabel('Object Search Result').first()).toContainText(\n      `Clock A ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(1)).toContainText(\n      `Clock B ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(2)).toContainText(\n      `Clock C ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(3)).toContainText(\n      `Clock D ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    // Click the Elements pool to dismiss the search menu\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await expect(page.getByLabel('Object Search Result').first()).toBeHidden();\n\n    await grandSearchInput.click();\n    await page.getByLabel('OpenMCT Search').getByText('Clock A').click();\n    await expect(page.getByRole('dialog', { name: 'Preview Container' })).toBeVisible();\n\n    // Close the Preview window\n    await page.getByRole('button', { name: 'Close' }).click();\n    await expect(page.getByLabel('Object Search Result').first()).toBeVisible();\n    await expect(page.getByLabel('Object Search Result').first()).toContainText(\n      `Clock A ${myItemsFolderName} Red Folder Blue Folder`\n    );\n\n    await page.getByLabel('Object Search Result').first().click();\n    await expect(page.getByLabel('Object Search Result').first()).toBeHidden();\n\n    await grandSearchInput.fill('foo');\n    await expect(page.getByLabel('Object Search Result').first()).toBeHidden();\n\n    // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    // Click [aria-label=\"OpenMCT Search\"] [aria-label=\"Search Input\"]\n    await grandSearchInput.click();\n    // Fill [aria-label=\"OpenMCT Search\"] [aria-label=\"Search Input\"]\n    await grandSearchInput.fill('Cl');\n    await Promise.all([\n      page.waitForNavigation(),\n      page.getByLabel('OpenMCT Search').getByText('Clock A').click()\n    ]);\n    await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible();\n\n    await grandSearchInput.fill('Disp');\n    await expect(page.getByLabel('Object Search Result').first()).toContainText(\n      createdObjects.displayLayout.name\n    );\n    await expect(page.getByLabel('Object Search Result').first()).not.toContainText('Folder');\n\n    await grandSearchInput.fill('Clock C');\n    await expect(page.getByLabel('Object Search Result').first()).toContainText(\n      `Clock C ${myItemsFolderName} Red Folder Blue Folder`\n    );\n\n    await grandSearchInput.fill('Cloc');\n    await expect(page.getByLabel('Object Search Result').first()).toContainText(\n      `Clock A ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(1)).toContainText(\n      `Clock B ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(2)).toContainText(\n      `Clock C ${myItemsFolderName} Red Folder Blue Folder`\n    );\n    await expect(page.getByLabel('Object Search Result').nth(3)).toContainText(\n      `Clock D ${myItemsFolderName} Red Folder Blue Folder`\n    );\n\n    await grandSearchInput.click();\n    await grandSearchInput.fill('Sine');\n  });\n\n  test('Clicking on a search result changes the URL even if the same type is already selected', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7303'\n    });\n\n    const { sineWaveGeneratorAlpha, sineWaveGeneratorBeta } = await createObjectsForSearch(page);\n    await grandSearchInput.click();\n    await grandSearchInput.fill('Sine');\n    await waitForSearchCompletion(page);\n    await page.getByLabel('OpenMCT Search').getByText('Sine Wave Generator Alpha').click();\n    const alphaPattern = new RegExp(sineWaveGeneratorAlpha.url.substring(1));\n    await expect(page).toHaveURL(alphaPattern);\n    await grandSearchInput.click();\n    await page.getByLabel('OpenMCT Search').getByText('Sine Wave Generator Beta').click();\n    const betaPattern = new RegExp(sineWaveGeneratorBeta.url.substring(1));\n    await expect(page).toHaveURL(betaPattern);\n  });\n\n  test('Validate empty search result', async ({ page }) => {\n    // Invalid search for objects\n    await grandSearchInput.fill('not found');\n\n    // Wait for search to complete\n    await waitForSearchCompletion(page);\n\n    // Get the search results\n    const searchResults = page.getByRole('listitem', { name: 'Object Search Result' });\n\n    // Verify that no results are found\n    await expect(searchResults).toHaveCount(0);\n\n    // Verify proper message appears\n    await expect(page.getByText('No results found')).toBeVisible();\n  });\n\n  test('Validate single object in search result @couchdb @network', async ({ page }) => {\n    // Create a folder object\n    const folderName = uuid();\n    await createDomainObjectWithDefaults(page, {\n      type: 'folder',\n      name: folderName\n    });\n\n    // Full search for object\n    await grandSearchInput.fill(folderName);\n\n    // Wait for search to complete\n    await waitForSearchCompletion(page);\n\n    // Get the search results\n    const searchResults = page.getByLabel('Object Search Result');\n\n    // Verify that one result is found\n    await expect(searchResults).toBeVisible();\n    await expect(searchResults).toHaveCount(1);\n    await expect(searchResults).toContainText(folderName);\n  });\n\n  test.describe('Search will test for the presence of the object_names index, and', () => {\n    test('use index if available @couchdb @network', async ({ page }) => {\n      await createObjectsForSearch(page);\n\n      let isObjectNamesViewAvailable = false;\n      let isObjectNamesUsedForSearch = false;\n\n      page.on('request', async (request) => {\n        const isObjectNamesRequest = request.url().endsWith('_view/object_names');\n        const isHeadRequest = request.method().toLowerCase() === 'head';\n\n        if (isObjectNamesRequest && isHeadRequest) {\n          const response = await request.response();\n          isObjectNamesViewAvailable = response.status() === 200;\n        }\n      });\n\n      page.on('request', (request) => {\n        const isObjectNamesRequest = request.url().endsWith('_view/object_names');\n        const isPostRequest = request.method().toLowerCase() === 'post';\n\n        if (isObjectNamesRequest && isPostRequest) {\n          isObjectNamesUsedForSearch = true;\n        }\n      });\n\n      // Full search for object\n      await grandSearchInput.pressSequentially('Clock', { delay: 100 });\n\n      // Wait for search to finish\n      await waitForSearchCompletion(page);\n\n      expect(isObjectNamesViewAvailable).toBe(true);\n      expect(isObjectNamesUsedForSearch).toBe(true);\n    });\n\n    test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {\n      await page.route('**/_view/object_names', (route) => {\n        route.fulfill({\n          status: 404\n        });\n      });\n      await createObjectsForSearch(page);\n\n      let isObjectNamesViewAvailable = false;\n      let isFindUsedForSearch = false;\n\n      page.on('request', async (request) => {\n        const isObjectNamesRequest = request.url().endsWith('_view/object_names');\n        const isHeadRequest = request.method().toLowerCase() === 'head';\n\n        if (isObjectNamesRequest && isHeadRequest) {\n          const response = await request.response();\n          isObjectNamesViewAvailable = response.status() === 200;\n        }\n      });\n\n      page.on('request', (request) => {\n        const isFindRequest = request.url().endsWith('_find');\n        const isPostRequest = request.method().toLowerCase() === 'post';\n\n        if (isFindRequest && isPostRequest) {\n          isFindUsedForSearch = true;\n        }\n      });\n\n      // Full search for object\n      await grandSearchInput.pressSequentially('Clock', { delay: 100 });\n\n      // Wait for search to finish\n      await waitForSearchCompletion(page);\n      console.info(\n        `isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`\n      );\n      expect(isObjectNamesViewAvailable).toBe(false);\n      expect(isFindUsedForSearch).toBe(true);\n    });\n  });\n\n  test('Search results are debounced @couchdb @network', async ({ page }) => {\n    // Unfortunately 404s are always logged to the JavaScript console and can't be suppressed\n    // A 404 is now thrown when we test for the presence of the object names view used by search.\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6179'\n    });\n    await createObjectsForSearch(page);\n\n    let networkRequests = [];\n\n    page.on('request', (request) => {\n      const isSearchRequest =\n        request.url().endsWith('object_names') ||\n        request.url().endsWith('_find') ||\n        request.url().includes('by_keystring');\n      const isFetchRequest = request.resourceType() === 'fetch';\n      // CouchDB search results in a one-time head request to test for the presence of an index.\n      const isHeadRequest = request.method().toLowerCase() === 'head';\n\n      if (isSearchRequest && isFetchRequest && !isHeadRequest) {\n        networkRequests.push(request);\n      }\n    });\n\n    // Full search for object\n    await grandSearchInput.pressSequentially('Clock', { delay: 100 });\n\n    // Wait for search to finish\n    await waitForSearchCompletion(page);\n\n    // Network requests for the composite telemetry with multiple items should be:\n    // 1.  batched request for latest telemetry using the bulk API\n    await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(1);\n\n    await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A');\n  });\n\n  test('Slowly typing after search debounce will abort requests @couchdb @network', async ({\n    page\n  }) => {\n    let requestWasAborted = false;\n    await createObjectsForSearch(page);\n    page.on('requestfailed', (request) => {\n      // check if the request was aborted\n      if (request.failure().errorText === 'net::ERR_ABORTED') {\n        requestWasAborted = true;\n      }\n    });\n\n    // Intercept and delay request\n    const delayInMs = 100;\n\n    await page.route('**', async (route, request) => {\n      await new Promise((resolve) => setTimeout(resolve, delayInMs));\n      route.continue();\n    });\n\n    // Slowly type after search delay\n    const searchInput = page.getByRole('searchbox', { name: 'Search Input' });\n    await searchInput.pressSequentially('Clock', { delay: 200 });\n    await expect(page.getByText('Clock B').first()).toBeVisible();\n\n    expect(requestWasAborted).toBe(true);\n  });\n\n  test('Validate multiple objects in search results return partial matches', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/4667'\n    });\n\n    // Create folder objects\n    const folderName1 = 'e928a26e-e924-4ea0';\n    const folderName2 = 'e928a26e-e924-4001';\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: folderName1\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: folderName2\n    });\n\n    // Partial search for objects\n    await grandSearchInput.fill('e928a26e');\n\n    // Wait for search to finish\n    await waitForSearchCompletion(page);\n\n    const searchResultDropDown = page.getByRole('dialog', { name: 'Search Results' });\n\n    // Verify that the search result/s correctly match the search query\n    await expect(searchResultDropDown).toContainText(folderName1);\n    await expect(searchResultDropDown).toContainText(folderName2);\n\n    // Get the search results\n    const objectSearchResults = page.getByLabel('Object Search Result');\n    // Verify that two results are found\n    await expect(objectSearchResults).toHaveCount(2);\n  });\n});\n\n/**\n * Wait for search to complete\n *\n * @param {import('@playwright/test').Page} page\n */\nasync function waitForSearchCompletion(page) {\n  // Wait loading spinner to disappear\n  await expect(\n    page\n      .getByRole('list', { name: 'Object Results' })\n      .or(\n        page\n          .getByRole('list', { name: 'Annotation Results' })\n          .or(page.getByText('No results found'))\n      )\n  ).toBeVisible();\n}\n\n/**\n * Creates some domain objects for searching\n * @param {import('@playwright/test').Page} page\n */\nasync function createObjectsForSearch(page) {\n  const redFolder = await createDomainObjectWithDefaults(page, {\n    type: 'Folder',\n    name: 'Red Folder'\n  });\n\n  const blueFolder = await createDomainObjectWithDefaults(page, {\n    type: 'Folder',\n    name: 'Blue Folder',\n    parent: redFolder.uuid\n  });\n\n  const clockA = await createDomainObjectWithDefaults(page, {\n    type: 'Clock',\n    name: 'Clock A',\n    parent: blueFolder.uuid\n  });\n  const clockB = await createDomainObjectWithDefaults(page, {\n    type: 'Clock',\n    name: 'Clock B',\n    parent: blueFolder.uuid\n  });\n  const clockC = await createDomainObjectWithDefaults(page, {\n    type: 'Clock',\n    name: 'Clock C',\n    parent: blueFolder.uuid\n  });\n  const clockD = await createDomainObjectWithDefaults(page, {\n    type: 'Clock',\n    name: 'Clock D',\n    parent: blueFolder.uuid\n  });\n\n  const sineWaveGeneratorAlpha = await createDomainObjectWithDefaults(page, {\n    type: 'Sine Wave Generator',\n    name: 'Sine Wave Generator Alpha'\n  });\n\n  const sineWaveGeneratorBeta = await createDomainObjectWithDefaults(page, {\n    type: 'Sine Wave Generator',\n    name: 'Sine Wave Generator Beta'\n  });\n\n  const displayLayout = await createDomainObjectWithDefaults(page, {\n    type: 'Display Layout'\n  });\n\n  return {\n    redFolder,\n    blueFolder,\n    clockA,\n    clockB,\n    clockC,\n    clockD,\n    displayLayout,\n    sineWaveGeneratorAlpha,\n    sineWaveGeneratorBeta\n  };\n}\n"
  },
  {
    "path": "e2e/tests/functional/smoke.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which can quickly verify that any openmct installation is\noperable and that any type of testing can proceed.\n\nIdeally, smoke tests should make zero assumptions about how and where they are run. This makes them\nmore resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly\nas they cover a very \"thin surface\" of functionality.\n\nWhen deciding between authoring new smoke tests or functional tests, ask yourself \"would I feel\ncomfortable running this test during a live mission?\" Avoid creating or deleting Domain Objects.\nMake no assumptions about the order that elements appear in the DOM.\n*/\n\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({\n  page\n}) => {\n  //Go to baseURL\n  await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n  //Click the Create button\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Verify that Create Folder appears in the dropdown\n  await expect(page.locator(':nth-match(:text(\"Folder\"), 2)')).toBeEnabled();\n});\n\ntest('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {\n  const { myItemsFolderName } = openmctConfig;\n  //Go to baseURL\n  await page.goto('./');\n\n  //My Items to be visible\n  await expect(page.locator(`a:has-text(\"${myItemsFolderName}\")`)).toBeEnabled();\n});\n"
  },
  {
    "path": "e2e/tests/functional/staleness.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { fileURLToPath } from 'url';\n\nimport { createDomainObjectWithDefaults, navigateToObjectWithRealTime } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Staleness with Controlled Clock @clock', () => {\n  test.describe('Using ExampleStalenessProvider in realtime mode', () => {\n    let objectView;\n    let stateGenerator;\n\n    test.beforeEach(async ({ page }) => {\n      objectView = page.getByLabel('Object View');\n\n      await page.addInitScript({\n        path: fileURLToPath(\n          new URL('../../helper/addInitExampleStalenessProvider.js', import.meta.url)\n        )\n      });\n\n      // Go to baseURL\n      await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n      // Create a state generator object, since it can have sparse data\n      stateGenerator = await createDomainObjectWithDefaults(page, {\n        type: 'State Generator',\n        name: 'Test State Generator'\n      });\n\n      await navigateToObjectWithRealTime(page, stateGenerator.url);\n    });\n\n    test('indicates when telemetry is stale and clears staleness when telemetry is not stale', async ({\n      page\n    }) => {\n      // Wait until telemetry goes stale\n      await page.evaluate(async (sg) => {\n        const openmct = window.openmct;\n        const domainObject = await openmct.objects.get(sg.uuid);\n        return new Promise((resolve) => {\n          openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {\n            if (stalenessResponse.isStale) {\n              resolve();\n            }\n          });\n        });\n      }, stateGenerator);\n\n      // Verify that the object view has the is-stale class\n      await expect(objectView).toHaveClass(/is-stale/);\n\n      // Wait until telemetry goes fresh\n      await page.evaluate(async (sg) => {\n        const openmct = window.openmct;\n        const domainObject = await openmct.objects.get(sg.uuid);\n        return new Promise((resolve) => {\n          openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {\n            if (!stalenessResponse.isStale) {\n              resolve();\n            }\n          });\n        });\n      }, stateGenerator);\n\n      // Verify that the object view does not have the is-stale class\n      await expect(objectView).not.toHaveClass(/is-stale/);\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/tooltips.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis suite is dedicated to tests which verify that tooltips are displayed correctly.\n*/\n\nimport { createDomainObjectWithDefaults, expandEntireTree } from '../../appActions.js';\nimport { MISSION_TIME } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Verify tooltips', () => {\n  let folder1;\n  let folder2;\n  let folder3;\n  let sineWaveObject1;\n  let sineWaveObject2;\n  let sineWaveObject3;\n\n  const swg1Path = 'My Items / Folder Foo / SWG 1';\n  const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';\n  const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3';\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    folder1 = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Folder Foo'\n    });\n    folder2 = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Folder Bar',\n      parent: folder1.uuid\n    });\n    folder3 = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Folder Baz',\n      parent: folder2.uuid\n    });\n    // Create Sine Wave Generator\n    sineWaveObject1 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG 1',\n      parent: folder1.uuid\n    });\n    sineWaveObject1.path = swg1Path;\n    sineWaveObject2 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG 2',\n      parent: folder2.uuid\n    });\n    sineWaveObject2.path = swg2Path;\n    sineWaveObject3 = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG 3',\n      parent: folder3.uuid\n    });\n    sineWaveObject3.path = swg3Path;\n\n    // Expand all folders\n    await expandEntireTree(page);\n  });\n\n  test('display correct paths for LAD tables', async ({ page }) => {\n    // Create LAD table\n    await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      name: 'Test LAD Table'\n    });\n    // Edit LAD table\n    await page.getByLabel('Edit Object').click();\n\n    // Add the Sine Wave Generator to the LAD table and save changes.\n    //TODO Follow up with https://github.com/nasa/openmct/issues/7773\n    await page.getByLabel(`Preview ${sineWaveObject1.name}`).dragTo(page.getByLabel('Object View'));\n    await page.getByLabel(`Preview ${sineWaveObject2.name}`).dragTo(page.getByLabel('Object View'));\n    await page.getByLabel(`Preview ${sineWaveObject3.name}`).dragTo(page.getByLabel('Object View'));\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.keyboard.down('Control');\n    //Hover on something else\n    await page.getByRole('button', { name: 'Create' }).hover();\n    //Hover over the first\n    await page.getByLabel('lad name').getByText(sineWaveObject1.name).hover();\n    await expect(page.getByRole('tooltip', { name: sineWaveObject1.path })).toBeVisible();\n\n    //Hover on something else\n    await page.getByRole('button', { name: 'Create' }).hover();\n    //Hover over second object\n    await page.getByLabel('lad name').getByText(sineWaveObject2.name).hover();\n    await expect(page.getByRole('tooltip', { name: sineWaveObject2.path })).toBeVisible();\n\n    //Hover on something else\n    await page.getByRole('button', { name: 'Create' }).hover();\n    //Hover over third object\n    await page.getByLabel('lad name').getByText(sineWaveObject3.name).hover();\n    await expect(page.getByRole('tooltip', { name: sineWaveObject3.path })).toBeVisible();\n  });\n\n  test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {\n    // Create Overlay Plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Test Overlay Plots'\n    });\n    // Edit Overlay Plot\n    await page.getByLabel('Edit Object').click();\n\n    // Add the Sine Wave Generators to the  and save changes\n    await page\n      .getByLabel('Preview SWG 1 generator Object')\n      .dragTo(page.getByLabel('Plot Container Style Target'));\n    await page\n      .getByLabel('Preview SWG 2 generator Object')\n      .dragTo(page.getByLabel('Plot Container Style Target'));\n    await page\n      .getByLabel('Preview SWG 3 generator Object')\n      .dragTo(page.getByLabel('Plot Container Style Target'));\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    //Hover over Collapsed Plot Legend Components with the Control Key pressed\n    await page.keyboard.down('Control');\n    //Hover over first object\n    await page.getByText('SWG 1 Hz').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n    //Hover over another object to clear\n    await page.getByRole('button', { name: 'create' }).hover();\n    //Hover over second object\n    await page.getByText('SWG 2 Hz').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);\n    //Hover over another object to clear\n    await page.getByRole('button', { name: 'create' }).hover();\n    //Hover over third object\n    await page.getByText('SWG 3 Hz').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n    //Release the Control Key\n    await page.keyboard.up('Control');\n\n    //Expand the legend\n    await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click();\n\n    //Hover over Expanded Plot Legend Components with the Control Key pressed\n    await page.keyboard.down('Control');\n\n    await page.getByLabel('Plot Legend Expanded').getByText('SWG 1').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n    //Hover over another object to clear\n    await page.getByRole('button', { name: 'create' }).hover();\n    //Hover over second object\n    await page.getByLabel('Plot Legend Expanded').getByText('SWG 2').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);\n    //Hover over another object to clear\n    await page.getByRole('button', { name: 'create' }).hover();\n    //Hover over third object\n    await page.getByLabel('Plot Legend Expanded').getByText('SWG 3').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering over object labels', async ({ page }) => {\n    //Navigate to SWG 1 in Tree\n    await page.getByLabel('Navigate to SWG 1 generator').click();\n\n    //Expect tooltip to be the path of SWG 1\n    await page.keyboard.down('Control');\n    await page.getByRole('main').getByText('SWG 1', { exact: true }).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n    await page.keyboard.up('Control');\n\n    //Navigate to SWG 3 in Tree\n    await page.getByLabel('Navigate to SWG 3 generator').click();\n    //Expect tooltip to be the path of SWG 3\n    await page.keyboard.down('Control');\n    await page.getByRole('main').getByText('SWG 3', { exact: true }).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering over display layout pane headers', async ({ page }) => {\n    // Create Overlay Plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Test Overlay Plot'\n    });\n    // Edit Overlay Plot\n    await page.getByLabel('Edit Object').click();\n\n    await page\n      .getByLabel('Preview SWG 1 generator Object')\n      .dragTo(page.getByLabel('Plot Container Style Target'));\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Create Stacked Plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'Test Stacked Plot'\n    });\n    // Edit Stacked Plot\n    await page.getByLabel('Edit Object').click();\n\n    await page.getByLabel(`Preview ${sineWaveObject2.name}`).dragTo(page.getByLabel('Object View'));\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Create Display Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Test Display Layout'\n    });\n    // Edit Display Layout\n    await page.getByLabel('Edit Object').click();\n\n    await page\n      .getByLabel('Preview Test Overlay Plot')\n      .dragTo(page.locator('#display-layout-drop-area'), {\n        targetPosition: { x: 0, y: 0 }\n      });\n\n    //Add Display Layout below Overlay Plot\n    await page\n      .getByLabel('Preview Test Stacked Plot')\n      .dragTo(page.locator('#display-layout-drop-area'), {\n        targetPosition: { x: 0, y: 250 }\n      });\n\n    //Drag the SWG3 Object to the Display off to the right\n    await page\n      .getByLabel('Preview SWG 3 generator Object')\n      .dragTo(page.locator('#display-layout-drop-area'), {\n        targetPosition: { x: 500, y: 200 }\n      });\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    //Hover over Overlay Plot with the Control Key pressed\n    await page.keyboard.down('Control');\n\n    //Hover Overlay Plot\n    await page.getByTitle('Test Overlay Plot').hover();\n    await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Overlay Plot');\n    await page.keyboard.up('Control');\n\n    //Expand the Overlay Plot Legend and hover over the first legend item\n    await page.getByLabel('Expand Test Overlay Plot Legend').click();\n\n    await page.keyboard.down('Control');\n    await page.getByLabel('Plot Legend Item for Test').getByText('SWG').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n\n    //Hover over Stacked Plot Title\n    await page.getByTitle('Test Stacked Plot').hover();\n    await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Stacked Plot');\n\n    //Hover over SWG3 Object\n    await page.getByLabel('Alpha-numeric telemetry name for SWG').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering over flexible object labels', async ({ page }) => {\n    //Create Flexible Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout',\n      name: 'Test Flexible Layout'\n    });\n\n    //Add SWG1 and SWG3 to Flexible Layout\n    await page.getByLabel('Navigate to SWG 1 generator').dragTo(page.getByRole('row').nth(0));\n    await page\n      .getByLabel('Preview SWG 3 generator Object')\n      .dragTo(page.getByLabel('Container Handle 2'));\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    //Hover over SWG1 Object\n    await page.keyboard.down('Control');\n    await page.getByTitle('SWG 1').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n\n    //Hover over SWG3 Object\n    await page.getByTitle('SWG 3').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering over tab view labels', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Tabs View',\n      name: 'Test Tabs View'\n    });\n\n    //Add SWG1 and SWG3 to Flexible Layout\n    await page\n      .getByLabel('Navigate to SWG 1 generator')\n      .dragTo(page.getByText('Drag objects here to add them'));\n    await page.getByLabel('Preview SWG 3 generator Object').dragTo(page.getByLabel('SWG 1 tab'));\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.keyboard.down('Control');\n    await page.getByLabel('SWG 1 tab').getByText('SWG').hover();\n\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n\n    await page.getByLabel('SWG 3 tab').getByText('SWG').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering tree items', async ({ page }) => {\n    await page.keyboard.down('Control');\n    await page.getByText('SWG 1').first().hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n\n    await page.getByText('SWG 3').first().hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display correct paths when hovering search items', async ({ page }) => {\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill('SWG 3');\n\n    await page.keyboard.down('Control');\n    await page.getByLabel('Object Results').getByText('SWG').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display path for source telemetry when hovering over gauge', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Test Gauge'\n    });\n\n    await page.getByLabel('Navigate to SWG 3 generator').dragTo(page.getByRole('meter'));\n    await page.keyboard.down('Control');\n    // FIXME: We shouldn't need a `force: true` here, but the parent\n    // element blocks\n    // eslint-disable-next-line playwright/no-force-option\n    await page.getByRole('meter').hover({ position: { x: 0, y: 0 }, force: true });\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display tooltip path for notebook embeds', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Notebook',\n      name: 'Test Notebook'\n    });\n\n    await page\n      .getByLabel('Navigate to SWG 3 generator')\n      .dragTo(page.getByLabel('To start a new entry, click'));\n    await page.keyboard.down('Control');\n    await page.getByLabel('SWG 3 Notebook Embed').hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n\n  test('display tooltip path for telemetry table names @clock', async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      name: 'Test Telemetry Table'\n    });\n\n    await page\n      .getByLabel(`Navigate to ${sineWaveObject1.name}`)\n      .dragTo(page.getByLabel('Object View'));\n    await page.getByLabel(`Preview ${sineWaveObject3.name}`).dragTo(page.getByLabel('Object View'));\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Confirm that telemetry rows exist for SWG 1 and 3 and are in view\n    await expect(page.getByLabel('name table cell SWG 1').first()).toBeInViewport();\n    await expect(page.getByLabel('name table cell SWG 3').first()).toBeInViewport();\n\n    // Pause to prevent more telemetry from streaming in\n    await page.clock.pauseAt(MISSION_TIME + 30 * 1000);\n    // Run for 30 seconds to allow SOME telemetry to stream in\n    await page.clock.runFor(30 * 1000);\n\n    await page.keyboard.down('Control');\n    // Hover over SWG3 in Telemetry Table\n    await page.getByLabel('name table cell SWG 3').first().hover();\n    await expect(page.getByRole('tooltip', { name: sineWaveObject3.path })).toBeVisible();\n\n    // Release Control Key\n    await page.keyboard.up('Control');\n    // Hover somewhere else so the tooltip goes away\n    await page.getByLabel('Navigate to Test Telemetry Table').hover();\n    await expect(page.getByRole('tooltip')).toBeHidden();\n\n    await page.keyboard.down('Control');\n    // Hover over SWG1 in Telemetry Table\n    await page.getByLabel('name table cell SWG 1').first().hover();\n    await expect(page.getByRole('tooltip', { name: sineWaveObject1.path })).toBeVisible();\n  });\n\n  test('display tooltip path for recently viewed items', async ({ page }) => {\n    // drag up Recently Viewed pane\n    await page.getByLabel('Resize Recently Viewed Pane').hover();\n    await page.mouse.down();\n    await page.mouse.move(0, 300);\n    await page.mouse.up();\n\n    await page.keyboard.down('Control');\n    await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n\n    await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);\n\n    await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n  });\n\n  test('display tooltip path for time strips', async ({ page }) => {\n    // Create Time Strip\n    await createDomainObjectWithDefaults(page, {\n      type: 'Time Strip',\n      name: 'Test Time Strip'\n    });\n    // Edit Overlay Plot\n    await page.getByLabel('Edit Object').click();\n    await page\n      .getByLabel(`Preview ${sineWaveObject1.name}`)\n      .dragTo(page.getByLabel('Test Time Strip Object View'));\n    await page\n      .getByLabel(`Preview ${sineWaveObject2.name}`)\n      .dragTo(page.getByLabel('Test Time Strip Object View'));\n    await page\n      .getByLabel(`Preview ${sineWaveObject3.name}`)\n      .dragTo(page.getByLabel('Test Time Strip Object View'));\n\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    await page.keyboard.down('Control');\n    await page.getByText(sineWaveObject1.name).nth(2).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);\n\n    await page.getByText(sineWaveObject2.name).nth(2).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);\n\n    await page.getByText(sineWaveObject3.name).nth(2).hover();\n    await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/tree.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Main Tree', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb @network', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/5975'\n    });\n\n    const folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n\n    await page.getByLabel('Show selected item in tree').click();\n\n    const clock = await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      parent: folder.uuid\n    });\n\n    await page.getByLabel(`Expand ${folder.name} folder`).click();\n\n    await expect(\n      page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { name: clock.name })\n    ).toBeVisible();\n  });\n\n  test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6391'\n    });\n\n    const page2 = await page.context().newPage();\n\n    // Both pages: Go to baseURL\n    await Promise.all([\n      page.goto('./', { waitUntil: 'domcontentloaded' }),\n      page2.goto('./', { waitUntil: 'domcontentloaded' })\n    ]);\n\n    await Promise.all([\n      page.waitForURL('**/browse/mine?**'),\n      page2.waitForURL('**/browse/mine?**')\n    ]);\n\n    const page1Folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n\n    await page2.getByLabel('Expand My Items folder').click();\n\n    await expect(\n      page2\n        .getByRole('tree', { name: 'Main Tree' })\n        .getByRole('treeitem', { name: page1Folder.name })\n    ).toBeVisible();\n  });\n\n  test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @network @2p', async ({\n    page\n  }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6391'\n    });\n\n    const page2 = await page.context().newPage();\n\n    // Both pages: Go to baseURL\n    await Promise.all([\n      page.goto('./', { waitUntil: 'domcontentloaded' }),\n      page2.goto('./', { waitUntil: 'domcontentloaded' })\n    ]);\n\n    await Promise.all([\n      page.waitForURL('**/browse/mine?**'),\n      page2.waitForURL('**/browse/mine?**')\n    ]);\n\n    const page1Folder = await createDomainObjectWithDefaults(page, {\n      type: 'Folder'\n    });\n\n    await page2.getByLabel('Expand My Items folder').click();\n    await expect(\n      page2\n        .getByRole('tree', { name: 'Main Tree' })\n        .getByRole('treeitem', { name: page1Folder.name })\n    ).toBeVisible();\n  });\n\n  test('Renaming an object reorders the tree', async ({ page }) => {\n    const foo = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Foo'\n    });\n\n    const bar = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Bar'\n    });\n\n    const baz = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Baz'\n    });\n\n    let clock1 = await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      name: 'aaa'\n    });\n\n    const www = await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      name: 'www'\n    });\n\n    // Expand the root folder\n    await page.getByLabel('Expand My Items folder').click();\n\n    await test.step('Reorders objects with the same tree depth', async () => {\n      await getAndAssertTreeItems(page, ['My Items', 'aaa', 'Bar', 'Baz', 'Foo', 'www']);\n      clock1.name = 'zzz';\n      await renameObjectFromContextMenu(page, clock1.url, clock1.name);\n      await getAndAssertTreeItems(page, ['My Items', 'Bar', 'Baz', 'Foo', 'www', 'zzz']);\n    });\n\n    await test.step('Reorders links to objects as well as original objects', async () => {\n      await page.getByLabel(`Navigate to ${bar.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${baz.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View'));\n      await page.goto(foo.url);\n      await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View'));\n      await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View'));\n      // Expand the unopened folders\n      await page.getByLabel(`Expand Bar folder`).click();\n      await page.getByLabel(`Expand Baz folder`).click();\n      await page.getByLabel(`Expand Foo folder`).click();\n\n      clock1.name = '___';\n      await renameObjectFromContextMenu(page, clock1.url, clock1.name);\n      await expect(page.getByLabel('Navigate to ' + clock1.name)).toHaveCount(2);\n      await getAndAssertTreeItems(page, [\n        'My Items',\n        '___',\n        'Bar',\n        'Baz',\n        'Foo',\n        '___',\n        'www',\n        'www'\n      ]);\n    });\n  });\n  test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb @network', async ({\n    page\n  }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Foo'\n    });\n\n    // Intercept and delay request\n    const ARTIFICIAL_NETWORK_DELAY_MS = 1000;\n\n    page.route('**/_all_docs*', async (route) => {\n      await new Promise((resolve) => {\n        setTimeout(resolve, ARTIFICIAL_NETWORK_DELAY_MS);\n      });\n      return route.continue();\n    });\n\n    const allDocsRequestAbortedPromise = new Promise((resolve) => {\n      page.on('requestfailed', (request) => {\n        // check if the request was aborted\n        if (request.url().includes('_all_docs')) {\n          if (request.failure().errorText === 'net::ERR_ABORTED') {\n            resolve(true);\n          } else {\n            resolve(false);\n          }\n        }\n      });\n      page.on('requestfinished', (request) => {\n        if (request.url().includes('_all_docs')) {\n          resolve(false);\n        }\n      });\n    });\n\n    // Quickly Expand/close the root folder\n    await page\n      .getByRole('button', {\n        name: `Expand My Items folder`\n      })\n      .dblclick({ delay: 400 });\n\n    const allDocsRequestAborted = await allDocsRequestAbortedPromise;\n    expect(allDocsRequestAborted).toBe(true);\n  });\n\n  test.describe('Root objects', () => {\n    const testRootObjects = {\n      rootA: {\n        name: 'Root Object A',\n        type: 'Folder'\n      },\n      rootB: {\n        name: 'Root Object B',\n        type: 'Folder'\n      },\n      rootC: {\n        name: 'Root Object C',\n        type: 'Folder'\n      }\n    };\n\n    test.beforeEach(async ({ page }) => {\n      const openmctLocation = '/openmct.js';\n      await page.goto('./test-data/blank.html');\n      await page.setContent(`\n        <!doctype html>\n        <html>\n        <head>\n          <script src=\"${openmctLocation}\"></script>\n          <script>\n            openmct.install(openmct.plugins.LocalStorage());\n            openmct.install(openmct.plugins.Espresso());\n            openmct.install(openmct.plugins.UTCTimeSystem());\n          </script>\n          <link\n            rel=\"icon\" type=\"image/png\" href=\"/dist/favicons/favicon-96x96.png\" sizes=\"96x96\" \n            type=\"image/x-icon\"\n          />\n        </head>\n        <body>\n          <div id=\"test-container\"></div>\n        </body>\n      </html>`);\n      //First, confirm initial test assumptions\n      await page.waitForLoadState('domcontentloaded');\n      await page.evaluate((testObjects) => {\n        const openmct = window.openmct;\n\n        const testObjectProvider = {\n          get({ key }) {\n            return Promise.resolve({\n              identifier: {\n                namespace: 'test-namespace',\n                key\n              },\n              ...testObjects[key]\n            });\n          }\n        };\n\n        openmct.objects.addProvider('test-namespace', testObjectProvider);\n        openmct.objects.addRoot({ namespace: 'test-namespace', key: 'rootA' });\n        openmct.objects.addRoot({ namespace: 'test-namespace', key: 'rootB' });\n      }, testRootObjects);\n    });\n    test('Load composition correctly on load', async ({ page }) => {\n      await page.evaluate(() => {\n        const openmct = window.openmct;\n        openmct.start('#test-container');\n      });\n      await expect(page.locator('#openmct-app')).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object A' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object B' })).toBeVisible();\n    });\n    test('Show a new root object when added asynchronously', async ({ page }) => {\n      await page.evaluate(() => {\n        const openmct = window.openmct;\n        openmct.start('#test-container');\n      });\n      await expect(page.locator('#openmct-app')).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object A' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object B' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object C' })).toBeHidden();\n\n      await page.evaluate(() => {\n        const openmct = window.openmct;\n        openmct.objects.addRoot({ namespace: 'test-namespace', key: 'rootC' });\n      });\n      await expect(page.getByRole('treeitem', { name: 'Root Object C' })).toBeVisible();\n    });\n    test('Update correctly when a root object is removed asynchronously', async ({ page }) => {\n      await page.evaluate(() => {\n        const openmct = window.openmct;\n        openmct.start('#test-container');\n      });\n      await expect(page.locator('#openmct-app')).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object A' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object B' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object C' })).toBeHidden();\n\n      await page.evaluate(() => {\n        const openmct = window.openmct;\n        openmct.objects.removeRoot({ namespace: 'test-namespace', key: 'rootB' });\n      });\n      await expect(page.getByRole('treeitem', { name: 'Root Object A' })).toBeVisible();\n      await expect(page.getByRole('treeitem', { name: 'Root Object B' })).toBeHidden();\n      await expect(page.getByRole('treeitem', { name: 'Root Object C' })).toBeHidden();\n    });\n  });\n});\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {Array<string>} expected\n */\nasync function getAndAssertTreeItems(page, expected) {\n  const treeItems = page.getByRole('treeitem');\n  await expect(treeItems).toHaveCount(expected.length);\n  await expect(treeItems).toHaveText(expected, { useInnerText: true });\n}\n\n/**\n * @param {import('@playwright/test').Page} page\n * @param {string} myItemsFolderName\n * @param {string} url\n * @param {string} newName\n */\nasync function renameObjectFromContextMenu(page, url, newName) {\n  await openObjectTreeContextMenu(page, url);\n  await page.getByLabel('Edit Properties...').click();\n  const nameInput = page.getByLabel('Title', { exact: true });\n  await nameInput.fill(newName);\n  await page.getByLabel('Save').click();\n}\n\n/**\n * Open the given `domainObject`'s context menu from the object tree.\n * Expands the path to the object and scrolls to it if necessary.\n *\n * @param {import('@playwright/test').Page} page\n * @param {string} url the url to the object\n */\nasync function openObjectTreeContextMenu(page, url) {\n  await page.goto(url);\n  await page.getByLabel('Show selected item in tree').click();\n  await page.locator('.is-navigated-object').click({\n    button: 'right'\n  });\n}\n"
  },
  {
    "path": "e2e/tests/functional/ui/inspector.e2e.spec.js",
    "content": "/* eslint-disable playwright/no-conditional-in-test */\n/* eslint-disable playwright/no-conditional-expect */\n/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createDomainObjectWithDefaults } from '../../../appActions.js';\nimport { expect, test } from '../../../baseFixtures.js';\n\n// We don't need cspell to check this. It doesn't know latin.\n/* cSpell:disable */\nconst loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Molestie at elementum eu facilisis sed. Feugiat pretium nibh ipsum consequat. Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Eget nullam non nisi est sit amet. A pellentesque sit amet porttitor eget dolor morbi non arcu. Ullamcorper sit amet risus nullam eget felis eget nunc. In tellus integer feugiat scelerisque varius morbi enim nunc. Ac feugiat sed lectus vestibulum mattis ullamcorper. Nulla facilisi morbi tempus iaculis urna id volutpat. Massa vitae tortor condimentum lacinia quis vel eros donec. Ornare quam viverra orci sagittis eu. Vestibulum sed arcu non odio. In egestas erat imperdiet sed euismod nisi porta lorem. Vitae auctor eu augue ut lectus arcu bibendum at. Donec adipiscing tristique risus nec feugiat in fermentum posuere urna. Velit euismod in pellentesque massa placerat duis ultricies. Nulla facilisi nullam vehicula ipsum a arcu cursus vitae. Aliquam malesuada bibendum arcu vitae elementum curabitur.\nVel eros donec ac odio tempor orci. Et netus et malesuada fames ac turpis egestas sed tempus. Turpis egestas pretium aenean pharetra magna ac placerat. Euismod elementum nisi quis eleifend. Vitae auctor eu augue ut lectus arcu. At imperdiet dui accumsan sit amet nulla facilisi. Est velit egestas dui id ornare arcu odio ut sem. Ornare arcu dui vivamus arcu felis. Luctus venenatis lectus magna fringilla. At elementum eu facilisis sed. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Enim eu turpis egestas pretium aenean pharetra magna ac placerat. Lobortis scelerisque fermentum dui faucibus in. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Dignissim convallis aenean et tortor at risus. Enim tortor at auctor urna nunc id cursus. Libero volutpat sed cras ornare arcu dui vivamus. Scelerisque fermentum dui faucibus in ornare quam viverra.\nOdio ut sem nulla pharetra. Neque vitae tempus quam pellentesque nec. A arcu cursus vitae congue mauris. Turpis nunc eget lorem dolor sed viverra ipsum nunc aliquet. Nibh tellus molestie nunc non blandit massa enim nec. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Pulvinar elementum integer enim neque. Bibendum ut tristique et egestas. Nibh praesent tristique magna sit. Lectus magna fringilla urna porttitor. Eu non diam phasellus vestibulum lorem sed risus. Rhoncus mattis rhoncus urna neque. Rutrum tellus pellentesque eu tincidunt tortor aliquam. Pharetra convallis posuere morbi leo urna molestie at elementum. Quis commodo odio aenean sed adipiscing. Enim sit amet venenatis urna cursus eget nunc.\nEnim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing tristique risus nec feugiat in. Eleifend mi in nulla posuere sollicitudin. Donec enim diam vulputate ut pharetra sit. Ultricies mi eget mauris pharetra et ultrices neque. Eros in cursus turpis massa tincidunt dui. Cursus risus at ultrices mi tempus imperdiet nulla malesuada. Morbi enim nunc faucibus a pellentesque sit. Porttitor rhoncus dolor purus non. Ac tortor vitae purus faucibus.\nProin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.\n`;\n\nconst viewsTabsMatrix = {\n  Clock: {\n    Browse: ['Properties']\n  },\n  'Condition Set': {\n    Browse: ['Properties', 'Elements', 'Annotations'],\n    Edit: ['Elements', 'Properties']\n  },\n  'Condition Widget': {\n    Browse: ['Properties', 'Styles'],\n    Edit: ['Styles', 'Properties']\n  },\n  'Display Layout': {\n    Browse: ['Properties', 'Elements', 'Styles'],\n    Edit: ['Elements', 'Styles', 'Properties']\n  },\n  'Event Message Generator': {\n    Browse: ['Properties']\n  },\n  'Event Message Generator with Acknowledge': {\n    Browse: ['Properties']\n  },\n  'Example Imagery': {\n    Browse: ['Properties', 'Annotations']\n  },\n  'Flexible Layout': {\n    Browse: ['Properties', 'Elements', 'Styles'],\n    Edit: ['Elements', 'Styles', 'Properties']\n  },\n  Folder: {\n    Browse: ['Properties']\n  },\n  'Gantt Chart': {\n    Browse: ['Properties', 'Config', 'Elements'],\n    Edit: ['Config', 'Elements', 'Properties']\n  },\n  Gauge: {\n    Browse: ['Properties', 'Elements', 'Styles'],\n    Edit: ['Elements', 'Styles', 'Properties']\n  },\n  Graph: {\n    Browse: ['Properties', 'Config', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Properties']\n  },\n  Hyperlink: {\n    Browse: ['Properties'],\n    required: {\n      url: 'https://www.google.com',\n      displayText: 'Google'\n    }\n  },\n  'LAD Table': {\n    Browse: ['Properties', 'Config', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Properties']\n  },\n  'LAD Table Set': {\n    Browse: ['Properties', 'Config', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Properties']\n  },\n  Notebook: {\n    Browse: ['Properties']\n  },\n  'Overlay Plot': {\n    Browse: ['Properties', 'Config', 'Annotations', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']\n  },\n  'Scatter Plot': {\n    Browse: ['Properties', 'Config', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Properties']\n  },\n  'Sine Wave Generator': {\n    Browse: ['Properties', 'Annotations']\n  },\n  'Stacked Plot': {\n    Browse: ['Properties', 'Config', 'Annotations', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Properties']\n  },\n  'Tabs View': {\n    Browse: ['Properties', 'Elements', 'Styles'],\n    Edit: ['Elements', 'Styles', 'Properties']\n  },\n  'Telemetry Table': {\n    Browse: ['Properties', 'Config', 'Elements', 'Styles'],\n    Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']\n  },\n  'Time List': {\n    Browse: ['Properties', 'Config', 'Elements'],\n    Edit: ['Config', 'Elements', 'Properties']\n  },\n  'Time Strip': {\n    Browse: ['Properties', 'Elements'],\n    Edit: ['Elements', 'Properties']\n  },\n  Timer: {\n    Browse: ['Properties']\n  }\n};\n\ntest.describe('Inspector tests', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Content in inspector can be scrolled to vertically', async ({ page }) => {\n    const folderWithOverflowingTitle = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: loremIpsum\n    });\n\n    await page.goto(folderWithOverflowingTitle.url);\n\n    const inspectorPropertiesLocator = page\n      .getByRole('tabpanel', { name: 'Inspector Views' })\n      .getByLabel('Inspector Properties Details');\n    const inspectorPropertiesList = inspectorPropertiesLocator.getByRole('list');\n    const firstInspectorPropertyValue = inspectorPropertiesList\n      .getByRole('listitem')\n      .first()\n      .getByLabel('value', { exact: false });\n    const lastInspectorPropertyValue = inspectorPropertiesList\n      .getByRole('listitem')\n      .last()\n      .getByLabel('value', { exact: false });\n\n    // inspector content partially in viewport, but not all the way in viewport\n    await expect(inspectorPropertiesLocator).toBeInViewport();\n    await expect(inspectorPropertiesLocator).not.toBeInViewport({ ratio: 0.9 });\n\n    await expect(firstInspectorPropertyValue).toBeInViewport();\n    await expect(lastInspectorPropertyValue).not.toBeInViewport();\n\n    // using page.mouse.wheel to scroll the inspector content by the height of the content\n    // because click and scrollIntoView will scroll even if scrollbar not available\n    await inspectorPropertiesLocator.hover();\n    const offset = await inspectorPropertiesLocator.evaluate((el) => el.offsetHeight);\n    await page.mouse.wheel(0, offset);\n\n    await expect(lastInspectorPropertyValue).toBeInViewport();\n  });\n\n  test(`Inspector tabs show the correct tabs per view and mode`, async ({ page }) => {\n    // loop through each view type\n    for (const view of Object.keys(viewsTabsMatrix)) {\n      const viewConfig = viewsTabsMatrix[view];\n      const createOptions = {\n        type: view,\n        name: view\n      };\n\n      // create and navigate to view;\n      const objectInfo = await createDomainObjectWithDefaults(\n        page,\n        createOptions,\n        viewConfig.required ?? {}\n      );\n      await page.goto(objectInfo.url);\n\n      // verify correct number of tabs for browse mode\n      expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Browse).length);\n\n      // verify correct order of tabs for browse mode\n      for (const [index, value] of Object.entries(viewConfig.Browse)) {\n        const tab = page.getByRole('tab').nth(index);\n        await expect(tab).toHaveText(value);\n      }\n\n      // enter Edit if necessary\n      if (viewConfig.Edit) {\n        await page.getByLabel('Edit Object').click();\n\n        // verify correct number of tabs for edit mode\n        expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Edit).length);\n\n        // verify correct order of tabs for edit mode\n        for (const [index, value] of Object.entries(viewConfig.Edit)) {\n          const tab = page.getByRole('tab').nth(index);\n          await expect(tab).toHaveText(value);\n        }\n\n        await page.getByLabel('Save').first().click();\n        await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/ui/statusArea.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { expect, test } from '../../../baseFixtures.js';\n\ntest.describe('Status Area', () => {\n  let viewportHeight;\n  let viewportWidth;\n  let expandButton;\n  let collapseButton;\n  let singleLineButton;\n  let multiLineButton;\n  let indicatorsContainer;\n  let firstIndicator;\n  let indicatorsContainerLeftPosition;\n  let indicatorsContainerWidth;\n  let indicatorsContainerHeight;\n  let indicatorsContainerRightPosition;\n  let firstIndicatorPosition;\n  let indicatorsWidth;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const viewportSize = page.viewportSize();\n    viewportHeight = viewportSize.height;\n    viewportWidth = viewportSize.width;\n    expandButton = page.getByLabel('Show icon and name');\n    collapseButton = page.getByLabel('Show icon only');\n    singleLineButton = page.getByLabel('Display as single line');\n    multiLineButton = page.getByLabel('Display as multiple lines');\n    indicatorsContainer = page.getByLabel('Status Indicators');\n    const indicatorsContainerBoundingBox = await indicatorsContainer.boundingBox();\n    indicatorsContainerLeftPosition = indicatorsContainerBoundingBox.x;\n    indicatorsContainerWidth = indicatorsContainerBoundingBox.width;\n    indicatorsContainerHeight = indicatorsContainerBoundingBox.height;\n    indicatorsContainerRightPosition = indicatorsContainerLeftPosition + indicatorsContainerWidth;\n    firstIndicator = indicatorsContainer.getByRole('status').first();\n    firstIndicatorPosition = (await firstIndicator.boundingBox()).x;\n    indicatorsWidth = indicatorsContainerRightPosition - firstIndicatorPosition;\n  });\n\n  test.describe('in single line mode', () => {\n    test('restricts to one line', async ({ page }) => {\n      await test.step('toggle from allow wrap multi line indicators to restrict to single line', async () => {\n        await singleLineButton.click();\n      });\n\n      await test.step('resize viewport so that half of the indicators would overflow', async () => {\n        await page.setViewportSize({\n          width: Math.round(viewportWidth - indicatorsContainerWidth + indicatorsWidth / 2),\n          height: viewportHeight\n        });\n      });\n\n      await test.step('verify indicators restricted to one line even with overflow', async () => {\n        const indicatorsContainerHeightAfterResize = (await indicatorsContainer.boundingBox())\n          .height;\n        expect(indicatorsContainerHeightAfterResize).toBe(indicatorsContainerHeight);\n      });\n    });\n\n    test('provides overflow indication by highlighting single/multi line toggle', async ({\n      page\n    }) => {\n      await test.step('toggle from allow wrap multi line indicators to restrict to single line', async () => {\n        await singleLineButton.click();\n      });\n\n      await test.step('verify toggle button does not indicate overflow', async () => {\n        await expect(multiLineButton).toBeVisible();\n        await expect(multiLineButton).not.toHaveClass(/c-button--major/);\n      });\n\n      await test.step('resize viewport so that half of the indicators would overflow', async () => {\n        await page.setViewportSize({\n          width: Math.round(viewportWidth - indicatorsContainerWidth + indicatorsWidth / 2),\n          height: viewportHeight\n        });\n      });\n\n      await test.step('verify toggle button does indicate overflow', async () => {\n        await expect(multiLineButton).toHaveClass(/c-button--major/);\n      });\n    });\n  });\n\n  test.describe('in multi line mode', () => {\n    test('allows wrapping to span multiple lines', async ({ page }) => {\n      await test.step('resize viewport so that half of the indicators would overflow', async () => {\n        await page.setViewportSize({\n          width: Math.round(viewportWidth - indicatorsContainerWidth + indicatorsWidth / 2),\n          height: viewportHeight\n        });\n      });\n\n      await test.step('verify indicators wrap to multiple lines', async () => {\n        const indicatorsContainerHeightAfterResize = (await indicatorsContainer.boundingBox())\n          .height;\n        expect(indicatorsContainerHeightAfterResize).toBeGreaterThan(indicatorsContainerHeight);\n      });\n    });\n  });\n\n  test('allows for collapsed or expanded indicators mode', async ({ page }) => {\n    const initialExpandedPosition = (await firstIndicator.boundingBox()).x;\n\n    await test.step('verify button indicates action to collapse', async () => {\n      await expect(collapseButton).toBeVisible();\n      await expect(expandButton).toBeHidden();\n    });\n\n    await test.step('toggle indicators to collapsed mode', async () => {\n      await collapseButton.click();\n    });\n\n    const collapsedPosition = (await firstIndicator.boundingBox()).x;\n\n    await test.step('verify indicators in collapsed mode', async () => {\n      await expect(initialExpandedPosition).toBeLessThan(collapsedPosition);\n      await expect(collapseButton).toBeHidden();\n      await expect(expandButton).toBeVisible();\n    });\n\n    await test.step('verify button indicates action to expand', async () => {\n      await expect(expandButton).toBeVisible();\n      await expect(collapseButton).toBeHidden();\n    });\n\n    await test.step('toggle indicators to expanded mode', async () => {\n      await expandButton.click();\n    });\n\n    const finalExpandedPosition = (await firstIndicator.boundingBox()).x;\n\n    await test.step('verify indicators in expanded mode', async () => {\n      await expect(finalExpandedPosition).toBeLessThan(collapsedPosition);\n    });\n\n    await test.step('verify indicators size return to initial conditions', async () => {\n      // this assertion may not always be true for indicators with dynamic width (text)\n      await expect(finalExpandedPosition).toBe(initialExpandedPosition);\n    });\n\n    await test.step('verify button indicates action to collapse', async () => {\n      await expect(collapseButton).toBeVisible();\n      await expect(expandButton).toBeHidden();\n    });\n  });\n\n  test('collapse/expand toggle and single/multi line toggle work in conjunction', async ({\n    page\n  }) => {\n    await test.step('toggle indicators to collapsed mode', async () => {\n      await collapseButton.click();\n    });\n\n    await test.step('resize viewport so that indicators just do not overflow in collapsed mode', async () => {\n      const delta = (await firstIndicator.boundingBox()).x - firstIndicatorPosition;\n      console.log(delta);\n      await page.setViewportSize({\n        width: Math.round(viewportWidth - indicatorsContainerWidth + indicatorsWidth - delta / 2),\n        height: viewportHeight\n      });\n    });\n\n    await test.step('verify indicators are on a single line', async () => {\n      const indicatorsContainerHeightAfterResize = (await indicatorsContainer.boundingBox()).height;\n      expect(indicatorsContainerHeightAfterResize).toBe(indicatorsContainerHeight);\n    });\n\n    await test.step('toggle indicators to expanded mode', async () => {\n      await expandButton.click();\n    });\n\n    await test.step('verify indicators wrap to multiple lines', async () => {\n      const indicatorsContainerHeightAfterResize = (await indicatorsContainer.boundingBox()).height;\n      expect(indicatorsContainerHeightAfterResize).toBeGreaterThan(indicatorsContainerHeight);\n    });\n\n    await test.step('toggle to single line', async () => {\n      await singleLineButton.click();\n    });\n\n    await test.step('verify indicators are on a single line', async () => {\n      const indicatorsContainerHeightAfterResize = (await indicatorsContainer.boundingBox()).height;\n      expect(indicatorsContainerHeightAfterResize).toBe(indicatorsContainerHeight);\n    });\n\n    await test.step('verify indicators overflow', async () => {\n      await expect(multiLineButton).toHaveClass(/c-button--major/);\n    });\n  });\n\n  test('collapse/expand toggle and single/multi line toggle state saved in local storage', async ({\n    page\n  }) => {\n    await test.step('verify initial state', async () => {\n      await expect(collapseButton).toBeVisible();\n      await expect(singleLineButton).toBeVisible();\n      await expect(expandButton).toBeHidden();\n      await expect(multiLineButton).toBeHidden();\n    });\n\n    await test.step('change state for both toggle buttons', async () => {\n      await collapseButton.click();\n      await singleLineButton.click();\n    });\n\n    await test.step('verify alternate state after toggling', async () => {\n      await expect(expandButton).toBeVisible();\n      await expect(multiLineButton).toBeVisible();\n      await expect(collapseButton).toBeHidden();\n      await expect(singleLineButton).toBeHidden();\n    });\n\n    await test.step('reload the page', async () => {\n      await page.reload({ waitUntil: 'domcontentloaded' });\n    });\n\n    await test.step('verify alternate state after reloading', async () => {\n      await expect(expandButton).toBeVisible();\n      await expect(multiLineButton).toBeVisible();\n      await expect(collapseButton).toBeHidden();\n      await expect(singleLineButton).toBeHidden();\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/tests/functional/userRoles.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { fileURLToPath } from 'url';\n\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('User Roles', () => {\n  test('Role prompting', async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // we have multiple available roles, so it should prompt the user\n    await expect(page.getByText('Select Role')).toBeVisible();\n    await page.getByRole('combobox').selectOption('driver');\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    await expect(page.getByLabel('User Role')).toContainText('driver');\n\n    // attempt changing the role to another valid available role\n    await page.getByRole('button', { name: 'Change Role' }).click();\n    await page.getByRole('combobox').selectOption('flight');\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    await expect(page.getByLabel('User Role')).toContainText('flight');\n\n    // reload page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n    // should still be logged in as flight, and tell the user as much\n    await expect(page.getByLabel('User Role')).toContainText('flight');\n    await expect(page.getByText(\"You're logged in as role flight\")).toBeVisible();\n\n    // change active role in local storage to \"apple_role\", a bogus role not in the list of available roles\n    await page.evaluate(() => {\n      const openmct = window.openmct;\n      openmct.user.setActiveRole('apple_role');\n    });\n\n    // reload page\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    // verify that role is prompted\n    await expect(page.getByText('Select Role')).toBeVisible();\n\n    // select real role of \"driver\"\n    await page.getByRole('combobox').selectOption('driver');\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    await expect(page.getByLabel('User Role')).toContainText('driver');\n  });\n});\n"
  },
  {
    "path": "e2e/tests/mobile/smoke.e2e.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which can quickly verify that any openmct installation is\noperable and that any type of testing can proceed.\n\nIdeally, smoke tests should make zero assumptions about how and where they are run. This makes them\nmore resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly\nas they cover a very \"thin surface\" of functionality.\n\nWhen deciding between authoring new smoke tests or functional tests, ask yourself \"would I feel\ncomfortable running this test during a live mission?\" Avoid creating or deleting Domain Objects.\nMake no assumptions about the order that elements appear in the DOM.\n*/\n\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Smoke tests for @mobile', () => {\n  test.beforeEach(async ({ page }) => {\n    //For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'\n    await page.goto('./');\n  });\n\n  test('Verify that My Items Tree appears @mobile', async ({ page }) => {\n    //My Items to be visible\n    await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();\n  });\n\n  test('Verify that user can search @mobile', async ({ page }) => {\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');\n    //Search Results appear in search modal\n    await expect(\n      page.getByLabel('Object Results').getByText('Parent Display Layout')\n    ).toBeVisible();\n    //Clicking on the search result takes you to the object\n    await page.getByLabel('Object Results').getByText('Parent Display Layout').click();\n    await page.getByTitle('Collapse Browse Pane').click();\n    await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();\n  });\n\n  test('Verify that user can change time conductor @mobile', async ({ page }) => {\n    //Collapse Browse Pane to get more Time Conductor space\n    await page.getByLabel('Collapse Browse Pane').click();\n    // Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour\n    // Disabling line because we're intentionally obscuring the text\n    // eslint-disable-next-line playwright/no-force-option\n    await page.getByLabel('Time Conductor Mode').click({ force: true });\n    await page.getByLabel('Time Conductor Mode Menu').click();\n    await page.getByLabel('Real-Time').click();\n    await page.getByLabel('Start offset hours').fill('01');\n    await page.getByLabel('Submit time offsets').click();\n    await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();\n  });\n\n  test('Remove Object and confirmation dialog @mobile', async ({ page }) => {\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');\n    //Search Results appear in search modal\n    //Clicking on the search result takes you to the object\n    await page.getByLabel('Object Results').getByText('Parent Display Layout').click();\n    await page.getByTitle('Collapse Browse Pane').click();\n    await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();\n    //Verify both objects are in view\n    await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible();\n    await expect(page.getByLabel('Child Layout 2 Layout')).toBeVisible();\n    //Remove First Object to bring up confirmation dialog\n    await page.getByLabel('View menu items').nth(1).click();\n    await page.getByLabel('Remove').click();\n    await page.getByRole('button', { name: 'Ok' }).click();\n    //Verify that the object is removed\n    await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible();\n    await expect(page.getByLabel('Child Layout 2 Layout')).toHaveCount(0);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/performance/contract/imagery.contract.perf.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to performance tests to ensure that testability of performance\nis not broken upstream on Open MCT. Any assumptions made downstream will be tested here\n\nTODO:\n - Update resolution of performance config\n - Add Performance Observer on init to push all performance marks\n - Move client CDP connection to before or to a fixture\n -\n\n*/\n\nimport { expect, test } from '@playwright/test';\n\nconst filePath = 'test-data/PerformanceDisplayLayout.json';\n\ntest.describe('Performance tests', () => {\n  test.beforeEach(async ({ page, browser }, testInfo) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Click a:has-text(\"My Items\")\n    await page.locator('a:has-text(\"My Items\")').click({\n      button: 'right'\n    });\n\n    // Click text=Import from JSON\n    await page.locator('text=Import from JSON').click();\n\n    // Upload Performance Display Layout.json\n    await page.setInputFiles('#fileElem', filePath);\n\n    // Click text=OK\n    await page.locator('button:has-text(\"OK\")').click();\n\n    await expect(\n      page.locator('a:has-text(\"Performance Display Layout Display Layout\")')\n    ).toBeVisible();\n\n    //Create a Chrome Performance Timeline trace to store as a test artifact\n    console.log('\\n==== Devtools: startTracing ====\\n');\n    await browser.startTracing(page, {\n      path: `${testInfo.outputPath()}-trace.json`,\n      screenshots: true\n    });\n  });\n  test.afterEach(async ({ page, browser }) => {\n    console.log('\\n==== Devtools: stopTracing ====\\n');\n    await browser.stopTracing();\n\n    /* Measurement Section\n        / The following section includes a block of performance measurements.\n        */\n    //Get time difference between viewlarge actionability and evaluate time\n    await page.evaluate(() =>\n      window.performance.measure(\n        'machine-time-difference',\n        'viewlarge.start',\n        'viewLarge.start.test'\n      )\n    );\n\n    //Get StartTime\n    const startTime = await page.evaluate(() => window.performance.timing.navigationStart);\n    console.log('window.performance.timing.navigationStart', startTime);\n\n    //Get All Performance Marks\n    const getAllMarksJson = await page.evaluate(() =>\n      JSON.stringify(window.performance.getEntriesByType('mark'))\n    );\n    const getAllMarks = JSON.parse(getAllMarksJson);\n    console.log('window.performance.getEntriesByType(\"mark\")', getAllMarks);\n\n    //Get All Performance Measures\n    const getAllMeasuresJson = await page.evaluate(() =>\n      JSON.stringify(window.performance.getEntriesByType('measure'))\n    );\n    const getAllMeasures = JSON.parse(getAllMeasuresJson);\n    console.log('window.performance.getEntriesByType(\"measure\")', getAllMeasures);\n  });\n  /* The following test will navigate to a previously created Performance Display Layout and measure the\n    /  following metrics:\n    /  - ElementResourceTiming\n    /  - Interaction Timing\n    */\n  test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {\n    const client = await page.context().newCDPSession(page);\n    // Tell the DevTools session to record performance metrics\n    // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics\n    await client.send('Performance.enable');\n    // Go to baseURL\n    await page.goto('./');\n\n    // Search Available after Launch\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').click();\n    await page.evaluate(() => window.performance.mark('search-available'));\n    // Fill Search input\n    await page\n      .locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]')\n      .fill('Performance Display Layout');\n    await page.evaluate(() => window.performance.mark('search-entered'));\n    //Search Result Appears and is clicked\n    await Promise.all([\n      page.waitForNavigation(),\n      page.locator('a:has-text(\"Performance Display Layout\")').first().click(),\n      page.evaluate(() => window.performance.mark('click-search-result'))\n    ]);\n\n    //Time to Example Imagery Frame loads within Display Layout\n    await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' });\n    //Time to Example Imagery object loads\n    await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' });\n\n    //Get background-image url from background-image css prop\n    const backgroundImage = page.locator('.c-imagery__main-image__background-image');\n    let backgroundImageUrl = await backgroundImage.evaluate((el) => {\n      return window\n        .getComputedStyle(el)\n        .getPropertyValue('background-image')\n        .match(/url\\(([^)]+)\\)/)[1];\n    });\n    backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre\n    console.log('backgroundImageurl ' + backgroundImageUrl);\n\n    //Get ResourceTiming of background-image jpg\n    const resourceTimingJson = await page.evaluate(\n      (bgImageUrl) => JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),\n      backgroundImageUrl\n    );\n    console.log('resourceTimingJson ' + resourceTimingJson);\n\n    //Open Large view\n    await page.locator('button:has-text(\"Large View\")').click(); //This action includes the performance.mark named 'viewLarge.start'\n    await page.evaluate(() => window.performance.mark('viewLarge.start.test')); //This is a mark only to compare evaluate timing\n\n    //Time to Imagery Rendered in Large Frame\n    await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('background-image-frame'));\n\n    //Time to Example Imagery object loads\n    await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('background-image-visible'));\n\n    // Get Current number of images in thumbstrip\n    await expect(page.locator('.c-imagery__thumb').last()).toBeInViewport();\n    const thumbCount = await page.locator('.c-imagery__thumb').count();\n    console.log('number of thumbs rendered ' + thumbCount);\n    await page.locator('.c-imagery__thumb').last().click();\n\n    //Get ResourceTiming of all jpg resources\n    const resourceTimingJson2 = await page.evaluate(() =>\n      JSON.stringify(window.performance.getEntriesByType('resource'))\n    );\n    const resourceTiming = JSON.parse(resourceTimingJson2);\n    const jpgResourceTiming = resourceTiming.find((element) => element.name.includes('.jpg'));\n    console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));\n\n    // Click Close Icon\n    await page.getByRole('button', { name: 'Close' }).click();\n    await page.evaluate(() => window.performance.mark('view-large-close-button'));\n\n    //await client.send('HeapProfiler.enable');\n    await client.send('HeapProfiler.collectGarbage');\n\n    let performanceMetrics = await client.send('Performance.getMetrics');\n    console.log(performanceMetrics.metrics);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/performance/contract/notebook.contract.perf.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to performance tests to ensure that testability of performance\nis not broken upstream on Open MCT. Any assumptions made downstream will be tested here.\n\nTODO:\n - Update resolution of performance config\n - Add Performance Observer on init to push all performance marks\n - Move client CDP connection to before or to a fixture\n\n*/\n\nimport { expect, test } from '@playwright/test';\n\nconst notebookFilePath = 'test-data/PerformanceNotebook.json';\n\ntest.describe('Performance tests', () => {\n  test.beforeEach(async ({ page, browser }, testInfo) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Click a:has-text(\"My Items\")\n    await page.locator('a:has-text(\"My Items\")').click({\n      button: 'right'\n    });\n\n    // Click text=Import from JSON\n    await page.locator('text=Import from JSON').click();\n\n    // Upload Performance Display Layout.json\n    await page.setInputFiles('#fileElem', notebookFilePath);\n\n    // TODO Fix this\n    await page.locator('text=OK >> nth=1').click();\n\n    await expect(page.locator('a:has-text(\"Performance Notebook\")')).toBeVisible();\n\n    //Create a Chrome Performance Timeline trace to store as a test artifact\n    console.log('\\n==== Devtools: startTracing ====\\n');\n    await browser.startTracing(page, {\n      path: `${testInfo.outputPath()}-trace.json`,\n      screenshots: true\n    });\n  });\n  test.afterEach(async ({ page, browser }) => {\n    console.log('\\n==== Devtools: stopTracing ====\\n');\n    await browser.stopTracing();\n\n    /* Measurement Section\n        / The following section includes a block of performance measurements.\n        */\n    const startTime = await page.evaluate(() => window.performance.timing.navigationStart);\n    console.log('window.performance.timing.navigationStart', startTime);\n\n    //Get All Performance Marks\n    const getAllMarksJson = await page.evaluate(() =>\n      JSON.stringify(window.performance.getEntriesByType('mark'))\n    );\n    const getAllMarks = JSON.parse(getAllMarksJson);\n    console.log('window.performance.getEntriesByType(\"mark\")', getAllMarks);\n\n    //Get All Performance Measures\n    const getAllMeasuresJson = await page.evaluate(() =>\n      JSON.stringify(window.performance.getEntriesByType('measure'))\n    );\n    const getAllMeasures = JSON.parse(getAllMeasuresJson);\n    console.log('window.performance.getEntriesByType(\"measure\")', getAllMeasures);\n  });\n  /* The following test will navigate to a previously created Performance Display Layout and measure the\n    /  following metrics:\n    /  - ElementResourceTiming\n    /  - Interaction Timing\n    */\n  test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {\n    const client = await page.context().newCDPSession(page);\n    // Tell the DevTools session to record performance metrics\n    // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics\n    await client.send('Performance.enable');\n    // Go to baseURL\n    await page.goto('./');\n\n    // To to Search Available after Launch\n    await page.locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]').click();\n    await page.evaluate(() => window.performance.mark('search-available'));\n    // Fill Search input\n    await page\n      .locator('[aria-label=\"OpenMCT Search\"] input[type=\"search\"]')\n      .fill('Performance Notebook');\n    await page.evaluate(() => window.performance.mark('search-entered'));\n    //Search Result Appears and is clicked\n    await Promise.all([\n      page.locator('a:has-text(\"Performance Notebook\")').first().click(),\n      page.evaluate(() => window.performance.mark('click-search-result'))\n    ]);\n\n    await page\n      .locator('.c-tree__item c-tree-and-search__loading loading')\n      .waitFor({ state: 'hidden' });\n    await page.evaluate(() => window.performance.mark('search-spinner-gone'));\n\n    await page.locator('.l-browse-bar__object-name').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('object-title-appears'));\n\n    await page.locator('.c-notebook__entry >> nth=0').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('notebook-entry-appears'));\n\n    // Click Add new Notebook Entry\n    await page.locator('.c-notebook__drag-area').click();\n    await page.evaluate(() => window.performance.mark('new-notebook-entry-created'));\n\n    // Enter Notebook Entry text\n    await page.getByLabel('Notebook Entry Input').last().fill('New Entry');\n    await page.locator('.c-ne__save-button').click();\n    await page.evaluate(() => window.performance.mark('new-notebook-entry-filled'));\n\n    //Individual Notebook Entry Search\n    await page.evaluate(() => window.performance.mark('notebook-search-start'));\n    await page.locator('.c-notebook__search >> input').fill('Existing Entry');\n    await page.evaluate(() => window.performance.mark('notebook-search-filled'));\n    await page.locator('text=Search Results (3)').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('notebook-search-processed'));\n    await page.locator('.c-notebook__entry >> nth=2').waitFor({ state: 'visible' });\n    await page.evaluate(() => window.performance.mark('notebook-search-processed'));\n\n    //Clear Search\n    await page.locator('.c-search.c-notebook__search .c-search__input').hover();\n    await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();\n    await page.evaluate(() => window.performance.mark('notebook-search-processed'));\n\n    // Hover on Last\n    await page.evaluate(() => window.performance.mark('new-notebook-entry-delete'));\n    await page.locator('div.c-ne__time-and-content').last().hover();\n    await page.locator('button[title=\"Delete this entry\"]').last().click();\n    await page.locator('button:has-text(\"Ok\")').click();\n    await page.locator('.c-notebook__entry >> nth=3').waitFor({ state: 'detached' });\n    await page.evaluate(() => window.performance.mark('new-notebook-entry-deleted'));\n\n    //await client.send('HeapProfiler.enable');\n    await client.send('HeapProfiler.collectGarbage');\n\n    let performanceMetrics = await client.send('Performance.getMetrics');\n    console.log(performanceMetrics.metrics);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/performance/memory/navigation.memory.perf.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { expect, test } from '@playwright/test';\nimport { fileURLToPath } from 'url';\n\nconst memoryLeakFilePath = fileURLToPath(\n  new URL('../../../../e2e/test-data/memory-leak-detection.json', import.meta.url)\n);\n/**\n * Executes tests to verify that views are not leaking memory on navigation away. This sort of\n * memory leak is generally caused by a failure to clean up registered listeners.\n *\n * These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.\n *\n * In order to modify the test data set:\n * 1. Run Open MCT locally (npm start)\n * 2. Right click on a folder in the tree, and select \"Import From JSON\"\n * 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json\n * 4. Click \"OK\"\n * 5. Modify test objects as desired\n * 6. Right click on the \"Memory Leak Detection\" folder, and select \"Export to JSON\"\n * 7. Copy the exported file to ../test-data/memory-leak-detection.json\n *\n */\n\ntest.describe('Navigation memory leak is not detected in', () => {\n  test.beforeEach(async ({ page }) => {\n    // Go to baseURL\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    await page\n      .getByRole('treeitem', {\n        name: /My Items/\n      })\n      .click({\n        button: 'right'\n      });\n\n    await page\n      .getByRole('menuitem', {\n        name: /Import from JSON/\n      })\n      .click();\n\n    // Upload memory-leak-detection.json\n    await page.setInputFiles('#fileElem', memoryLeakFilePath);\n\n    await page\n      .getByRole('button', {\n        name: 'Save'\n      })\n      .click();\n\n    await expect(page.locator('a:has-text(\"Memory Leak Detection\")')).toBeVisible();\n  });\n\n  test('gauge', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'gauge-single-1hz-swg');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('plan', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'plan-generated');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('time list', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'time-list');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('scatter', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'scatter-plot-single-1hz-swg');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('graph', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'graph-single-1hz-swg');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('gantt chart', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'gantt-chart');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('clock', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'clock');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('timer', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'timer-far-future');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('web page (nasa.gov)', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'web-page');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('Complex Display Layout', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'complex-display-layout');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('plot view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg');\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('stacked plot view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg');\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('LAD table view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg');\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('LAD table set', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg');\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  //TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects\n  test('telemetry table view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'telemetry-table-single-1hz-swg'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  //TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects\n  test('notebook view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'notebook-memory-leak-detection-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('display layout of a single SWG alphanumeric', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(page, 'display-layout-single-1hz-swg');\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('display layout of a single SWG plot', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'display-layout-single-overlay-plot'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  //TODO: Figure out why `svg` in the CompassRose component leaks imagery\n  test('example imagery view', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'example-imagery-memory-leak-test'\n    );\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('display layout of example imagery views', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'display-layout-images-memory-leak-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('display layout with plots of swgs, alphanumerics, and condition sets', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'display-layout-simple-telemetry'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('flexible layout with plots of swgs', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'flexible-layout-plots-memory-leak-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('flexible layout of example imagery views', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'flexible-layout-images-memory-leak-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('tabbed view of display layouts and time strips', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'tab-view-simple-memory-leak-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  test('time strip view of telemetry', async ({ page }) => {\n    const result = await navigateToObjectAndDetectMemoryLeak(\n      page,\n      'time-strip-telemetry-memory-leak-test'\n    );\n\n    // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.\n    expect(result).toBe(true);\n  });\n\n  /**\n   *\n   * @param {import('@playwright/test').Page} page\n   * @param {*} objectName\n   * @returns\n   */\n  async function navigateToObjectAndDetectMemoryLeak(page, objectName) {\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    // Fill Search input\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill(objectName);\n\n    //Search Result Appears and is clicked\n    await page.getByText(objectName, { exact: true }).click();\n\n    // Register a finalization listener on the root node for the view. This tends to be the last thing to be\n    // garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy\n    // for detecting memory leaks.\n    await page.evaluate(() => {\n      window.gcPromise = new Promise((resolve) => {\n        window.fr = new FinalizationRegistry(resolve);\n        window.fr.register(\n          window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,\n          'navigatedObject',\n          window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild\n        );\n      });\n    });\n\n    // Nav back to folder\n    await page.goto('./#/browse/mine');\n\n    // This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.\n    // In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.\n    await page.evaluate(() => {\n      const gcPromise = window.gcPromise;\n      window.gcPromise = null;\n\n      // Manually invoke the garbage collector once all references are removed.\n      window.gc();\n      window.gc();\n      window.gc();\n\n      setTimeout(() => {\n        window.gc();\n      }, 1000);\n\n      return gcPromise;\n    });\n\n    // Clean up the finalization registry since we don't need it any more.\n    await page.evaluate(() => {\n      window.fr = null;\n    });\n\n    // If we get here without timing out, it means the garbage collection promise resolved and the test passed.\n    return true;\n  }\n});\n"
  },
  {
    "path": "e2e/tests/performance/tabs.perf.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appActions.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Tabs View', () => {\n  test('Renders tabbed elements only when visible', async ({ page }) => {\n    // Code to hook into the requestAnimationFrame function and log each call\n    let animationCalls = [];\n    await page.exposeFunction('logCall', (callCount) => {\n      animationCalls.push(callCount);\n    });\n    await page.addInitScript(() => {\n      const oldRequestAnimationFrame = window.requestAnimationFrame;\n      let callCount = 0;\n      window.requestAnimationFrame = function (callback) {\n        // eslint-disable-next-line no-undef\n        logCall(callCount++);\n        return oldRequestAnimationFrame(callback);\n      };\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    const tabsView = await createDomainObjectWithDefaults(page, {\n      type: 'Tabs View'\n    });\n    const table = await createDomainObjectWithDefaults(page, {\n      type: 'Telemetry Table',\n      parent: tabsView.uuid\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Event Message Generator',\n      parent: table.uuid\n    });\n    const notebook = await createDomainObjectWithDefaults(page, {\n      type: 'Notebook',\n      parent: tabsView.uuid\n    });\n    const sineWaveGenerator = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: tabsView.uuid\n    });\n\n    page.goto(tabsView.url);\n\n    // select first tab\n    await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();\n    // ensure table header visible\n    await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();\n\n    // select second tab\n    await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();\n\n    // expect notebook visible\n    await expect(page.locator('.c-notebook__drag-area')).toBeVisible();\n\n    // select third tab\n    await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();\n\n    // ensure sine wave generator visible\n    await expect(page.locator('.c-plot')).toBeVisible();\n\n    // now select notebook and clear animation calls\n    await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();\n    animationCalls = [];\n    // expect notebook visible\n    await expect(page.locator('.c-notebook__drag-area')).toBeVisible();\n    const notebookAnimationCalls = animationCalls.length;\n\n    // select sine wave generator and clear animation calls\n    animationCalls = [];\n    await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();\n\n    // ensure sine wave generator visible\n    await waitForPlotsToRender(page);\n    // we should be calling animation frames\n    const sineWaveAnimationCalls = animationCalls.length;\n    expect(sineWaveAnimationCalls).toBeGreaterThanOrEqual(notebookAnimationCalls);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/performance/tagging.perf.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests to verify plot tagging performance.\n*/\n\nimport {\n  createDomainObjectWithDefaults,\n  setFixedTimeMode,\n  setRealTimeMode,\n  waitForPlotsToRender\n} from '../../appActions.js';\nimport { basicTagsTests, createTags, testTelemetryItem } from '../../helper/plotTagsUtils.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Plot Tagging Performance', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Tags work with Overlay Plots', async ({ page }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/6822'\n    });\n    //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374\n    test.slow();\n\n    const overlayPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot'\n    });\n\n    const alphaSineWave = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave',\n      parent: overlayPlot.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave',\n      parent: overlayPlot.uuid\n    });\n\n    await page.goto(overlayPlot.url);\n\n    let canvas = page.locator('canvas').nth(1);\n\n    // Switch to real-time mode\n    // Adding tags should pause the plot\n    await setRealTimeMode(page);\n\n    await createTags({\n      page,\n      canvas\n    });\n\n    await setFixedTimeMode(page);\n\n    await basicTagsTests(page);\n    await testTelemetryItem(page, alphaSineWave);\n\n    // set to real time mode\n    await setRealTimeMode(page);\n\n    // Search for Science\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');\n\n    // click on the search result\n    await page.getByLabel('Search Result').getByText('Alpha Sine Wave').first().click();\n\n    await waitForPlotsToRender(page);\n    // expect plot to be paused\n    await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();\n\n    await setFixedTimeMode(page);\n  });\n\n  test('Tags work with Plot View of telemetry items', async ({ page }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator'\n    });\n    const canvas = page.locator('canvas').nth(1);\n    await createTags({\n      page,\n      canvas\n    });\n    await basicTagsTests(page);\n  });\n\n  test('Tags work with Stacked Plots', async ({ page }) => {\n    const stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot'\n    });\n\n    const alphaSineWave = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Alpha Sine Wave',\n      parent: stackedPlot.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Beta Sine Wave',\n      parent: stackedPlot.uuid\n    });\n\n    await page.goto(stackedPlot.url);\n\n    const canvas = page.locator('canvas').nth(1);\n\n    await createTags({\n      page,\n      canvas,\n      xEnd: 700,\n      yEnd: 240\n    });\n    await basicTagsTests(page);\n    await testTelemetryItem(page, alphaSineWave);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/a11y.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\n\ntest.describe('a11y - Default @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n  test('main view', async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/components/about.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests the branding associated with the default deployment. At least the about modal for now\n*/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../../constants.js';\n\ntest.describe('Visual - Branding @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    //Go to baseURL and Hide Tree\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Visual - About Modal', async ({ page, theme }) => {\n    // Click About button\n    await page.getByLabel('About Modal').click();\n\n    // Modify the Build information in 'about' to be consistent run-over-run\n    await expect(page.locator('id=versionInformation')).toBeEnabled();\n    await page\n      .locator('id=versionInformation')\n      .evaluate(\n        (node) =>\n          (node.innerHTML =\n            '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>')\n      );\n\n    // Take a snapshot of the About modal\n    await percySnapshot(page, `About (theme: '${theme}')`);\n  });\n});\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/components/header.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nTests the branding associated with the default deployment. At least the about modal for now\n*/\n\nimport percySnapshot from '@percy/playwright';\nimport { fileURLToPath } from 'url';\n\nimport { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../../constants.js';\n\n//Declare the component scope of the visual test for Percy\nconst header = '.l-shell__head';\n\ntest.describe('Visual - Header @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    //Go to baseURL and Hide Tree\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n    // Wait for status bar to load\n    await expect(\n      page.getByRole('status', {\n        name: 'Clock Indicator'\n      })\n    ).toBeInViewport();\n    await expect(\n      page.getByRole('status', {\n        name: 'Global Clear Indicator'\n      })\n    ).toBeInViewport();\n    await expect(\n      page.getByRole('status', {\n        name: 'Snapshot Indicator'\n      })\n    ).toBeInViewport();\n  });\n\n  test('header sizing', async ({ page, theme }) => {\n    // Click About button\n    await percySnapshot(page, `Header default (theme: '${theme}')`, {\n      scope: header\n    });\n\n    await page.getByLabel('Show icon only').click();\n\n    await percySnapshot(page, `Header Collapsed (theme: '${theme}')`, {\n      scope: header\n    });\n  });\n\n  test('show snapshot button', async ({ page, theme }) => {\n    test.slow(true, 'We have to wait for the snapshot indicator to stop flashing');\n    await page.getByLabel('Open the Notebook Snapshot Menu').click();\n\n    await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();\n\n    await expect(page.getByLabel('Show Snapshots')).toBeVisible();\n\n    /**\n     * We have to wait for the snapshot indicator to stop flashing. This happens\n     * for a really long time (15 seconds 😳).\n     * TODO: Either reduce the length of the animation, convert this to a\n     * Playwright snapshot test (and disable animations), or augment the `waitForAnimations`\n     * fixture to adjust the timeout.\n     */\n    await expect(page.locator('.has-new-snapshot')).not.toBeAttached({\n      timeout: 30 * 1000\n    });\n    await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {\n      scope: header\n    });\n    await expect(page.getByLabel('Show Snapshots')).toBeVisible();\n  });\n});\n\n//Header test with all mission status options. Right now, this is just Mission Status, but should grow over time\ntest.describe('Mission Header @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../../helper/addInitExampleUser.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await expect(page.getByText('Select Role')).toBeVisible();\n    // set role\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    // dismiss role confirmation popup\n    await page.getByRole('button', { name: 'Dismiss' }).click();\n  });\n  test('Mission status panel', async ({ page, theme }) => {\n    await percySnapshot(page, `Header default with Mission Header (theme: '${theme}')`, {\n      scope: header\n    });\n  });\n});\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/components/inspector.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { expandInspectorPane } from '../../../appActions.js';\nimport { scanForA11yViolations, test } from '../../../avpFixtures.js';\nimport { MISSION_TIME, VISUAL_FIXED_URL } from '../../../constants.js';\n\n//Declare the scope of the visual test\nconst inspectorPane = '.l-shell__pane-inspector';\n\ntest.describe('Visual - Inspector @ally @clock', () => {\n  test.use({\n    storageState: 'test-data/overlay_plot_with_delay_storage.json'\n  });\n\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {\n    // navigate to the plot\n    await page.getByRole('gridcell', { name: 'Overlay Plot with 5s Delay' }).click();\n\n    //Expand the Inspector Pane\n    await expandInspectorPane(page);\n    await percySnapshot(page, `Inspector view of overlayPlot (theme: ${theme})`, {\n      scope: inspectorPane\n    });\n    //Open Annotations Tab\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    await percySnapshot(page, `Inspector view of Annotations Tab (theme: ${theme})`, {\n      scope: inspectorPane\n    });\n  });\n});\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/components/timeConductor.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/* \n  Tests the visual appearance of the Time Conductor component\n*/\n\nimport { expect, test } from '../../../avpFixtures.js';\nimport {\n  MISSION_TIME,\n  MISSION_TIME_FIXED_END,\n  MISSION_TIME_FIXED_START,\n  VISUAL_REALTIME_URL\n} from '../../../constants.js';\n\ntest.describe('Visual - Time Conductor', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.pauseAt(MISSION_TIME);\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n  });\n\n  // FIXME: checking for a11y violations times out. Might have something to do with the frozen clock.\n  // test.afterEach(async ({ page }, testInfo) => {\n  //   await scanForA11yViolations(page, testInfo.title);\n  // });\n\n  /**\n   * FIXME: This test fails sporadically due to layout shift during initial load.\n   * The layout shift seems to be caused by loading Open MCT's icons, which are not preloaded\n   * and load after the initial DOM content has loaded.\n   * @see https://github.com/nasa/openmct/issues/7775\n   */\n  test.fixme('Visual - Time Conductor (Fixed time) @clock @snapshot', async ({ page }) => {\n    // Navigate to a specific view that uses the Time Conductor in Fixed Time mode with inspect and browse panes collapsed\n    await page.goto(\n      `./#/browse/mine?tc.mode=fixed&tc.startBound=${MISSION_TIME_FIXED_START}&tc.endBound=${MISSION_TIME_FIXED_END}&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true`,\n      {\n        waitUntil: 'domcontentloaded'\n      }\n    );\n\n    // Take a snapshot for comparison\n    const snapshot = await page.screenshot({\n      mask: []\n    });\n    expect(snapshot).toMatchSnapshot('time-conductor-fixed-time.png');\n  });\n  /**\n   * As above, small pixel differences render this test unstable.\n   */\n  test.fixme('Visual - Time Conductor (Realtime) @clock @snapshot', async ({ page }) => {\n    // Navigate to a specific view that uses the Time Conductor in Fixed Time mode with inspect and browse panes collapsed\n    await page.goto(VISUAL_REALTIME_URL, {\n      waitUntil: 'domcontentloaded'\n    });\n\n    const mask = [];\n\n    // Take a snapshot for comparison\n    const snapshot = await page.screenshot({\n      mask\n    });\n    expect(snapshot).toMatchSnapshot('time-conductor-realtime.png');\n  });\n  test(\n    'Visual - Time Conductor Axis Resized @clock @snapshot',\n    { annotation: [{ type: 'issue', description: 'https://github.com/nasa/openmct/issues/7623' }] },\n    async ({ page }) => {\n      const VISUAL_REALTIME_WITH_PANES = VISUAL_REALTIME_URL.replace(\n        'hideTree=true',\n        'hideTree=false'\n      ).replace('hideInspector=true', 'hideInspector=false');\n      // Navigate to a specific view that uses the Time Conductor in Fixed Time mode with inspect\n      await page.goto(VISUAL_REALTIME_WITH_PANES, {\n        waitUntil: 'domcontentloaded'\n      });\n\n      // Set the time conductor to fixed time mode\n      await page.getByLabel('Time Conductor Mode').click();\n      await page.getByLabel('Time Conductor Mode Menu').click();\n      await page.getByLabel('Fixed Timespan').click();\n      await page.getByLabel('Submit time bounds').click();\n\n      // Collapse the inspect and browse panes to trigger a resize of the conductor axis\n      await page.getByLabel('Collapse Inspect Pane').click();\n      await page.getByLabel('Collapse Browse Pane').click();\n\n      // manually tick the clock to trigger the resize / re-render\n      await page.clock.runFor(1000 * 2);\n\n      const mask = [];\n\n      // Take a snapshot for comparison\n      const snapshot = await page.screenshot({\n        mask\n      });\n      expect(snapshot).toMatchSnapshot('time-conductor-axis-resized.png');\n    }\n  );\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/components/tree.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults, expandTreePane } from '../../../appActions.js';\nimport { test } from '../../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../../constants.js';\n\n//Declare the scope of the visual test\nconst treePane = \"[role=tree][aria-label='Main Tree']\";\n\ntest.describe('Visual - Tree Pane', () => {\n  test('Tree pane in various states', async ({ page, theme }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    //Open Tree\n    await expandTreePane(page);\n\n    //Create a Folder Structure\n    const foo = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Foo Folder'\n    });\n\n    const bar = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Bar Folder',\n      parent: foo.uuid\n    });\n\n    const baz = await createDomainObjectWithDefaults(page, {\n      type: 'Folder',\n      name: 'Baz Folder',\n      parent: bar.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      name: 'A Clock'\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Clock',\n      name: 'Z Clock'\n    });\n\n    await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {\n      scope: treePane\n    });\n\n    await page.getByLabel('Expand My Items folder').click();\n\n    await page.goto(foo.url);\n    await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View'));\n    await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View'));\n    await page.goto(bar.url);\n    await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View'));\n    await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View'));\n    await page.goto(baz.url);\n    await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View'));\n    await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View'));\n\n    await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {\n      scope: treePane\n    });\n\n    await page.getByLabel(`Expand ${foo.name} folder`).click();\n    await page.getByLabel(`Expand ${bar.name} folder`).click();\n    await page.getByLabel(`Expand ${baz.name} folder`).click();\n\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(3 * 1000); //https://github.com/nasa/openmct/issues/7059\n\n    await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, {\n      scope: treePane\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/controlledClock.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nCollection of Visual Tests set to run with browser clock manipulate made possible with the\nclockOptions plugin fixture.\n*/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Visual - Controlled Clock @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n  test.use({\n    storageState: 'test-data/overlay_plot_with_delay_storage.json'\n  });\n\n  test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {\n    await page\n      .getByRole('gridcell', { hasText: 'Overlay Plot with 5s Delay Overlay Plot' })\n      .click();\n    //Ensure that we're on the Unnamed Overlay Plot object\n    await expect(page.getByRole('main')).toContainText('Overlay Plot with 5s Delay');\n\n    //Wait for canvas to be rendered and stop animating, but plot should not be loaded.\n    //Cannot use waitForPlotsToRender due to clockOptions.\n    await page.locator('#webglContext').hover({ trial: true });\n\n    //Take snapshot of Sine Wave Generator within Overlay Plot\n    await percySnapshot(page, `SineWaveInOverlayPlot (theme: '${theme}')`);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/defaultPlugins.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nCollection of Visual Tests set to run in a default context with default Plugins. The tests within this suite\nare only meant to run against openmct's app.js started by `npm run start` within the\n`playwright-visual.config.js` file.\n*/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\n\ntest.describe('Visual - Default @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Visual - Default Dashboard', async ({ page, theme }) => {\n    // Verify that Create button is actionable\n    await expect(page.getByRole('button', { name: 'Create' })).toBeEnabled();\n\n    // Take a snapshot of the Dashboard\n    await percySnapshot(page, `Default Dashboard (theme: '${theme}')`);\n  });\n\n  test('Visual - Default Condition Set', async ({ page, theme }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Condition Set',\n      name: 'Default Condition Set'\n    });\n\n    // Take a snapshot of the newly created Condition Set object\n    await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);\n  });\n\n  test('Visual - Default Condition Widget', async ({ page, theme }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Condition Widget',\n      name: 'Default Condition Widget'\n    });\n\n    // Take a snapshot of the newly created Condition Widget object\n    await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);\n  });\n\n  test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await page.getByRole('menuItem', { name: 'Sine Wave Generator' }).click();\n\n    await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);\n\n    await page.getByLabel('Period').click();\n    await page.getByLabel('Period').fill('');\n\n    await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);\n  });\n\n  test('Visual - Display Layout Icon is correct in Create Menu', async ({ page, theme }) => {\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await page.getByRole('menuItem', { name: 'Display Layout' }).hover({ trial: true });\n    await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);\n  });\n\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/displayLayout.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport {\n  createDomainObjectWithDefaults,\n  createStableStateTelemetry,\n  expandInspectorPane,\n  linkParameterToObject\n} from '../../appActions.js';\nimport { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';\nimport { test } from '../../pluginFixtures.js';\n\ntest.describe('Visual - Display Layout @clock', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.clock.install({ time: MISSION_TIME });\n    await page.clock.resume();\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    const parentLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Parent Layout'\n    });\n    const child2Layout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Left Layout',\n      parent: parentLayout.uuid\n    });\n    //Create this layout second so that it is on top for the position change\n    const child1Layout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Child Right Layout',\n      parent: parentLayout.uuid\n    });\n\n    const stableStateTelemetry = await createStableStateTelemetry(page);\n    await linkParameterToObject(page, stableStateTelemetry.name, child1Layout.name);\n    await linkParameterToObject(page, stableStateTelemetry.name, child2Layout.name);\n\n    // Pause the clock at a time where the telemetry is stable 20 minutes in the future\n    await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));\n\n    await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Select the child right layout\n    await page\n      .getByLabel('Child Right Layout Layout', { exact: true })\n      .getByLabel('Move Sub-object Frame')\n      .click();\n    // FIXME: Click to select the parent object (layout)\n    await page.getByLabel('Move Sub-object Frame').nth(3).click();\n\n    // Move the second layout element to the right\n    await page.getByLabel('X:').click();\n    await page.getByLabel('X:').fill('35');\n  });\n\n  test('Resize Marquee surrounds selection', async ({ page, theme }) => {\n    //This is where the beforeEach leaves off.\n    await percySnapshot(page, `Last modified object selected (theme: '${theme}')`);\n\n    await page.getByLabel('Child Left Layout Layout', { exact: true }).click();\n    await percySnapshot(page, `Only Left Child Layout has Marque selection (theme: '${theme}')`);\n\n    await page.getByLabel('Child Right Layout Layout', { exact: true }).click();\n    await percySnapshot(page, `Only Right Child Layout has Marque selection (theme: '${theme}')`);\n\n    //Only the sub-object in the Right Layout should be highlighted with a marquee\n    await page\n      .getByLabel('Child Right Layout Layout', { exact: true })\n      .getByLabel('Move Sub-object Frame')\n      .click();\n\n    await percySnapshot(\n      page,\n      `Selecting a sub-object from Right Layout selected (theme: '${theme}')`\n    );\n\n    await page.getByLabel('Parent Layout Layout', { exact: true }).click();\n    await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`);\n  });\n\n  test('Toolbar does not overflow into inspector', async ({ page, theme }) => {\n    test.info().annotations.push({\n      type: 'issue',\n      description: 'https://github.com/nasa/openmct/issues/7036'\n    });\n    await expandInspectorPane(page);\n    await page.getByLabel('Resize Inspect Pane').dragTo(page.getByLabel('X:'));\n    await page.getByRole('tab', { name: 'Elements' }).click();\n    await percySnapshot(page, `Toolbar does not overflow into inspector (theme: '${theme}')`);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/faultManagement.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport percySnapshot from '@percy/playwright';\n\nimport {\n  acknowledgeFault,\n  changeViewTo,\n  navigateToFaultManagementWithoutExample,\n  navigateToFaultManagementWithStaticExample,\n  openFaultRowMenu,\n  selectFaultItem,\n  shelveFault\n} from '../../helper/faultUtils.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Fault Management Visual Tests - without example', () => {\n  test.beforeEach(async ({ page }) => {\n    await navigateToFaultManagementWithoutExample(page);\n    await page.getByLabel('Collapse Inspect Pane').click();\n    await page.getByLabel('Show icon only').click();\n  });\n\n  test('fault management icon appears in tree', async ({ page, theme }) => {\n    // Wait for status bar to load\n    await expect(\n      page.getByRole('status', {\n        name: 'Clock Indicator'\n      })\n    ).toBeInViewport();\n    await expect(\n      page.getByRole('status', {\n        name: 'Global Clear Indicator'\n      })\n    ).toBeInViewport();\n    await expect(\n      page.getByRole('status', {\n        name: 'Snapshot Indicator'\n      })\n    ).toBeInViewport();\n\n    await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);\n  });\n});\n\ntest.describe('Fault Management Visual Tests', () => {\n  test.beforeEach(async ({ page }) => {\n    await navigateToFaultManagementWithStaticExample(page);\n    await page.getByLabel('Collapse Inspect Pane').click();\n    await page.getByLabel('Show icon only').click();\n  });\n\n  test('fault list and acknowledged faults', async ({ page, theme }) => {\n    await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);\n\n    await acknowledgeFault(page, 1);\n    await changeViewTo(page, 'acknowledged');\n\n    await percySnapshot(\n      page,\n      `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowledged view (theme: '${theme}')`\n    );\n  });\n\n  test('shelved faults', async ({ page, theme }) => {\n    await shelveFault(page, 1);\n    await changeViewTo(page, 'shelved');\n\n    /* cspell:disable-next-line */\n    // Since fault management is heavily dependent on events (bleh), we need to wait for the correct\n    // element counts\n    await expect(page.getByLabel('Select fault:')).toHaveCount(1);\n    await expect(page.getByLabel('Disposition Actions')).toHaveCount(1);\n\n    await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);\n\n    await openFaultRowMenu(page, 1);\n\n    await percySnapshot(\n      page,\n      `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')`\n    );\n  });\n\n  test('3-dot menu for fault', async ({ page, theme }) => {\n    await openFaultRowMenu(page, 1);\n\n    await percySnapshot(\n      page,\n      `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')`\n    );\n  });\n\n  test('ability to acknowledge or shelve', async ({ page, theme }) => {\n    await selectFaultItem(page, 1);\n\n    await percySnapshot(\n      page,\n      `Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/gauge.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\n\ntest.describe('Visual - Gauges', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Visual - Default Gauge', async ({ page, theme }) => {\n    await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Default Gauge'\n    });\n\n    // Take a snapshot of the newly created Gauge object\n    await percySnapshot(page, `Default Gauge (theme: '${theme}')`);\n  });\n\n  test('Visual - Needle Gauge with State Generator', async ({ page, theme }) => {\n    const needleGauge = await createDomainObjectWithDefaults(page, {\n      type: 'Gauge',\n      name: 'Needle Gauge'\n    });\n\n    //Modify the Gauge to be a Needle Gauge\n    await page.getByLabel('More actions').click();\n    await page.getByLabel('Edit Properties...').click();\n    await page.getByLabel('Gauge type', { exact: true }).selectOption('dial-needle');\n    await page.getByText('Ok').click();\n\n    //Add a State Generator to the Gauge\n    await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {\n      waitUntil: 'domcontentloaded'\n    });\n\n    // Take a snapshot of the newly created Gauge object\n    await percySnapshot(page, `Needle Gauge with no telemetry source (theme: '${theme}')`);\n\n    //Add a State Generator to the Gauge. Note this requires that snapshots are taken within 5 seconds\n    await page.getByLabel('Create', { exact: true }).click();\n    await page.getByLabel('State Generator').click();\n    await page.getByLabel('Modal Overlay').getByLabel('Navigate to Needle Gauge').click();\n    await page.getByLabel('Save').click();\n\n    //Add a State Generator to the Gauge\n    await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {\n      waitUntil: 'domcontentloaded'\n    });\n\n    // Take a snapshot of the newly created Gauge object\n    await percySnapshot(page, `Needle Gauge with State Generator (theme: '${theme}')`);\n  });\n\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/imagery.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\nconst TEN_MINUTES = 10 * 60 * 1000;\n\ntest.describe('Visual - Example Imagery', () => {\n  let exampleImagery;\n  let parentLayout;\n\n  test.beforeEach(async ({ page }) => {\n    //Start at UNIX epoch time while initializing. The clock needs to run so that debounce functions etc. work.\n    await page.clock.install({ time: 0 });\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    parentLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Parent Layout'\n    });\n\n    exampleImagery = await createDomainObjectWithDefaults(page, {\n      type: 'Example Imagery',\n      name: 'Example Imagery Test',\n      parent: parentLayout.uuid\n    });\n\n    // Modify Example Imagery to create a really stable image which will never let us down\n    await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });\n    await page.getByRole('button', { name: 'More actions' }).click();\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n    await page\n      .locator('#imageLocation-textarea')\n      .fill(\n        'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'\n      );\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.clock.pauseAt(MISSION_TIME);\n    await page.reload({ waitUntil: 'domcontentloaded' });\n\n    //Hide the Browse and Inspect panes to make the image more stable\n    await page.getByTitle('Collapse Browse Pane').click();\n    await page.getByTitle('Collapse Inspect Pane').click();\n  });\n\n  test('Example Imagery in Fixed Time', async ({ page, theme }) => {\n    await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });\n    // Scroll the rightmost thumbnail into view\n    const lastImageThumbnail = page.getByLabel('Image Thumbnail from').last();\n    await lastImageThumbnail.scrollIntoViewIfNeeded();\n    await expect(lastImageThumbnail).toBeInViewport();\n\n    await expect(page.getByLabel('Image Wrapper')).toBeVisible();\n\n    await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`);\n\n    await page.getByLabel('Image Wrapper').hover();\n\n    await percySnapshot(page, `Example Imagery Hover in Fixed Time (theme: ${theme})`);\n  });\n\n  test('Example Imagery in Real Time', async ({ page, theme }) => {\n    await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });\n\n    // Scroll the rightmost thumbnail into view\n    await scrollLastThumbnailIntoView(page);\n\n    await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();\n    await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();\n    await page.getByRole('menuitem', { name: /Real-Time/ }).click();\n    //dismiss the time conductor popup\n    await page.getByLabel('Submit time offsets').click();\n    await page.clock.pauseAt(MISSION_TIME + TEN_MINUTES);\n    await page.waitForURL(/tc\\.mode=local/);\n    await scrollLastThumbnailIntoView(page);\n\n    await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);\n  });\n\n  test('Example Imagery in Display Layout', async ({ page, theme }) => {\n    await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });\n\n    await expect(page.getByLabel('Image Wrapper')).toBeVisible();\n    await percySnapshot(page, `Example Imagery in Display Layout (theme: ${theme})`);\n  });\n});\n\nasync function scrollLastThumbnailIntoView(page) {\n  const lastImageThumbnail = page.getByLabel('Image Thumbnail from').last();\n  await lastImageThumbnail.scrollIntoViewIfNeeded();\n  await expect(lastImageThumbnail).toBeInViewport();\n}\n"
  },
  {
    "path": "e2e/tests/visual-a11y/ladTable.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Visual - LAD Table', () => {\n  let ladTable;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    // Create LAD Table\n    ladTable = await createDomainObjectWithDefaults(page, {\n      type: 'LAD Table',\n      name: 'LAD Table Test'\n    });\n    // Create SWG inside of LAD Table\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG4LAD Table Test',\n      parent: ladTable.uuid\n    });\n\n    //Modify SWG to create a really stable SWG\n    await page.getByRole('button', { name: 'More actions' }).click();\n\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n\n    //Forgive me, padre\n    await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0');\n    await page.getByRole('spinbutton', { name: 'Period' }).fill('0');\n\n    await page.getByRole('button', { name: 'Save' }).click();\n  });\n  test('Toggled column widths behave accordingly', async ({ page, theme }) => {\n    await page.goto(ladTable.url, { waitUntil: 'domcontentloaded' });\n\n    await expect(page.getByLabel('Expand Columns')).toBeVisible();\n\n    await percySnapshot(\n      page,\n      `LAD Table w/ Sine Wave Generator columns autosized (theme: ${theme})`\n    );\n\n    await page.getByLabel('Expand Columns').click();\n\n    await expect(page.getByRole('button', { name: 'Autosize Columns' })).toBeVisible();\n\n    await percySnapshot(\n      page,\n      `LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})`\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/missionStatus.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport percySnapshot from '@percy/playwright';\nimport { fileURLToPath } from 'url';\n\nimport { expect, scanForA11yViolations, test } from '../../avpFixtures.js';\n\ntest.describe('Mission Status Visual Tests @a11y', () => {\n  const GO = '1';\n  test.beforeEach(async ({ page }) => {\n    await page.addInitScript({\n      path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))\n    });\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n    await expect(page.getByText('Select Role')).toBeVisible();\n    // set role\n    await page.getByRole('button', { name: 'Select', exact: true }).click();\n    // dismiss role confirmation popup\n    await page.getByRole('button', { name: 'Dismiss' }).click();\n    await page.getByLabel('Collapse Inspect Pane').click();\n    await page.getByLabel('Collapse Browse Pane').click();\n  });\n  test('Mission status panel', async ({ page, theme }) => {\n    await page.getByLabel('Toggle Mission Status Panel').click();\n    await expect(page.getByRole('dialog', { name: 'User Control Panel' })).toBeVisible();\n    await percySnapshot(page, `Mission status panel w/ default statuses (theme: '${theme}')`);\n    await page.getByRole('combobox', { name: 'Commanding' }).selectOption(GO);\n    await expect(\n      page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })\n    ).toBeVisible();\n    await page.getByLabel('Dismiss').click();\n    await percySnapshot(page, `Mission status panel w/ non-default status (theme: '${theme}')`);\n  });\n\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/notebook.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport {\n  createDomainObjectWithDefaults,\n  expandInspectorPane,\n  expandTreePane\n} from '../../appActions.js';\nimport { expect, scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';\n\ntest.describe('Visual - Restricted Notebook @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    const restrictedNotebook = await startAndAddRestrictedNotebookObject(page);\n    await page.goto(restrictedNotebook.url + '?hideTree=true&hideInspector=true');\n  });\n\n  test('Restricted Notebook is visually correct @addInit', async ({ page, theme }) => {\n    // Take a snapshot of the newly created CUSTOM_NAME notebook\n    await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);\n  });\n});\n\ntest.describe('Visual - Notebook Snapshot @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./?hideTree=true&hideInspector=true', { waitUntil: 'domcontentloaded' });\n  });\n  test('Visual check for Snapshot Annotation', async ({ page, theme }) => {\n    await page.getByLabel('Open the Notebook Snapshot Menu').click();\n    await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();\n    await page.getByLabel('Show Snapshots').click();\n\n    await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();\n    await page.getByRole('menuitem', { name: 'View Snapshot' }).click();\n\n    await page.getByLabel('Annotate this snapshot').click();\n    await expect(page.locator('#snap-annotation-canvas')).toBeVisible();\n    // Clear the canvas\n    await page.getByRole('button', { name: 'Put text [T]' }).click();\n    // Click in the Painterro canvas to add a text annotation\n    await page.locator('.ptro-crp-el').click();\n    await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');\n    await percySnapshot(page, `Notebook Snapshot with text entry open (theme: '${theme}')`);\n\n    // When working with Painterro, we need to check that the Apply button is hidden after clicking\n    const painterroApplyButton = page.locator('.ptro-text-tool-buttons').getByTitle('Apply');\n    await painterroApplyButton.click();\n    await expect(painterroApplyButton).toBeHidden();\n\n    // Save and exit annotation window\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('button', { name: 'Done' }).click();\n\n    // Open up annotation again\n    await page.getByRole('img', { name: 'My Items thumbnail' }).click();\n    await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();\n\n    // Take a snapshot\n    await percySnapshot(page, `Notebook Snapshot with annotation (theme: '${theme}')`);\n  });\n});\n\ntest.describe('Visual - Notebook @a11y', () => {\n  let notebook;\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n    notebook = await createDomainObjectWithDefaults(page, {\n      type: 'Notebook',\n      name: 'Test Notebook'\n    });\n  });\n  test('Accepts dropped objects as embeds', async ({ page, theme }) => {\n    // Create Overlay Plot\n    await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Dropped Overlay Plot'\n    });\n\n    //Open Tree to perform drag\n    await expandTreePane(page);\n    await page.getByLabel('Expand My Items folder').click();\n\n    await page.goto(notebook.url);\n\n    await expect(page.getByLabel('Browse bar object name')).toHaveText(notebook.name);\n\n    await page\n      .getByLabel('Navigate to Dropped Overlay Plot')\n      .dragTo(page.getByLabel('To start a new entry, click here or drag and drop any object'));\n\n    await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);\n  });\n  test(\"Blur 'Add tag' on Notebook\", async ({ page, theme }) => {\n    await enterTextEntry(page, 'Entry 0');\n\n    await percySnapshot(page, `Notebook Entry (theme: '${theme}')`);\n\n    // Open the Inspector\n    await expandInspectorPane(page);\n    // Open the Annotations tab\n    await page.getByRole('tab', { name: 'Annotations' }).click();\n\n    // Take snapshot of the notebook with the Annotations tab opened\n    await percySnapshot(page, `Notebook Annotation (theme: '${theme}')`);\n\n    // Add annotation\n    await page.locator('button:has-text(\"Add Tag\")').click();\n\n    // Take snapshot of the notebook with the AutoComplete field visible\n    await percySnapshot(page, `Notebook Add Tag (theme: '${theme}')`);\n\n    // Click inside the AutoComplete field\n    await page.locator('[placeholder=\"Type to select tag\"]').click();\n\n    // Click on the \"Tags\" header (simulating a click outside the autocomplete field)\n    await page.locator('div.c-inspect-properties__header:has-text(\"Tags\")').click();\n\n    // Take snapshot of the notebook with the AutoComplete field hidden and with the \"Add Tag\" button visible\n    await percySnapshot(page, `Notebook Annotation de-select blur (theme: '${theme}')`);\n  });\n  test('Visual check of entry hover and selection', async ({ page, theme }) => {\n    // Make two entries so we can test an unselected entry\n    await enterTextEntry(page, 'Entry 0');\n    await enterTextEntry(page, 'Entry 1');\n\n    // Hover the first entry\n    await page.getByText('Entry 0').hover();\n\n    // Take a snapshot\n    await percySnapshot(page, `Notebook Non-selected Entry Hover (theme: '${theme}')`);\n\n    // Click the first entry\n    await page.getByText('Entry 0').click();\n\n    // Take a snapshot\n    await percySnapshot(page, `Notebook Selected Entry Hover (theme: '${theme}')`);\n\n    // Hover the text entry area\n    await page.getByText('Entry 0').hover();\n\n    // Take a snapshot\n    await percySnapshot(page, `Notebook Selected Entry Text Area Hover (theme: '${theme}')`);\n\n    // Click the text entry area\n    await page.getByText('Entry 0').click();\n\n    // Take a snapshot\n    await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);\n  });\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/notification.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test notification banner functionality and its accessibility attributes.\n */\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createNotification } from '../../appActions.js';\nimport { expect, scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\n\ntest.describe('Visual - Notifications @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Alert Levels and Notification List Modal', async ({ page, theme }) => {\n    await createNotification(page, {\n      message: 'Test info notification',\n      severity: 'info'\n    });\n    await expect(page.getByText('Test info notification')).toBeVisible();\n    await percySnapshot(page, `Info Notification banner shown (theme: '${theme}')`);\n    await page.getByLabel('Dismiss').click();\n    await page.getByRole('alert').waitFor({ state: 'detached' });\n    await createNotification(page, {\n      message: 'Test alert notification',\n      severity: 'alert'\n    });\n    await expect(page.getByText('Test alert notification')).toBeVisible();\n    await percySnapshot(page, `Alert Notification banner shown (theme: '${theme}')`);\n    await page.getByLabel('Dismiss').click();\n    await page.getByRole('alert').waitFor({ state: 'detached' });\n    await createNotification(page, {\n      message: 'Test error notification',\n      severity: 'error'\n    });\n    await expect(page.getByText('Test error notification')).toBeVisible();\n    await percySnapshot(page, `Error Notification banner shown (theme: '${theme}')`);\n    await page.getByLabel('Dismiss').click();\n    await page.getByRole('alert').waitFor({ state: 'detached' });\n\n    await page.getByLabel('Review 2 Notifications').click();\n    await page.getByText('Test alert notification').waitFor();\n    await percySnapshot(page, `Notification List Modal with alert and error (theme: '${theme}')`);\n\n    // Skipping due to https://github.com/nasa/openmct/issues/6820\n    // await page.getByLabel('Dismiss notification of Test alert notification').click();\n    // await page.getByText('Test alert notification').waitFor({ state: 'detached' });\n    // await percySnapshot(page, `Notification Modal with error only (theme: '${theme}')`);\n\n    await page.getByRole('button', { name: 'Clear All Notifications', exact: true }).click();\n    await percySnapshot(page, `Notification Modal after Clear All (theme: '${theme}')`);\n  });\n});\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/planning-gantt.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\nimport fs from 'fs';\n\nimport {\n  createDomainObjectWithDefaults,\n  createPlanFromJSON,\n  expandInspectorPane\n} from '../../appActions.js';\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport {\n  getFirstActivity,\n  setBoundsToSpanAllActivities,\n  setDraftStatusForPlan\n} from '../../helper/planningUtils.js';\n\nconst examplePlanSmall2 = JSON.parse(\n  fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))\n);\n\nconst FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);\n\ntest.describe('Visual - Gantt Chart @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    // Set the clock to the end of the first activity in the plan\n    // This is so we can see the \"now\" line in the plan view\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });\n    await page.clock.resume();\n\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n  test('Gantt Chart View', async ({ page, theme }) => {\n    const ganttChart = await createDomainObjectWithDefaults(page, {\n      type: 'Gantt Chart',\n      name: 'Gantt Chart Visual Test'\n    });\n    await createPlanFromJSON(page, {\n      json: examplePlanSmall2,\n      parent: ganttChart.uuid\n    });\n    await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);\n    await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`);\n\n    // Expand the inspect pane and uncheck the 'Clip Activity Names' option\n    await expandInspectorPane(page);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Clip Activity Names').click();\n\n    // Close the inspect pane and save the changes\n    await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Dismiss the notification\n    await page.getByLabel('Dismiss').click();\n\n    await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`);\n  });\n\n  test('Gantt Chart View w/ draft status', async ({ page, theme }) => {\n    const ganttChart = await createDomainObjectWithDefaults(page, {\n      type: 'Gantt Chart',\n      name: 'Gantt Chart Visual Test (Draft)'\n    });\n    const plan = await createPlanFromJSON(page, {\n      json: examplePlanSmall2,\n      parent: ganttChart.uuid\n    });\n\n    await setDraftStatusForPlan(page, plan);\n\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);\n    await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`);\n\n    // Expand the inspect pane and uncheck the 'Clip Activity Names' option\n    await expandInspectorPane(page);\n    await page.getByRole('tab', { name: 'Config' }).click();\n    await page.getByLabel('Edit Object').click();\n    await page.getByLabel('Clip Activity Names').click();\n\n    // Close the inspect pane and save the changes\n    await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();\n    await page.getByLabel('Save').click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Dismiss the notification\n    await page.getByLabel('Dismiss').click();\n\n    await percySnapshot(\n      page,\n      `Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`\n    );\n  });\n});\n\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/planning-timestrip.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\nimport fs from 'fs';\n\nimport { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { waitForAnimations } from '../../baseFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';\n\nconst examplePlanSmall2 = JSON.parse(\n  fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))\n);\n\nconst FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);\n\ntest.describe('Visual - Time Strip @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    // Set the clock to the end of the first activity in the plan\n    // This is so we can see the \"now\" line in the plan view\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });\n    await page.clock.resume();\n\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n  test('Time Strip View', async ({ page, theme }) => {\n    const timeStrip = await createDomainObjectWithDefaults(page, {\n      type: 'Time Strip',\n      name: 'Time Strip Visual Test'\n    });\n    await createPlanFromJSON(page, {\n      json: examplePlanSmall2,\n      parent: timeStrip.uuid,\n      name: 'examplePlanSmall2'\n    });\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      parent: timeStrip.uuid,\n      name: 'Sine Wave Generator'\n    });\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    //This will indirectly modify the url such that the SWG is not rendered\n    await setBoundsToSpanAllActivities(page, examplePlanSmall2, timeStrip.url);\n\n    //TODO Find a way to set the \"now\" activity line\n\n    //This will stabilize the state of the test and allow the SWG to render as empty\n    await waitForAnimations(page.getByLabel('Plot Canvas'));\n\n    // FIXME: https://github.com/nasa/openmct/issues/8005\n    // eslint-disable-next-line playwright/no-wait-for-timeout\n    await page.waitForTimeout(500);\n\n    await percySnapshot(page, `Time Strip View (theme: ${theme}) - With SWG and Plan`);\n  });\n});\n\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/planning-view.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\nimport fs from 'fs';\n\nimport { createPlanFromJSON } from '../../appActions.js';\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport {\n  getFirstActivity,\n  setBoundsToSpanAllActivities,\n  setDraftStatusForPlan\n} from '../../helper/planningUtils.js';\n\nconst examplePlanSmall2 = JSON.parse(\n  fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))\n);\n\nconst FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);\n\ntest.describe('Visual - Plan View @a11y', () => {\n  test.beforeEach(async ({ page }) => {\n    // Set the clock to the end of the first activity in the plan\n    // This is so we can see the \"now\" line in the plan view\n    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });\n    await page.clock.resume();\n\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n  });\n\n  test('Plan View', async ({ page, theme }) => {\n    const plan = await createPlanFromJSON(page, {\n      name: 'Plan Visual Test',\n      json: examplePlanSmall2\n    });\n    await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);\n    await percySnapshot(page, `Plan View (theme: ${theme})`);\n  });\n\n  test('Resize Plan View @2p', async ({ browser, theme }) => {\n    // need to set viewport to null to allow for resizing\n    const newContext = await browser.newContext({\n      viewport: null\n    });\n    const newPage = await newContext.newPage();\n\n    await newPage.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n    const plan = await createPlanFromJSON(newPage, {\n      name: 'Plan Visual Test',\n      json: examplePlanSmall2\n    });\n\n    await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);\n    // resize the window\n    await newPage.setViewportSize({ width: 800, height: 600 });\n    await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);\n  });\n\n  test('Plan View w/ draft status', async ({ page, theme }) => {\n    const plan = await createPlanFromJSON(page, {\n      name: 'Plan Visual Test (Draft)',\n      json: examplePlanSmall2\n    });\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n    await setDraftStatusForPlan(page, plan);\n\n    await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);\n    await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);\n  });\n});\n\ntest.afterEach(async ({ page }, testInfo) => {\n  await scanForA11yViolations(page, testInfo.title);\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/search.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*\nThis test suite is dedicated to tests which verify search functionality.\n*/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { expect, scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\n\ntest.describe('Grand Search @a11y', () => {\n  let conditionWidget;\n  let displayLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    displayLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Display Layout',\n      name: 'Visual Test Display Layout'\n    });\n\n    conditionWidget = await createDomainObjectWithDefaults(page, {\n      type: 'Condition Widget',\n      name: 'Visual Condition Widget',\n      parent: displayLayout.uuid\n    });\n  });\n\n  test('Can search for folder object, and subsequent search dropdown behaves properly', async ({\n    page,\n    theme\n  }) => {\n    // Navigate to display layout\n    await page.goto(displayLayout.url);\n    await expect(page.getByLabel('Browse bar object name')).toHaveText(displayLayout.name);\n\n    // Search for the object\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill(conditionWidget.name);\n    await expect(page.getByLabel('Search Result').getByText(conditionWidget.name)).toBeVisible();\n\n    //Searching for an object returns that object in the grandsearch\n    await percySnapshot(page, `Searching for Object (theme: '${theme}')`);\n\n    // Enter Edit mode on the Display Layout\n    await page.getByRole('button', { name: 'Edit Object' }).click();\n\n    // Navigate to the object while in edit mode on the display layout\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByLabel('Search Result').getByText(conditionWidget.name).click();\n\n    await percySnapshot(\n      page,\n      `Preview should display when editing enabled and search item clicked (theme: '${theme}')`\n    );\n\n    // Close the preview\n    await page.getByRole('button', { name: 'Close' }).click();\n    await percySnapshot(\n      page,\n      `Search should still be showing after preview closed (theme: '${theme}')`\n    );\n\n    // Save and finish editing the Display Layout\n    await page.getByRole('button', { name: 'Save', exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Search for the object\n    await page.getByRole('searchbox', { name: 'Search Input' }).click();\n    await page.getByRole('searchbox', { name: 'Search Input' }).fill(conditionWidget.name);\n    await expect(page.getByLabel('Search Result').getByText(conditionWidget.name)).toBeVisible();\n\n    // Navigate to the object while not in edit mode on the display layout\n    await page.getByLabel('Search Result').getByText(conditionWidget.name).click();\n\n    await percySnapshot(\n      page,\n      `Clicking on search results should navigate to them if not editing (theme: '${theme}')`\n    );\n  });\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/styling.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This test is dedicated to test notification banner functionality and its accessibility attributes.\n */\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { scanForA11yViolations, test } from '../../avpFixtures.js';\nimport { setStyles } from '../../helper/stylingUtils.js';\n\nconst setBorderColor = '#ff00ff';\nconst setBackgroundColor = '#5b0f00';\nconst setTextColor = '#e6b8af';\n\ntest.describe('Flexible Layout styling @a11y', () => {\n  let flexibleLayout;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Flexible Layout and attach to the Stacked Plot\n    flexibleLayout = await createDomainObjectWithDefaults(page, {\n      type: 'Flexible Layout',\n      name: 'Flexible Layout'\n    });\n\n    // Create a Stacked Plot and attach to the Flexible Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot1',\n      parent: flexibleLayout.uuid\n    });\n\n    // Create a Stacked Plot and attach to the Flexible Layout\n    await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot2',\n      parent: flexibleLayout.uuid\n    });\n  });\n\n  test('styling the flexible layout properly applies the styles to flex layout', async ({\n    page,\n    theme\n  }) => {\n    // Directly navigate to the flexible layout\n    await page.goto(flexibleLayout.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    await percySnapshot(page, `Flex Layout with 2 children (theme: '${theme}')`);\n\n    // Set styles using setStyles function\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Flexible Layout Column')\n    );\n\n    await percySnapshot(page, `Edit Mode Styled Flex Layout Column (theme: '${theme}')`);\n\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('group', { name: 'StackedPlot1 Frame' })\n    );\n\n    await percySnapshot(\n      page,\n      `Edit Mode Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`\n    );\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save', exact: true }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    await percySnapshot(\n      page,\n      `Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`\n    );\n  });\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n\ntest.describe('Stacked Plot styling @a11y', () => {\n  let stackedPlot;\n  test.beforeEach(async ({ page }) => {\n    await page.goto('./', { waitUntil: 'domcontentloaded' });\n\n    // Create a Stacked Plot\n    stackedPlot = await createDomainObjectWithDefaults(page, {\n      type: 'Stacked Plot',\n      name: 'StackedPlot1'\n    });\n\n    // Create an overlay plots to hold the SWGs\n    const overlayPlot1 = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Overlay Plot 1',\n      parent: stackedPlot.uuid\n    });\n\n    const overlayPlot2 = await createDomainObjectWithDefaults(page, {\n      type: 'Overlay Plot',\n      name: 'Overlay Plot 2',\n      parent: stackedPlot.uuid\n    });\n\n    // Create two SWGs and attach them to the overlay plots\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 1',\n      parent: overlayPlot1.uuid\n    });\n\n    await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'Sine Wave Generator 2',\n      parent: overlayPlot2.uuid\n    });\n  });\n\n  test('styling the flexible layout properly applies the styles to flex layout', async ({\n    page,\n    theme\n  }) => {\n    // Directly navigate to the flexible layout\n    await page.goto(stackedPlot.url, { waitUntil: 'domcontentloaded' });\n\n    // Edit Flexible Layout\n    await page.getByLabel('Edit Object').click();\n\n    // Select styles tab\n    await page.getByRole('tab', { name: 'Styles' }).click();\n\n    await percySnapshot(page, `StackedPlot with 2 SWG (theme: '${theme}')`);\n\n    // Set styles using setStyles function\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByRole('tab', { name: 'Styles' }) //Workaround for https://github.com/nasa/openmct/issues/7229\n    );\n\n    //Set Font Size to 72\n    await page.getByLabel('Set Font Size').click();\n    await page.getByRole('menuitem', { name: '72px' }).click();\n\n    //Set Font Type to Monospace Bold\n    await page.getByLabel('Set Font Type').click();\n    await page.getByRole('menuitem', { name: 'Monospace Bold' }).click();\n\n    await percySnapshot(page, `Edit Mode StackedPlot Styled (theme: '${theme}')`);\n\n    await setStyles(\n      page,\n      setBorderColor,\n      setBackgroundColor,\n      setTextColor,\n      page.getByLabel('Stacked Plot Item Overlay Plot 1')\n    );\n\n    await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);\n\n    // Save Flexible Layout\n    await page.getByRole('button', { name: 'Save' }).click();\n    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();\n    await page.getByRole('tab', { name: 'Styles' }).click();\n    await percySnapshot(page, `Saved Styled StackedPlot (theme: '${theme}')`);\n  });\n  test.afterEach(async ({ page }, testInfo) => {\n    await scanForA11yViolations(page, testInfo.title);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/visual-a11y/telemetryViews.visual.spec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport percySnapshot from '@percy/playwright';\n\nimport { createDomainObjectWithDefaults } from '../../appActions.js';\nimport { VISUAL_FIXED_URL } from '../../constants.js';\nimport { expect, test } from '../../pluginFixtures.js';\n\ntest.describe('Visual - Telemetry Views', () => {\n  let telemetry;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });\n\n    // Create SWG inside of LAD Table\n    telemetry = await createDomainObjectWithDefaults(page, {\n      type: 'Sine Wave Generator',\n      name: 'SWG4'\n    });\n\n    //Modify SWG to create a really stable SWG\n    await page.getByRole('button', { name: 'More actions' }).click();\n\n    await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();\n\n    //Forgive me, padre\n    await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0');\n    await page.getByRole('spinbutton', { name: 'Period' }).fill('0');\n\n    await page.getByRole('button', { name: 'Save' }).click();\n  });\n  test('Telemetry Table toggled column widths behave accordingly', async ({ page, theme }) => {\n    await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });\n\n    //Click this button to see telemetry display options\n    await page.getByLabel('Open the View Switcher Menu').click();\n    await page.getByLabel('Telemetry Table').click();\n\n    //Get Table View in place\n    await expect(page.getByLabel('Expand Columns')).toBeInViewport();\n\n    await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);\n\n    await page.getByLabel('Expand Columns').click();\n\n    await expect(page.getByRole('button', { name: 'Autosize Columns' })).toBeVisible();\n\n    await percySnapshot(page, `Default Telemetry Table columns expanded (theme: ${theme})`);\n\n    await page.getByLabel('More actions').click();\n\n    await percySnapshot(page, `Telemetry View Actions Menu expanded (theme: ${theme})`);\n\n    await page.getByRole('menuitem', { name: 'Pause' }).click();\n\n    await percySnapshot(page, `Telemetry View Paused (theme: ${theme})`);\n  });\n});\n"
  },
  {
    "path": "example/README.md",
    "content": "This directory is for example bundles, which are intended to illustrate \nhow to author new software components using Open MCT.\n"
  },
  {
    "path": "example/dataVisualization/ExampleDataVisualizationSourceViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport ExampleDataVisualizationSource from './components/ExampleDataVisualizationSource.vue';\n\nexport default function ExampleDataVisualizationSourceViewProvider(openmct) {\n  return {\n    key: 'exampleDataVisualizationSource',\n    name: 'Example Data Visualization Source',\n    cssClass: 'icon-telemetry',\n    canView: function (domainObject) {\n      return domainObject.type === 'exampleDataVisualizationSource';\n    },\n    canEdit: function (domainObject) {\n      if (domainObject.type === 'exampleDataVisualizationSource') {\n        return true;\n      }\n    },\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element, isEditing) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                ExampleDataVisualizationSource\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: '<example-data-visualization-source></example-data-visualization-source>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    },\n    priority: function () {\n      return 1;\n    }\n  };\n}\n"
  },
  {
    "path": "example/dataVisualization/components/ExampleDataVisualizationSource.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-table c-list-view c-list-view--selectable\">\n    <table class=\"c-table__body\">\n      <thead class=\"c-table__header\">\n        <tr>\n          <th>Name</th>\n          <th>Type</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr\n          v-for=\"item in items\"\n          :key=\"item.keyString\"\n          class=\"c-list-item js-folder-child\"\n          @click=\"selectItem(item, $event)\"\n        >\n          <td class=\"c-list-item__name\">\n            <a ref=\"objectLink\" class=\"c-object-label\">\n              <div\n                class=\"c-object-label__type-icon c-list-item__name__type-icon\"\n                :class=\"item.type.cssClass\"\n              ></div>\n              <div class=\"c-object-label__name c-list-item__name__name\">{{ item.model.name }}</div>\n            </a>\n          </td>\n          <td class=\"c-list-item__type\">\n            {{ item.type.name }}\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script>\nconst ONE_HOUR = 60 * 60 * 1000;\nexport default {\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      items: []\n    };\n  },\n  mounted() {\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.composition.on('add', this.addedTelemetry);\n    this.composition.on('remove', this.removedTelemetry);\n    this.composition.load();\n  },\n  unmounted() {\n    this.composition.off('add', this.addedTelemetry);\n    this.composition.off('remove', this.removedTelemetry);\n  },\n  methods: {\n    selectItem(item, event) {\n      event.stopPropagation();\n      const bounds = this.openmct.time.getBounds();\n      const otherBounds = {\n        start: bounds.start - ONE_HOUR,\n        end: bounds.end + ONE_HOUR\n      };\n      const selection = [\n        {\n          element: this.$el,\n          context: {\n            dataVisualization: {\n              telemetryKeys: [item.objectKeyString],\n              description: {\n                text: item.model.name,\n                icon: item.type.cssClass\n              },\n              dataRanges: [\n                {\n                  bounds: otherBounds\n                },\n                {\n                  bounds\n                }\n              ],\n              loading: false\n            },\n            item: this.domainObject\n          }\n        }\n      ];\n      this.openmct.selection.select(selection, false);\n    },\n    addedTelemetry(child) {\n      const type = this.openmct.types.get(child.type) || {\n        definition: {\n          cssClass: 'icon-object-unknown',\n          name: 'Unknown Type'\n        }\n      };\n      this.items.push({\n        model: child,\n        type: type.definition,\n        isAlias: this.keystring !== child.location,\n        objectPath: [child].concat(this.openmct.router.path),\n        objectKeyString: this.openmct.objects.makeKeyString(child.identifier)\n      });\n    },\n    removedTelemetry(identifier) {\n      this.items = this.items.filter((i) => {\n        return (\n          i.model.identifier.key !== identifier.key ||\n          i.model.identifier.namespace !== identifier.namespace\n        );\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "example/dataVisualization/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ExampleDataVisualizationSourceViewProvider from './ExampleDataVisualizationSourceViewProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new ExampleDataVisualizationSourceViewProvider(openmct));\n\n    openmct.types.addType('exampleDataVisualizationSource', {\n      name: 'Example Data Visualization Source',\n      creatable: true,\n      description: 'An example data visualization source to be used with an inspector.',\n      cssClass: 'icon-telemetry',\n      initialize(domainObject) {\n        domainObject.composition = [];\n      }\n    });\n\n    openmct.composition.addPolicy((parent, child) => {\n      if (parent.type === 'exampleDataVisualizationSource') {\n        return Object.prototype.hasOwnProperty.call(child, 'telemetry');\n      } else {\n        return true;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "example/eventGenerator/EventLimitProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const SEVERITY_CSS = {\n  WATCH: 'is-event--yellow',\n  WARNING: 'is-event--yellow',\n  DISTRESS: 'is-event--red',\n  CRITICAL: 'is-event--red',\n  SEVERE: 'is-event--purple'\n};\n\nconst NOMINAL_SEVERITY = {\n  cssClass: 'is-event--no-style',\n  name: 'NOMINAL'\n};\n\n/**\n * @typedef {Object} EvaluationResult\n * @property {string} cssClass CSS class information\n * @property {string} name a severity name\n */\nexport default class EventLimitProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  getLimitEvaluator(domainObject) {\n    const self = this;\n\n    return {\n      /**\n       * Evaluates a telemetry datum for severity.\n       *\n       * @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum})\n       * @param {object} valueMetadata metadata about the telemetry datum\n       *\n       * @returns {EvaluationResult} ({@link EvaluationResult})\n       */\n      evaluate: function (datum, valueMetadata) {\n        // prevent applying the class to the tr, only to td\n        if (!valueMetadata) {\n          return;\n        }\n\n        if (datum.severity in SEVERITY_CSS) {\n          return self.getSeverity(datum, valueMetadata);\n        }\n\n        return NOMINAL_SEVERITY;\n      }\n    };\n  }\n  getSeverity(datum, valueMetadata) {\n    if (!valueMetadata) {\n      return;\n    }\n\n    const severityValue = datum.severity;\n\n    return {\n      cssClass: SEVERITY_CSS[severityValue],\n      name: severityValue\n    };\n  }\n\n  supportsLimits(domainObject) {\n    return domainObject.type === 'eventGenerator';\n  }\n}\n"
  },
  {
    "path": "example/eventGenerator/EventMetadataProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nclass EventMetadataProvider {\n  constructor() {\n    this.METADATA_BY_TYPE = {\n      eventGenerator: {\n        values: [\n          {\n            key: 'name',\n            name: 'Name',\n            format: 'string'\n          },\n          {\n            key: 'utc',\n            name: 'Time',\n            format: 'utc',\n            hints: {\n              domain: 1\n            }\n          },\n          {\n            key: 'message',\n            name: 'Message',\n            format: 'string',\n            hints: {\n              // this is used in the EventTimelineView to provide a title for the event\n              // label can be changed to other properties for the title (e.g., the `name` property)\n              label: 0\n            }\n          }\n        ]\n      }\n    };\n\n    const inPlaceUpdateMetadataValue = {\n      key: 'messageId',\n      name: 'row identifier',\n      format: 'string',\n      useToUpdateInPlace: true\n    };\n    const eventAcknowledgeMetadataValue = {\n      key: 'acknowledge',\n      name: 'Acknowledge',\n      format: 'string'\n    };\n\n    const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);\n    eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);\n    eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);\n\n    this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;\n  }\n\n  supportsMetadata(domainObject) {\n    return Object.prototype.hasOwnProperty.call(this.METADATA_BY_TYPE, domainObject.type);\n  }\n\n  getMetadata(domainObject) {\n    return Object.assign({}, domainObject.telemetry, this.METADATA_BY_TYPE[domainObject.type]);\n  }\n}\n\nexport default EventMetadataProvider;\n"
  },
  {
    "path": "example/eventGenerator/EventTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.\n */\n\nimport { SEVERITY_CSS } from './EventLimitProvider.js';\nimport messages from './transcript.json';\n\nconst DUR_MIN = 1000;\nconst DUR_MAX = 10000;\nclass EventTelemetryProvider {\n  constructor() {\n    this.defaultSize = 25;\n  }\n\n  generateData(firstObservedTime, count, startTime, duration, name) {\n    const millisecondsSinceStart = startTime - firstObservedTime;\n    const randStartDelay = Math.max(DUR_MIN, Math.random() * DUR_MAX);\n    const utc = startTime + randStartDelay + count * duration;\n    const ind = count % messages.length;\n    const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';\n    // pick a random severity level + 1 for an undefined level so we can do nominal\n    const severity =\n      Math.random() > 0.4\n        ? Object.keys(SEVERITY_CSS)[\n            Math.floor(Math.random() * Object.keys(SEVERITY_CSS).length + 1)\n          ]\n        : undefined;\n\n    return {\n      name,\n      utc,\n      message,\n      severity\n    };\n  }\n\n  supportsRequest(domainObject) {\n    return domainObject.type === 'eventGenerator';\n  }\n\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'eventGenerator';\n  }\n\n  subscribe(domainObject, callback) {\n    const duration = domainObject.telemetry.duration * DUR_MIN;\n    const firstObservedTime = Date.now();\n    let count = 0;\n\n    const interval = setInterval(() => {\n      const startTime = Date.now();\n      const datum = this.generateData(\n        firstObservedTime,\n        count,\n        startTime,\n        duration,\n        domainObject.name\n      );\n      count += 1;\n      callback(datum);\n    }, duration);\n\n    return function () {\n      clearInterval(interval);\n    };\n  }\n\n  request(domainObject, options) {\n    let start = options.start;\n    const end = Math.min(Date.now(), options.end); // no future values\n    const duration = domainObject.telemetry.duration * DUR_MIN;\n    const size = options.size ? options.size : this.defaultSize;\n    const data = [];\n    const firstObservedTime = options.start;\n    let count = 0;\n\n    if (options.strategy === 'latest' || options.size === 1) {\n      start = end;\n    }\n\n    while (start <= end && data.length < size) {\n      const startTime = options.start + count;\n      data.push(\n        this.generateData(firstObservedTime, count, startTime, duration, domainObject.name)\n      );\n      start += duration;\n      count += 1;\n    }\n\n    return Promise.resolve(data);\n  }\n}\n\nexport default EventTelemetryProvider;\n"
  },
  {
    "path": "example/eventGenerator/EventWithAcknowledgeTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.\n */\n\nimport EventTelemetryProvider from './EventTelemetryProvider.js';\n\nclass EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {\n  constructor() {\n    super();\n\n    this.unAcknowledgedData = undefined;\n  }\n\n  generateData(firstObservedTime, count, startTime, duration, name) {\n    if (this.unAcknowledgedData === undefined) {\n      const unAcknowledgedData = super.generateData(\n        firstObservedTime,\n        count,\n        startTime,\n        duration,\n        name\n      );\n      unAcknowledgedData.messageId = unAcknowledgedData.message;\n      this.unAcknowledgedData = unAcknowledgedData;\n\n      return this.unAcknowledgedData;\n    } else {\n      const acknowledgedData = {\n        ...this.unAcknowledgedData,\n        acknowledge: 'OK'\n      };\n\n      this.unAcknowledgedData = undefined;\n\n      return acknowledgedData;\n    }\n  }\n\n  supportsRequest(domainObject) {\n    return false;\n  }\n\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'eventGeneratorWithAcknowledge';\n  }\n}\n\nexport default EventWithAcknowledgeTelemetryProvider;\n"
  },
  {
    "path": "example/eventGenerator/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport EventLimitProvider from './EventLimitProvider.js';\nimport EventMetadataProvider from './EventMetadataProvider.js';\nimport EventTelemetryProvider from './EventTelemetryProvider.js';\nimport EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';\n\nexport default function EventGeneratorPlugin(options) {\n  return function install(openmct) {\n    openmct.types.addType('eventGenerator', {\n      name: 'Event Message Generator',\n      description:\n        'For development use. Creates sample event message data that mimics a live data stream.',\n      cssClass: 'icon-generator-events',\n      creatable: true,\n      initialize: function (object) {\n        object.telemetry = {\n          duration: 5\n        };\n      }\n    });\n    openmct.telemetry.addProvider(new EventTelemetryProvider());\n    openmct.telemetry.addProvider(new EventMetadataProvider());\n\n    openmct.types.addType('eventGeneratorWithAcknowledge', {\n      name: 'Event Message Generator with Acknowledge',\n      description:\n        'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',\n      cssClass: 'icon-generator-events',\n      creatable: true,\n      initialize: function (object) {\n        object.telemetry = {\n          duration: 2.5\n        };\n      }\n    });\n\n    openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());\n\n    openmct.telemetry.addProvider(new EventLimitProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "example/eventGenerator/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from '../../src/utils/testing.js';\nimport EventMessageGeneratorPlugin from './plugin.js';\n\ndescribe('the plugin', () => {\n  let openmct;\n  const mockDomainObject = {\n    identifier: {\n      namespace: '',\n      key: 'some-value'\n    },\n    telemetry: {\n      duration: 0\n    },\n    options: {},\n    type: 'eventGenerator'\n  };\n\n  beforeEach((done) => {\n    const options = {};\n    openmct = createOpenMct();\n    openmct.install(new EventMessageGeneratorPlugin(options));\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    await resetApplicationState(openmct);\n  });\n\n  describe('the plugin', () => {\n    it('supports subscription', (done) => {\n      const unsubscribe = openmct.telemetry.subscribe(mockDomainObject, (telemetry) => {\n        expect(telemetry).not.toEqual(null);\n        expect(telemetry.message).toContain('CC: Eagle, Houston');\n        expect(unsubscribe).not.toEqual(null);\n        unsubscribe();\n        done();\n      });\n    });\n\n    it('supports requests without start/end defined', async () => {\n      const telemetry = await openmct.telemetry.request(mockDomainObject);\n      expect(telemetry[0].message).toContain('CC: Eagle, Houston');\n    });\n\n    it('supports requests with arbitrary start time in the past', async () => {\n      mockDomainObject.options.start = 100000000000; // Mar 03 1973\n      const telemetry = await openmct.telemetry.request(mockDomainObject);\n      expect(telemetry[0].message).toContain('CC: Eagle, Houston');\n    });\n  });\n});\n"
  },
  {
    "path": "example/eventGenerator/transcript.json",
    "content": "[\n  \"CC: Eagle, Houston. You're GO for landing. Over.\",\n  \"LMP: Roger. Understand. GO for landing. 3000 feet. PROGRAM ALARM.\",\n  \"CC: Copy.\",\n  \"LMP: 1201\",\n  \"CDR: 1201.\",\n  \"CC: Roger. 1201 alarm. We're GO. Same type. We're GO.\",\n  \"LMP: 2000 feet. 2000 feet, Into the AGS, 47 degrees.\",\n  \"CC: Roger.\",\n  \"LMP: 47 degrees.\",\n  \"CC: Eagle, looking great. You're GO.\",\n  \"CC: Roger. 1202. We copy it.\",\n  \"O1: LMP 35 degrees. 35 degrees. 750. Coming down to 23.fl\",\n  \"LMP: 700 feet, 21 down, 33 degrees.\",\n  \"LMP: 600 feet, down at 19.\",\n  \"LMP: 540 feet, down at - 30. Down at 15.\",\n  \"LMP: At 400 feet, down at 9.\",\n  \"LMP: ...forward.\",\n  \"LMP: 350 feet, down at 4.\",\n  \"LMP: 30, ... one-half down.\",\n  \"LMP: We're pegged on horizontal velocity.\",\n  \"LMP: 300 feet, down 3 1/2, 47 forward.\",\n  \"LMP: ... up.\",\n  \"LMP: On 1 a minute, 1 1/2 down.\",\n  \"CDR: 70.\",\n  \"LMP: Watch your shadow out there.\",\n  \"LMP: 50, down at 2 1/2, 19 forward.\",\n  \"LMP: Altitude-velocity light.\",\n  \"LMP: 3 1/2 down s 220 feet, 13 forward.\",\n  \"LMP: 1t forward. Coming down nicely.\",\n  \"LMP: 200 feet, 4 1/2 down.\",\n  \"LMP: 5 1/2 down.\",\n  \"LMP: 160, 6 - 6 1/2 down.\",\n  \"LMP: 5 1/2 down, 9 forward. That's good.\",\n  \"LMP: 120 feet.\",\n  \"LMP: 100 feet, 3 1/2 down, 9 forward. Five percent.\",\n  \"LMP: ...\",\n  \"LMP: Okay. 75 feet. There's looking good. Down a half, 6 forward.\",\n  \"CC:  60 seconds.\",\n  \"LMP: Lights on. ...\",\n  \"LMP: Down 2 1/2. Forward. Forward. Good.\",\n  \"LMP: 40 feet, down 2 1/2. Kicking up some dust.\",\n  \"LMP: 30 feet, 2 1/2 down. Faint shadow.\",\n  \"LMP: 4 forward. 4 forward. Drifting to the right a little. Okay. Down a half.\",\n  \"CC:  30 seconds.\",\n  \"CDR: Forward drift?\",\n  \"LMP: Yes.\",\n  \"LMP: Okay.\",\n  \"LMP: CONTACT LIGHT.\",\n  \"LMP: Okay. ENGINE STOP.\",\n  \"LMP: ACA - out of DETENT.\",\n  \"CDR: Out of DETENT.\",\n  \"LMP: MODE CONTROL - both AUTO. DESCENT ENGINE COMMAND OVERRIDE - OFF. ENGINE ARM - OFF.\",\n  \"LMP: 413 is in.\",\n  \"CC:  We copy you down, Eagle.\",\n  \"CDR: Houston, Tranquility Base here.\",\n  \"CDR: THE EAGLE HAS LANDED.\"\n]\n"
  },
  {
    "path": "example/exampleStalenessProvider/ExampleStalenessProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @implements {import('src/api/telemetry/TelemetryAPI').StalenessProvider}\n */\nexport default class ExampleStalenessProvider {\n  #intervalId;\n  constructor(openmct, config = { stalenessInterval: 3000, reportStalenessInterval: 300 }) {\n    this.openmct = openmct;\n    this.stalenessInterval = config.stalenessInterval;\n    this.reportStalenessInterval = config.reportStalenessInterval;\n    this.observingStaleness = {};\n    this.latestReceivedTelemetry = {};\n\n    this.#observeTimeSystem();\n    this.#observeStaleness();\n  }\n\n  #observeTimeSystem() {\n    this.openmct.time.on('timeSystemChanged', () => {\n      this.timeSystem = this.openmct.time.getTimeSystem();\n    });\n  }\n\n  supportsStaleness(domainObject) {\n    return this.openmct.telemetry.isTelemetryObject(domainObject);\n  }\n\n  subscribeToStaleness(domainObject, callback) {\n    const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n    this.observingStaleness[keyString] = { callback };\n    const unsubscribe = this.openmct.telemetry.subscribe(domainObject, (datum) => {\n      this.#updateLatestReceivedTelemetry(domainObject, datum);\n    });\n\n    return () => {\n      delete this.observingStaleness[keyString];\n      unsubscribe?.();\n      if (Object.keys(this.observingStaleness).length === 0) {\n        clearInterval(this.#intervalId);\n      }\n    };\n  }\n\n  #observeStaleness() {\n    this.#intervalId = setInterval(() => {\n      if (!this.timeSystem) {\n        return;\n      }\n\n      Object.entries(this.observingStaleness).forEach(([keyString, observer]) => {\n        if (!this.latestReceivedTelemetry[keyString]) {\n          return;\n        }\n\n        const now = this.openmct.time.now();\n        const isStale = now - this.latestReceivedTelemetry[keyString] >= this.stalenessInterval;\n\n        // Overly reports when not stale because of generated telemetry flake\n        if (!isStale || !observer.response || isStale !== observer.response.isStale) {\n          const stalenessResponseObject = {\n            isStale,\n            [this.timeSystem.key]: now\n          };\n\n          observer.response = stalenessResponseObject;\n          observer.callback(stalenessResponseObject);\n        }\n      });\n    }, this.reportStalenessInterval);\n  }\n\n  /**\n   * @param {*} domainObject\n   * @returns {import('src/api/telemetry/TelemetryAPI').StalenessResponseObject}\n   */\n  async isStale(domainObject) {\n    if (!this.timeSystem) {\n      this.timeSystem = this.openmct.time.getTimeSystem();\n    }\n\n    const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n    if (!this.latestReceivedTelemetry[keyString]) {\n      // Naively assumes sorted request response so uses last datum in array\n      const response = await this.openmct.telemetry.request(domainObject, { strategy: 'latest' });\n      const lastDatum = response?.length ? response[response.length - 1] : undefined;\n      this.#updateLatestReceivedTelemetry(domainObject, lastDatum);\n    }\n\n    const timestamp = this.latestReceivedTelemetry[keyString];\n    if (timestamp) {\n      const isStale = this.openmct.time.now() - timestamp >= this.stalenessInterval;\n\n      const stalenessResponseObject = { isStale };\n      stalenessResponseObject[this.timeSystem.key] = timestamp;\n\n      return stalenessResponseObject;\n    }\n  }\n\n  #updateLatestReceivedTelemetry(domainObject, datum) {\n    const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n    const metadata = this.openmct.telemetry.getMetadata(domainObject);\n    const metadataValue = metadata.value(this.timeSystem.key) || { format: this.timeSystem.key };\n    const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n    const timestamp = valueFormatter.parse(datum);\n\n    if (timestamp) {\n      this.latestReceivedTelemetry[keyString] = timestamp;\n    } else {\n      console.warn('Could not parse timestamp for staleness check');\n    }\n  }\n}\n"
  },
  {
    "path": "example/exampleStalenessProvider/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ExampleStalenessProvider from './ExampleStalenessProvider.js';\n\nexport default function ExampleStalenessPlugin(config) {\n  return function install(openmct) {\n    const stalenessProvider = new ExampleStalenessProvider(openmct, config);\n    openmct.telemetry.addProvider(stalenessProvider);\n  };\n}\n"
  },
  {
    "path": "example/exampleTags/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport availableTags from './tags.json';\n\n/**\n@typedef {{\n    namespaceToSaveAnnotations: string\n}} TagsPluginOptions\n*/\n\n/**\n * @typedef {TagsPluginOptions} options\n * @returns {function} The plugin install function\n */\nexport default function exampleTagsPlugin(options) {\n  return function install(openmct) {\n    if (options?.namespaceToSaveAnnotations) {\n      openmct.annotation.setNamespaceToSaveAnnotations(options?.namespaceToSaveAnnotations);\n    }\n\n    Object.keys(availableTags.tags).forEach((tagKey) => {\n      const tagDefinition = availableTags.tags[tagKey];\n      openmct.annotation.defineTag(tagKey, tagDefinition);\n    });\n  };\n}\n"
  },
  {
    "path": "example/exampleTags/tags.json",
    "content": "{\n  \"tags\": {\n    \"46a62ad1-bb86-4f88-9a17-2a029e12669d\": {\n      \"label\": \"Science\",\n      \"backgroundColor\": \"#cc0000\",\n      \"foregroundColor\": \"#ffffff\"\n    },\n    \"65f150ef-73b7-409a-b2e8-258cbd8b7323\": {\n      \"label\": \"Driving\",\n      \"backgroundColor\": \"#ffad32\",\n      \"foregroundColor\": \"#333333\"\n    },\n    \"f156b038-c605-46db-88a6-67cf2489a371\": {\n      \"label\": \"Drilling\",\n      \"backgroundColor\": \"#b0ac4e\",\n      \"foregroundColor\": \"#FFFFFF\"\n    }\n  }\n}\n"
  },
  {
    "path": "example/exampleUser/ExampleUserProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { v4 as uuid } from 'uuid';\n\nimport createExampleUser from './exampleUserCreator.js';\n\nconst STATUSES = [\n  {\n    key: 'NO_STATUS',\n    label: 'Not set',\n    iconClass: 'icon-question-mark',\n    iconClassPoll: 'icon-status-poll-question-mark'\n  },\n  {\n    key: 'GO',\n    label: 'Go',\n    iconClass: 'icon-check',\n    iconClassPoll: 'icon-status-poll-question-mark',\n    statusClass: 's-status-ok',\n    statusBgColor: '#33cc33',\n    statusFgColor: '#000'\n  },\n  {\n    key: 'MAYBE',\n    label: 'Maybe',\n    iconClass: 'icon-alert-triangle',\n    iconClassPoll: 'icon-status-poll-question-mark',\n    statusClass: 's-status-warning',\n    statusBgColor: '#ffb66c',\n    statusFgColor: '#000'\n  },\n  {\n    key: 'NO_GO',\n    label: 'No go',\n    iconClass: 'icon-circle-slash',\n    iconClassPoll: 'icon-status-poll-question-mark',\n    statusClass: 's-status-error',\n    statusBgColor: '#9900cc',\n    statusFgColor: '#fff'\n  }\n];\n\nconst MISSION_STATUSES = [\n  {\n    key: 0,\n    label: 'NO GO'\n  },\n  {\n    key: 1,\n    label: 'GO'\n  }\n];\n/**\n * @implements {StatusUserProvider}\n */\nexport default class ExampleUserProvider extends EventEmitter {\n  #actionToStatusMap;\n  constructor(\n    openmct,\n    { statusRoles } = {\n      statusRoles: []\n    }\n  ) {\n    super();\n\n    this.openmct = openmct;\n    this.#actionToStatusMap = {\n      Imagery: MISSION_STATUSES[0],\n      Commanding: MISSION_STATUSES[0],\n      Driving: MISSION_STATUSES[0]\n    };\n    this.user = undefined;\n    this.loggedIn = false;\n    this.autoLoginUser = undefined;\n    this.statusRoleValues = statusRoles.map((role) => ({\n      role: role,\n      status: STATUSES[0]\n    }));\n    this.pollQuestion = undefined;\n    this.statusRoles = statusRoles;\n\n    this.ExampleUser = createExampleUser(this.openmct.user.User);\n    this.loginPromise = undefined;\n  }\n\n  isLoggedIn() {\n    return this.loggedIn;\n  }\n\n  autoLogin(username) {\n    this.autoLoginUser = username;\n  }\n\n  getCurrentUser() {\n    if (!this.loginPromise) {\n      this.loginPromise = this._login().then(() => this.user);\n    }\n\n    return this.loginPromise;\n  }\n\n  canProvideStatusForRole(role) {\n    return this.statusRoles.includes(role);\n  }\n\n  canSetPollQuestion() {\n    return Promise.resolve(true);\n  }\n\n  canSetMissionStatus() {\n    return Promise.resolve(true);\n  }\n\n  hasRole(roleId) {\n    if (!this.loggedIn) {\n      Promise.resolve(undefined);\n    }\n\n    return Promise.resolve(this.user.getRoles().includes(roleId));\n  }\n\n  getPossibleRoles() {\n    return this.user.getRoles();\n  }\n\n  getPossibleMissionActions() {\n    return Promise.resolve(Object.keys(this.#actionToStatusMap));\n  }\n\n  getPossibleMissionActionStatuses() {\n    return Promise.resolve(MISSION_STATUSES);\n  }\n\n  getStatusForMissionAction(action) {\n    return Promise.resolve(this.#actionToStatusMap[action]);\n  }\n\n  setStatusForMissionAction(action, status) {\n    this.#actionToStatusMap[action] = status;\n    this.emit('missionStatusChange', {\n      action,\n      status\n    });\n\n    return true;\n  }\n\n  getAllStatusRoles() {\n    return Promise.resolve(this.statusRoles);\n  }\n\n  getStatusForRole(role) {\n    const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);\n\n    return Promise.resolve(statusForRole?.status);\n  }\n\n  async getDefaultStatusForRole(role) {\n    const allRoles = await this.getPossibleStatuses();\n\n    return allRoles?.[0];\n  }\n\n  setStatusForRole(role, status) {\n    status.timestamp = Date.now();\n    const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);\n    this.statusRoleValues[matchingIndex].status = status;\n    this.emit('statusChange', {\n      role,\n      status\n    });\n\n    return true;\n  }\n\n  // eslint-disable-next-line require-await\n  async getPollQuestion() {\n    if (this.pollQuestion) {\n      return this.pollQuestion;\n    } else {\n      return undefined;\n    }\n  }\n\n  setPollQuestion(pollQuestion) {\n    if (!pollQuestion) {\n      // If the poll question is undefined, set it to a blank string.\n      // This behavior better reflects how other telemetry systems\n      // deal with undefined poll questions.\n      pollQuestion = '';\n    }\n\n    this.pollQuestion = {\n      question: pollQuestion,\n      timestamp: Date.now()\n    };\n    this.emit('pollQuestionChange', this.pollQuestion);\n\n    return true;\n  }\n\n  getPossibleStatuses() {\n    return Promise.resolve(STATUSES);\n  }\n\n  _login() {\n    const id = uuid();\n\n    // for testing purposes, this will skip the form, this wouldn't be used in\n    // a normal authentication process\n    if (this.autoLoginUser) {\n      this.user = new this.ExampleUser(id, this.autoLoginUser, ['flight', 'driver', 'observer']);\n      this.loggedIn = true;\n\n      return Promise.resolve();\n    }\n\n    const formStructure = {\n      title: 'Login',\n      sections: [\n        {\n          rows: [\n            {\n              key: 'username',\n              control: 'textfield',\n              name: 'Username',\n              pattern: '\\\\S+',\n              required: true,\n              cssClass: 'l-input-lg',\n              value: ''\n            }\n          ]\n        }\n      ],\n      buttons: {\n        submit: {\n          label: 'Login'\n        }\n      }\n    };\n\n    return this.openmct.forms.showForm(formStructure).then(\n      (info) => {\n        this.user = new this.ExampleUser(id, info.username, ['example-role']);\n        this.loggedIn = true;\n      },\n      () => {\n        // user canceled, setting a default username\n        this.user = new this.ExampleUser(id, 'Pat', ['example-role']);\n        this.loggedIn = true;\n      }\n    );\n  }\n}\n/**\n * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider\n */\n"
  },
  {
    "path": "example/exampleUser/exampleUserCreator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function createExampleUser(UserClass) {\n  return class ExampleUser extends UserClass {\n    constructor(id, name, roles) {\n      super(id, name);\n\n      this.roles = roles;\n      this.getRoles = this.getRoles.bind(this);\n    }\n\n    getRoles() {\n      return this.roles;\n    }\n  };\n}\n"
  },
  {
    "path": "example/exampleUser/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ExampleUserProvider from './ExampleUserProvider.js';\nconst AUTO_LOGIN_USER = 'mct-user';\nconst STATUS_ROLES = ['flight', 'driver'];\n\nexport default function ExampleUserPlugin(\n  { autoLoginUser, statusRoles } = {\n    autoLoginUser: AUTO_LOGIN_USER,\n    statusRoles: STATUS_ROLES\n  }\n) {\n  return function install(openmct) {\n    const userProvider = new ExampleUserProvider(openmct, {\n      statusRoles\n    });\n\n    if (autoLoginUser !== undefined) {\n      userProvider.autoLogin(autoLoginUser);\n    }\n\n    openmct.user.setProvider(userProvider);\n  };\n}\n"
  },
  {
    "path": "example/exampleUser/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../src/utils/testing.js';\nimport ExampleUserProvider from './ExampleUserProvider.js';\n\ndescribe('The Example User Plugin', () => {\n  let openmct;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('is not installed by default', () => {\n    expect(openmct.user.hasProvider()).toBeFalse();\n  });\n\n  it('can be installed', () => {\n    openmct.user.on('providerAdded', (provider) => {\n      expect(provider).toBeInstanceOf(ExampleUserProvider);\n    });\n    openmct.install(openmct.plugins.example.ExampleUser());\n  });\n});\n"
  },
  {
    "path": "example/faultManagement/exampleFaultSource.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { DEFAULT_SHELVE_DURATIONS } from '../../src/api/faultmanagement/FaultManagementAPI.js';\nimport { acknowledgeFault, randomFaults, shelveFault } from './utils.js';\n\nexport default function (staticFaults = false) {\n  return function install(openmct) {\n    openmct.install(openmct.plugins.FaultManagement());\n\n    const faultsData = randomFaults(staticFaults);\n\n    openmct.faults.addProvider({\n      request(domainObject, options) {\n        return Promise.resolve(faultsData);\n      },\n      subscribe(domainObject, callback) {\n        callback({ type: 'global-alarm-status' });\n\n        return () => {};\n      },\n      supportsRequest(domainObject) {\n        return domainObject.type === 'faultManagement';\n      },\n      supportsSubscribe(domainObject) {\n        return domainObject.type === 'faultManagement';\n      },\n      acknowledgeFault(fault, { comment = '' }) {\n        acknowledgeFault(fault);\n\n        return Promise.resolve({\n          success: true\n        });\n      },\n      shelveFault(fault, duration) {\n        shelveFault(fault, duration);\n\n        return Promise.resolve({\n          success: true\n        });\n      },\n      getShelveDurations() {\n        return DEFAULT_SHELVE_DURATIONS;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "example/faultManagement/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../src/utils/testing.js';\n\ndescribe('The Example Fault Source Plugin', () => {\n  let openmct;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('is not installed by default', () => {\n    expect(openmct.faults.provider).toBeUndefined();\n  });\n\n  it('can be installed', () => {\n    openmct.install(openmct.plugins.example.ExampleFaultSource());\n    expect(openmct.faults.provider).not.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "example/faultManagement/utils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];\nconst MOONWALK_TIMESTAMP = 14159040000;\nconst NAMESPACE = '/Example/fault-';\nconst getRandom = {\n  severity: () => SEVERITIES[Math.floor(Math.random() * 3)],\n  value: () => Math.random() + Math.floor(Math.random() * 21) - 10,\n  fault: (num, staticFaults) => {\n    let val = getRandom.value();\n    let severity = getRandom.severity();\n    let time = Date.now() - num;\n\n    if (staticFaults) {\n      let severityIndex = num > 3 ? num % 3 : num;\n\n      val = num;\n      severity = SEVERITIES[severityIndex - 1];\n      // Subtract `num` from the timestamp so that the faults are in order\n      time = MOONWALK_TIMESTAMP - num; // Mon, 21 Jul 1969 02:56:00 GMT 🌔👨‍🚀👨‍🚀👨‍🚀\n    }\n\n    return {\n      type: num,\n      fault: {\n        acknowledged: false,\n        currentValueInfo: {\n          value: val,\n          rangeCondition: severity,\n          monitoringResult: severity\n        },\n        id: `id-${num}`,\n        name: `Example Fault ${num}`,\n        namespace: NAMESPACE + num,\n        seqNum: 0,\n        severity: severity,\n        shelved: false,\n        shortDescription: '',\n        triggerTime: time,\n        triggerValueInfo: {\n          value: val,\n          rangeCondition: severity,\n          monitoringResult: severity\n        }\n      }\n    };\n  }\n};\n\nexport function shelveFault(fault, opts = { shelved: true, comment: '', shelveDuration: 90000 }) {\n  fault.shelved = true;\n\n  setTimeout(() => {\n    fault.shelved = false;\n  }, opts.shelveDuration);\n}\n\nexport function acknowledgeFault(fault) {\n  fault.acknowledged = true;\n}\n\nexport function randomFaults(staticFaults, count = 5) {\n  let faults = [];\n\n  for (let i = 1; i <= count; i++) {\n    faults.push(getRandom.fault(i, staticFaults));\n  }\n\n  return faults;\n}\n"
  },
  {
    "path": "example/generator/GeneratorMetadataProvider.js",
    "content": "const METADATA_BY_TYPE = {\n  generator: {\n    values: [\n      {\n        key: 'name',\n        name: 'Name',\n        format: 'string'\n      },\n      {\n        key: 'utc',\n        name: 'Time',\n        format: 'utc',\n        hints: {\n          domain: 1\n        }\n      },\n      {\n        key: 'yesterday',\n        name: 'Yesterday',\n        format: 'utc',\n        hints: {\n          domain: 2\n        }\n      },\n      {\n        key: 'wavelengths',\n        name: 'Wavelength',\n        unit: 'nm',\n        format: 'string[]',\n        hints: {\n          range: 4\n        }\n      },\n      // Need to enable \"LocalTimeSystem\" plugin to make use of this\n      // {\n      //     key: \"local\",\n      //     name: \"Time\",\n      //     format: \"local-format\",\n      //     source: \"utc\",\n      //     hints: {\n      //         domain: 3\n      //     }\n      // },\n      {\n        key: 'sin',\n        name: 'Sine',\n        unit: 'Hz',\n        formatString: '%0.2f',\n        hints: {\n          range: 1\n        }\n      },\n      {\n        key: 'cos',\n        name: 'Cosine',\n        unit: 'deg',\n        formatString: '%0.2f',\n        hints: {\n          range: 2\n        }\n      },\n      {\n        key: 'intensities',\n        name: 'Intensities',\n        format: 'number[]',\n        hints: {\n          range: 3\n        }\n      }\n    ]\n  },\n  'example.state-generator': {\n    values: [\n      {\n        key: 'name',\n        name: 'Name',\n        format: 'string'\n      },\n      {\n        key: 'utc',\n        name: 'Time',\n        format: 'utc',\n        hints: {\n          domain: 1\n        }\n      },\n      {\n        key: 'local',\n        name: 'Time',\n        format: 'utc',\n        source: 'utc',\n        hints: {\n          domain: 2\n        }\n      },\n      {\n        key: 'state',\n        source: 'value',\n        name: 'State',\n        format: 'enum',\n        enumerations: [\n          {\n            value: 0,\n            string: 'OFF'\n          },\n          {\n            value: 1,\n            string: 'ON'\n          },\n          {\n            value: 99,\n            string: 'OUT OF ORDER'\n          }\n        ],\n        filters: [\n          {\n            singleSelectionThreshold: true,\n            comparator: 'equals',\n            possibleValues: [\n              { label: 'OFF', value: 0 },\n              { label: 'ON', value: 1 }\n            ]\n          }\n        ],\n        hints: {\n          range: 1\n        }\n      },\n      {\n        key: 'value',\n        name: 'Value',\n        hints: {\n          range: 2\n        }\n      }\n    ]\n  }\n};\n\nexport default function GeneratorMetadataProvider() {}\n\nGeneratorMetadataProvider.prototype.supportsMetadata = function (domainObject) {\n  return Object.prototype.hasOwnProperty.call(METADATA_BY_TYPE, domainObject.type);\n};\n\nGeneratorMetadataProvider.prototype.getMetadata = function (domainObject) {\n  return Object.assign({}, domainObject.telemetry, METADATA_BY_TYPE[domainObject.type]);\n};\n"
  },
  {
    "path": "example/generator/GeneratorProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport WorkerInterface from './WorkerInterface.js';\n\nconst REQUEST_DEFAULTS = {\n  amplitude: 1,\n  period: 10,\n  offset: 0,\n  dataRateInHz: 1,\n  randomness: 0,\n  phase: 0,\n  loadDelay: 0,\n  infinityValues: false,\n  exceedFloat32: false\n};\n\nexport default function GeneratorProvider(openmct, StalenessProvider) {\n  this.openmct = openmct;\n  this.workerInterface = new WorkerInterface(openmct, StalenessProvider);\n}\n\nGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {\n  return domainObject.type === 'generator';\n};\n\nGeneratorProvider.prototype.supportsRequest = GeneratorProvider.prototype.supportsSubscribe =\n  GeneratorProvider.prototype.canProvideTelemetry;\n\nGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) {\n  var props = [\n    'amplitude',\n    'period',\n    'offset',\n    'dataRateInHz',\n    'randomness',\n    'phase',\n    'loadDelay',\n    'infinityValues',\n    'exceedFloat32'\n  ];\n\n  request = request || {};\n\n  var workerRequest = {};\n\n  props.forEach(function (prop) {\n    if (\n      domainObject.telemetry &&\n      Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)\n    ) {\n      workerRequest[prop] = domainObject.telemetry[prop];\n    }\n\n    if (request && Object.prototype.hasOwnProperty.call(request, prop)) {\n      workerRequest[prop] = request[prop];\n    }\n\n    if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {\n      workerRequest[prop] = REQUEST_DEFAULTS[prop];\n    }\n\n    workerRequest[prop] = Number(workerRequest[prop]);\n  });\n\n  workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);\n  workerRequest.name = domainObject.name;\n\n  return workerRequest;\n};\n\nGeneratorProvider.prototype.request = function (domainObject, request) {\n  var workerRequest = this.makeWorkerRequest(domainObject, request);\n  workerRequest.start = request.start;\n  workerRequest.end = request.end;\n  workerRequest.size = request.size;\n  workerRequest.strategy = request.strategy;\n\n  return this.workerInterface.request(workerRequest);\n};\n\nGeneratorProvider.prototype.subscribe = function (domainObject, callback) {\n  var workerRequest = this.makeWorkerRequest(domainObject, {});\n\n  return this.workerInterface.subscribe(workerRequest, callback);\n};\n"
  },
  {
    "path": "example/generator/SinewaveLimitProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nvar PURPLE = {\n    sin: 2.2,\n    cos: 2.2\n  },\n  RED = {\n    sin: 0.9,\n    cos: 0.9\n  },\n  ORANGE = {\n    sin: 0.7,\n    cos: 0.7\n  },\n  YELLOW = {\n    sin: 0.5,\n    cos: 0.5\n  },\n  CYAN = {\n    sin: 0.45,\n    cos: 0.45\n  },\n  LIMITS = {\n    rh: {\n      cssClass: 'is-limit--upr is-limit--red',\n      low: RED,\n      high: Number.POSITIVE_INFINITY,\n      name: 'Red High'\n    },\n    rl: {\n      cssClass: 'is-limit--lwr is-limit--red',\n      high: -RED,\n      low: Number.NEGATIVE_INFINITY,\n      name: 'Red Low'\n    },\n    yh: {\n      cssClass: 'is-limit--upr is-limit--yellow',\n      low: YELLOW,\n      high: RED,\n      name: 'Yellow High'\n    },\n    yl: {\n      cssClass: 'is-limit--lwr is-limit--yellow',\n      low: -RED,\n      high: -YELLOW,\n      name: 'Yellow Low'\n    }\n  };\n\nexport default function SinewaveLimitProvider() {}\n\nSinewaveLimitProvider.prototype.supportsLimits = function (domainObject) {\n  return domainObject.type === 'generator';\n};\n\nSinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) {\n  return {\n    evaluate: function (datum, valueMetadata) {\n      var range = valueMetadata && valueMetadata.key;\n\n      if (datum[range] > RED[range]) {\n        return LIMITS.rh;\n      }\n\n      if (datum[range] < -RED[range]) {\n        return LIMITS.rl;\n      }\n\n      if (datum[range] > YELLOW[range]) {\n        return LIMITS.yh;\n      }\n\n      if (datum[range] < -YELLOW[range]) {\n        return LIMITS.yl;\n      }\n    }\n  };\n};\n\nSinewaveLimitProvider.prototype.getLimits = function (domainObject) {\n  return {\n    limits: function () {\n      return Promise.resolve({\n        WATCH: {\n          low: {\n            color: 'cyan',\n            sin: -CYAN.sin,\n            cos: -CYAN.cos\n          },\n          high: {\n            color: 'cyan',\n            ...CYAN\n          }\n        },\n        WARNING: {\n          low: {\n            color: 'yellow',\n            sin: -YELLOW.sin,\n            cos: -YELLOW.cos\n          },\n          high: {\n            color: 'yellow',\n            ...YELLOW\n          }\n        },\n        DISTRESS: {\n          low: {\n            color: 'orange',\n            sin: -ORANGE.sin,\n            cos: -ORANGE.cos\n          },\n          high: {\n            color: 'orange',\n            ...ORANGE\n          }\n        },\n        CRITICAL: {\n          low: {\n            color: 'red',\n            sin: -RED.sin,\n            cos: -RED.cos\n          },\n          high: {\n            color: 'red',\n            ...RED\n          }\n        },\n        SEVERE: {\n          low: {\n            color: 'purple',\n            sin: -PURPLE.sin,\n            cos: -PURPLE.cos\n          },\n          high: {\n            color: 'purple',\n            ...PURPLE\n          }\n        }\n      });\n    }\n  };\n};\n"
  },
  {
    "path": "example/generator/SinewaveStalenessProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nexport default class SinewaveLimitProvider extends EventEmitter {\n  #openmct;\n  #observingStaleness;\n  #watchingTheClock;\n\n  constructor(openmct) {\n    super();\n\n    this.#openmct = openmct;\n    this.#observingStaleness = {};\n    this.#watchingTheClock = false;\n  }\n\n  supportsStaleness(domainObject) {\n    return domainObject.type === 'generator';\n  }\n\n  isStale(domainObject, options) {\n    if (!this.#providingStaleness(domainObject)) {\n      return;\n    }\n\n    const id = this.#getObjectKeyString(domainObject);\n\n    if (!this.#observerExists(id)) {\n      this.#createObserver(id);\n    }\n\n    return Promise.resolve({\n      isStale: this.#observingStaleness[id].isStale,\n      utc: Date.now()\n    });\n  }\n\n  subscribeToStaleness(domainObject, callback) {\n    const id = this.#getObjectKeyString(domainObject);\n\n    this.#realTimeCheck();\n    this.#handleClockUpdate();\n\n    if (this.#observerExists(id)) {\n      this.#addCallbackToObserver(id, callback);\n    } else {\n      this.#createObserver(id, callback);\n    }\n\n    const intervalId = setInterval(() => {\n      if (this.#providingStaleness(domainObject)) {\n        this.#updateStaleness(id, !this.#observingStaleness[id].isStale);\n      }\n    }, 10000);\n\n    return () => {\n      clearInterval(intervalId);\n      this.#updateStaleness(id, false);\n      this.#handleClockUpdate();\n      this.#destroyObserver(id);\n    };\n  }\n\n  #handleClockUpdate() {\n    let observers = Object.values(this.#observingStaleness).length > 0;\n\n    if (observers && !this.#watchingTheClock) {\n      this.#watchingTheClock = true;\n      this.#openmct.time.on('modeChanged', this.#realTimeCheck, this);\n    } else if (!observers && this.#watchingTheClock) {\n      this.#watchingTheClock = false;\n      this.#openmct.time.off('modeChanged', this.#realTimeCheck, this);\n    }\n  }\n\n  #realTimeCheck() {\n    if (!this.#openmct.time.isRealTime()) {\n      Object.keys(this.#observingStaleness).forEach((id) => {\n        this.#updateStaleness(id, false);\n      });\n    }\n  }\n\n  #updateStaleness(id, isStale) {\n    this.#observingStaleness[id].isStale = isStale;\n    this.#observingStaleness[id].utc = Date.now();\n    this.#observingStaleness[id].callback({\n      isStale: this.#observingStaleness[id].isStale,\n      utc: this.#observingStaleness[id].utc\n    });\n    this.emit('stalenessEvent', {\n      id,\n      isStale: this.#observingStaleness[id].isStale\n    });\n  }\n\n  #createObserver(id, callback) {\n    this.#observingStaleness[id] = {\n      isStale: false,\n      utc: Date.now()\n    };\n\n    if (typeof callback === 'function') {\n      this.#addCallbackToObserver(id, callback);\n    }\n  }\n\n  #destroyObserver(id) {\n    if (this.#observingStaleness[id]) {\n      delete this.#observingStaleness[id];\n    }\n  }\n\n  #providingStaleness(domainObject) {\n    return domainObject.telemetry?.staleness === true && this.#openmct.time.isRealTime();\n  }\n\n  #getObjectKeyString(object) {\n    return this.#openmct.objects.makeKeyString(object.identifier);\n  }\n\n  #addCallbackToObserver(id, callback) {\n    this.#observingStaleness[id].callback = callback;\n  }\n\n  #observerExists(id) {\n    return this.#observingStaleness?.[id];\n  }\n}\n"
  },
  {
    "path": "example/generator/StateGeneratorProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class StateGeneratorProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  supportsRequest(domainObject, options) {\n    return domainObject.type === 'example.state-generator';\n  }\n\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'example.state-generator';\n  }\n\n  subscribe(domainObject, callback, options) {\n    const duration = domainObject.telemetry.duration * 1000;\n    let tick = 0;\n    const interval = setInterval(() => {\n      tick += 1;\n      let now = this.openmct.time.now() || Date.now();\n      let flip = false;\n      if (domainObject.telemetry.outOfOrder && tick % 3 === 0) {\n        // 2 steps forward, 1 step back by duration * 2 to simulate out of order data\n        now -= duration * 1.5;\n        flip = true;\n      }\n      const datum = this.#pointForTimestamp(now, duration, domainObject.name, flip);\n\n      if (!this.#shouldBeFiltered(datum, options)) {\n        datum.value = String(datum.value);\n        callback(datum);\n      }\n    }, duration);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }\n\n  request(domainObject, options) {\n    let start = options.start;\n    const now = this.openmct.time.now() || Date.now();\n    const end = Math.min(now, options.end); // no future values\n    const duration = domainObject.telemetry.duration * 1000;\n    if (options.strategy === 'latest' || options.size === 1) {\n      start = end;\n    }\n\n    const data = [];\n    while (start <= end && data.length < 5000) {\n      const point = this.#pointForTimestamp(start, duration, domainObject.name);\n\n      if (!this.#shouldBeFiltered(point, options)) {\n        data.push(point);\n      }\n      start += duration;\n    }\n\n    return Promise.resolve(data);\n  }\n\n  #pointForTimestamp(timestamp, duration, name, flip = false) {\n    const key = this.openmct.time.getTimeSystem()?.key || 'utc';\n    const point = {\n      name: name,\n      value: Math.floor(timestamp / duration) % 2\n    };\n    if (flip) {\n      point.value = 99;\n    }\n    point[key] = Math.floor(timestamp / duration) * duration;\n    return point;\n  }\n\n  #shouldBeFiltered(point, options) {\n    const valueToFilter = options?.filters?.state?.equals?.[0];\n\n    if (!valueToFilter) {\n      return false;\n    }\n\n    const { value } = point;\n\n    return value !== Number(valueToFilter);\n  }\n}\n"
  },
  {
    "path": "example/generator/WorkerInterface.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { v4 as uuid } from 'uuid';\n\nexport default function WorkerInterface(openmct, StalenessProvider) {\n  // eslint-disable-next-line no-undef\n  const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;\n  this.StalenessProvider = StalenessProvider;\n  this.worker = new Worker(workerUrl);\n  this.worker.onmessage = this.onMessage.bind(this);\n  this.callbacks = {};\n  this.staleTelemetryIds = {};\n\n  this.watchStaleness();\n}\n\nWorkerInterface.prototype.watchStaleness = function () {\n  this.StalenessProvider.on('stalenessEvent', ({ id, isStale }) => {\n    this.staleTelemetryIds[id] = isStale;\n  });\n};\n\nWorkerInterface.prototype.onMessage = function (message) {\n  message = message.data;\n  var callback = this.callbacks[message.id];\n  if (callback) {\n    callback(message);\n  }\n};\n\nWorkerInterface.prototype.dispatch = function (request, data, callback) {\n  var message = {\n    request: request,\n    data: data,\n    id: uuid()\n  };\n\n  if (callback) {\n    this.callbacks[message.id] = callback;\n  }\n\n  this.worker.postMessage(message);\n\n  return message.id;\n};\n\nWorkerInterface.prototype.request = function (request) {\n  var deferred = {};\n  var promise = new Promise(function (resolve, reject) {\n    deferred.resolve = resolve;\n    deferred.reject = reject;\n  });\n  var messageId;\n\n  let self = this;\n  function callback(message) {\n    if (message.error) {\n      deferred.reject(message.error);\n    } else {\n      deferred.resolve(message.data);\n    }\n\n    delete self.callbacks[messageId];\n  }\n\n  messageId = this.dispatch('request', request, callback.bind(this));\n\n  return promise;\n};\n\nWorkerInterface.prototype.subscribe = function (request, cb) {\n  const { id, loadDelay } = request;\n  const messageId = this.dispatch('subscribe', request, (message) => {\n    if (!this.staleTelemetryIds[id]) {\n      setTimeout(() => cb(message.data), Math.max(loadDelay, 0));\n    }\n  });\n\n  return function () {\n    this.dispatch('unsubscribe', {\n      id: messageId\n    });\n    delete this.callbacks[messageId];\n  }.bind(this);\n};\n"
  },
  {
    "path": "example/generator/generatorWorker.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n(function () {\n  var FIFTEEN_MINUTES = 15 * 60 * 1000;\n\n  var handlers = {\n    subscribe: onSubscribe,\n    unsubscribe: onUnsubscribe,\n    request: onRequest\n  };\n\n  var subscriptions = {};\n\n  function workSubscriptions(timestamp) {\n    var now = Date.now();\n    var nextWork = Math.min.apply(\n      Math,\n      Object.values(subscriptions).map(function (subscription) {\n        return subscription(now);\n      })\n    );\n    var wait = nextWork - now;\n    if (wait < 0) {\n      wait = 0;\n    }\n\n    if (Number.isFinite(wait)) {\n      setTimeout(workSubscriptions, wait);\n    }\n  }\n\n  function onSubscribe(message) {\n    var data = message.data;\n\n    // Keep\n    var start = Date.now();\n    var step = 1000 / data.dataRateInHz;\n    var nextStep = start - (start % step) + step;\n    let work;\n    if (data.spectra) {\n      work = function (now) {\n        while (nextStep < now) {\n          const messageCopy = Object.create(message);\n          message.data.start = nextStep - 60 * 1000;\n          message.data.end = nextStep;\n          onRequest(messageCopy);\n          nextStep += step;\n        }\n\n        return nextStep;\n      };\n    } else {\n      work = function (now) {\n        while (nextStep < now) {\n          self.postMessage({\n            id: message.id,\n            data: {\n              name: data.name,\n              utc: nextStep,\n              yesterday: nextStep - 60 * 60 * 24 * 1000,\n              sin: sin(\n                nextStep,\n                data.period,\n                data.amplitude,\n                data.offset,\n                data.phase,\n                data.randomness,\n                data.infinityValues,\n                data.exceedFloat32\n              ),\n              wavelengths: wavelengths(),\n              intensities: intensities(),\n              cos: cos(\n                nextStep,\n                data.period,\n                data.amplitude,\n                data.offset,\n                data.phase,\n                data.randomness,\n                data.infinityValues,\n                data.exceedFloat32\n              )\n            }\n          });\n          nextStep += step;\n        }\n\n        return nextStep;\n      };\n    }\n\n    subscriptions[message.id] = work;\n    workSubscriptions();\n  }\n\n  function onUnsubscribe(message) {\n    delete subscriptions[message.data.id];\n  }\n\n  function onRequest(message) {\n    var request = message.data;\n    if (request.end === undefined) {\n      request.end = Date.now();\n    }\n\n    if (request.start === undefined) {\n      request.start = request.end - FIFTEEN_MINUTES;\n    }\n\n    var now = Date.now();\n    var start = request.start;\n    var end = request.end > now ? now : request.end;\n    var period = request.period;\n    var dataRateInHz = request.dataRateInHz;\n    var loadDelay = Math.max(request.loadDelay, 0);\n    var size = request.size;\n    var duration = end - start;\n    var step = 1000 / dataRateInHz;\n    var maxPoints = Math.floor(duration / step);\n    var nextStep = start - (start % step) + step;\n\n    var data = [];\n\n    if (request.strategy === 'minmax' && size) {\n      // Calculate the number of cycles to include based on size (2 points per cycle)\n      var totalCycles = Math.min(Math.floor(size / 2), Math.floor(duration / period));\n\n      for (let cycle = 0; cycle < totalCycles; cycle++) {\n        // Distribute cycles evenly across the time range\n        let cycleStart = start + (duration / totalCycles) * cycle;\n        let minPointTime = cycleStart; // Assuming min at the start of the cycle\n        let maxPointTime = cycleStart + period / 2; // Assuming max at the halfway of the cycle\n\n        data.push(createDataPoint(minPointTime, request), createDataPoint(maxPointTime, request));\n      }\n    } else {\n      for (let i = 0; i < maxPoints && nextStep < end; i++, nextStep += step) {\n        data.push(createDataPoint(nextStep, request));\n      }\n    }\n\n    if (request.strategy !== 'minmax' && size) {\n      data = data.slice(-size);\n    }\n\n    if (loadDelay === 0) {\n      postOnRequest(message, request, data);\n    } else {\n      setTimeout(() => postOnRequest(message, request, data), loadDelay);\n    }\n  }\n\n  function createDataPoint(time, request) {\n    return {\n      utc: time,\n      yesterday: time - 60 * 60 * 24 * 1000,\n      sin: sin(\n        time,\n        request.period,\n        request.amplitude,\n        request.offset,\n        request.phase,\n        request.randomness,\n        request.infinityValues,\n        request.exceedFloat32\n      ),\n      wavelengths: wavelengths(),\n      intensities: intensities(),\n      cos: cos(\n        time,\n        request.period,\n        request.amplitude,\n        request.offset,\n        request.phase,\n        request.randomness,\n        request.infinityValues,\n        request.exceedFloat32\n      )\n    };\n  }\n\n  function postOnRequest(message, request, data) {\n    self.postMessage({\n      id: message.id,\n      data: request.spectra\n        ? {\n            wavelength: data.map((item) => {\n              return item.wavelength;\n            }),\n            cos: data.map((item) => {\n              return item.cos;\n            })\n          }\n        : data\n    });\n  }\n\n  function cos(\n    timestamp,\n    period,\n    amplitude,\n    offset,\n    phase,\n    randomness,\n    infinityValues,\n    exceedFloat32\n  ) {\n    if (infinityValues && exceedFloat32) {\n      if (Math.random() > 0.5) {\n        return Number.POSITIVE_INFINITY;\n      } else if (Math.random() < 0.01) {\n        return getRandomFloat32OverflowValue();\n      }\n    } else if (infinityValues && Math.random() > 0.5) {\n      return Number.POSITIVE_INFINITY;\n    } else if (exceedFloat32 && Math.random() < 0.01) {\n      return getRandomFloat32OverflowValue();\n    }\n\n    return (\n      amplitude * Math.cos(phase + (timestamp / period / 1000) * Math.PI * 2) +\n      amplitude * Math.random() * randomness +\n      offset\n    );\n  }\n\n  function sin(\n    timestamp,\n    period,\n    amplitude,\n    offset,\n    phase,\n    randomness,\n    infinityValues,\n    exceedFloat32\n  ) {\n    if (infinityValues && exceedFloat32) {\n      if (Math.random() > 0.5) {\n        return Number.POSITIVE_INFINITY;\n      } else if (Math.random() < 0.01) {\n        return getRandomFloat32OverflowValue();\n      }\n    } else if (infinityValues && Math.random() > 0.5) {\n      return Number.POSITIVE_INFINITY;\n    } else if (exceedFloat32 && Math.random() < 0.01) {\n      return getRandomFloat32OverflowValue();\n    }\n\n    return (\n      amplitude * Math.sin(phase + (timestamp / period / 1000) * Math.PI * 2) +\n      amplitude * Math.random() * randomness +\n      offset\n    );\n  }\n\n  // Values exceeding float32 range (Positive: 3.4+38, Negative: -3.4+38)\n  function getRandomFloat32OverflowValue() {\n    const sign = Math.random() > 0.5 ? 1 : -1;\n\n    return sign * 3.4e39;\n  }\n\n  function wavelengths() {\n    let values = [];\n    while (values.length < 5) {\n      const randomValue = Math.random() * 100;\n      if (!values.includes(randomValue)) {\n        values.push(String(randomValue));\n      }\n    }\n\n    return values;\n  }\n\n  function intensities() {\n    let values = [];\n    while (values.length < 5) {\n      const randomValue = Math.random() * 10;\n      if (!values.includes(randomValue)) {\n        values.push(String(randomValue));\n      }\n    }\n\n    return values;\n  }\n\n  function sendError(error, message) {\n    self.postMessage({\n      error: error.name + ': ' + error.message,\n      message: message,\n      id: message.id\n    });\n  }\n\n  self.onmessage = function handleMessage(event) {\n    var message = event.data;\n    var handler = handlers[message.request];\n\n    if (!handler) {\n      sendError(new Error('unknown message type'), message);\n    } else {\n      try {\n        handler(message);\n      } catch (e) {\n        sendError(e, message);\n      }\n    }\n  };\n})();\n"
  },
  {
    "path": "example/generator/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport GeneratorMetadataProvider from './GeneratorMetadataProvider.js';\nimport GeneratorProvider from './GeneratorProvider.js';\nimport SinewaveLimitProvider from './SinewaveLimitProvider.js';\nimport SinewaveStalenessProvider from './SinewaveStalenessProvider.js';\nimport StateGeneratorProvider from './StateGeneratorProvider.js';\n\nexport default function (openmct) {\n  openmct.types.addType('example.state-generator', {\n    name: 'State Generator',\n    description:\n      'For development use. Generates example enumerated telemetry by cycling through a given set of states.',\n    cssClass: 'icon-generator-telemetry',\n    creatable: true,\n    form: [\n      {\n        name: 'State Duration (seconds)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'duration',\n        required: true,\n        property: ['telemetry', 'duration']\n      },\n      {\n        name: 'Out of order data',\n        control: 'toggleSwitch',\n        cssClass: 'l-input',\n        key: 'outOfOrder',\n        required: true,\n        property: ['telemetry', 'outOfOrder']\n      }\n    ],\n    initialize: function (object) {\n      object.telemetry = {\n        duration: 5,\n        outOfOrder: false\n      };\n    }\n  });\n\n  openmct.telemetry.addProvider(new StateGeneratorProvider(openmct));\n\n  openmct.types.addType('generator', {\n    name: 'Sine Wave Generator',\n    description:\n      'For development use. Generates example streaming telemetry data using a simple sine wave algorithm.',\n    cssClass: 'icon-generator-telemetry',\n    creatable: true,\n    form: [\n      {\n        name: 'Period',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'period',\n        required: true,\n        property: ['telemetry', 'period']\n      },\n      {\n        name: 'Amplitude',\n        control: 'numberfield',\n        cssClass: 'l-numeric',\n        key: 'amplitude',\n        required: true,\n        property: ['telemetry', 'amplitude']\n      },\n      {\n        name: 'Offset',\n        control: 'numberfield',\n        cssClass: 'l-numeric',\n        key: 'offset',\n        required: true,\n        property: ['telemetry', 'offset']\n      },\n      {\n        name: 'Data Rate (hz)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'dataRateInHz',\n        required: true,\n        property: ['telemetry', 'dataRateInHz']\n      },\n      {\n        name: 'Phase (radians)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'phase',\n        required: true,\n        property: ['telemetry', 'phase']\n      },\n      {\n        name: 'Randomness',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'randomness',\n        required: true,\n        property: ['telemetry', 'randomness']\n      },\n      {\n        name: 'Loading Delay (ms)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        key: 'loadDelay',\n        required: true,\n        property: ['telemetry', 'loadDelay']\n      },\n      {\n        name: 'Include Infinity Values',\n        control: 'toggleSwitch',\n        cssClass: 'l-input',\n        key: 'infinityValues',\n        property: ['telemetry', 'infinityValues']\n      },\n      {\n        name: 'Exceed Float32 Limits',\n        control: 'toggleSwitch',\n        cssClass: 'l-input',\n        key: 'exceedFloat32',\n        property: ['telemetry', 'exceedFloat32']\n      },\n      {\n        name: 'Provide Staleness Updates',\n        control: 'toggleSwitch',\n        cssClass: 'l-input',\n        key: 'staleness',\n        property: ['telemetry', 'staleness']\n      }\n    ],\n    initialize: function (object) {\n      object.telemetry = {\n        period: 10,\n        amplitude: 1,\n        offset: 0,\n        dataRateInHz: 1,\n        phase: 0,\n        randomness: 0,\n        loadDelay: 0,\n        infinityValues: false,\n        exceedFloat32: false,\n        staleness: false\n      };\n    }\n  });\n  const stalenessProvider = new SinewaveStalenessProvider(openmct);\n\n  openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider));\n  openmct.telemetry.addProvider(new GeneratorMetadataProvider());\n  openmct.telemetry.addProvider(new SinewaveLimitProvider());\n  openmct.telemetry.addProvider(stalenessProvider);\n}\n"
  },
  {
    "path": "example/imagery/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { seededRandom } from 'utils/random.js';\n\nconst DEFAULT_IMAGE_SAMPLES = [\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18731.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18732.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18733.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18734.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18735.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18736.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18737.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18738.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18739.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18740.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18741.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18742.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18743.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18744.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18745.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18746.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18747.jpg',\n  'https://lpi.usra.edu/resources/apollo/images/browse/AS16/117/18748.jpg'\n];\nconst DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000;\nconst MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000;\n\nlet openmctInstance;\n\nexport default function () {\n  return function install(openmct) {\n    openmctInstance = openmct;\n    openmct.types.addType('example.imagery', {\n      key: 'example.imagery',\n      name: 'Example Imagery',\n      cssClass: 'icon-image',\n      description:\n        'For development use. Creates example imagery data that mimics a live imagery stream.',\n      creatable: true,\n      initialize: (object) => {\n        object.configuration = {\n          imageLocation: '',\n          imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS,\n          imageSamples: [],\n          layers: []\n        };\n\n        object.telemetry = {\n          values: [\n            {\n              name: 'Name',\n              key: 'name'\n            },\n            {\n              name: 'Time',\n              key: 'utc',\n              format: 'utc',\n              hints: {\n                domain: 2\n              }\n            },\n            {\n              name: 'Local Time',\n              key: 'local',\n              format: 'local-format',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              name: 'Image',\n              key: 'url',\n              format: 'image',\n              hints: {\n                image: 1\n              },\n              layers: [\n                {\n                  source: 'dist/imagery/example-imagery-layer-16x9.png',\n                  name: '16:9'\n                },\n                {\n                  source: 'dist/imagery/example-imagery-layer-safe.png',\n                  name: 'Safe'\n                },\n                {\n                  source: 'dist/imagery/example-imagery-layer-scale.png',\n                  name: 'Scale'\n                }\n              ]\n            },\n            {\n              name: 'Image Thumbnail',\n              key: 'thumbnail-url',\n              format: 'thumbnail',\n              hints: {\n                thumbnail: 1\n              },\n              source: 'url'\n            },\n            {\n              name: 'Image Download Name',\n              key: 'imageDownloadName',\n              format: 'imageDownloadName',\n              hints: {\n                imageDownloadName: 1\n              }\n            }\n          ]\n        };\n      },\n      form: [\n        {\n          key: 'imageLocation',\n          name: 'Images url list (comma separated)',\n          control: 'textarea',\n          cssClass: 'l-inline',\n          property: ['configuration', 'imageLocation']\n        },\n        {\n          key: 'imageLoadDelayInMilliSeconds',\n          name: 'Image load delay (milliseconds)',\n          control: 'numberfield',\n          required: true,\n          cssClass: 'l-inline',\n          property: ['configuration', 'imageLoadDelayInMilliSeconds']\n        }\n      ]\n    });\n\n    const formatThumbnail = {\n      format: function (url) {\n        return `${url}?w=100&h=100`;\n      }\n    };\n\n    openmct.telemetry.addFormat({\n      key: 'thumbnail',\n      ...formatThumbnail\n    });\n    openmct.telemetry.addProvider(getRealtimeProvider(openmct));\n    openmct.telemetry.addProvider(getHistoricalProvider(openmct));\n    openmct.telemetry.addProvider(getLadProvider(openmct));\n  };\n}\n\nfunction getCompassValues(min, max, timestamp) {\n  return min + seededRandom(timestamp) * (max - min);\n}\n\nfunction getImageSamples(configuration) {\n  let imageSamples = DEFAULT_IMAGE_SAMPLES;\n\n  if (configuration.imageLocation && configuration.imageLocation.length) {\n    imageSamples = getImageUrlListFromConfig(configuration);\n  }\n\n  return imageSamples;\n}\n\nfunction getImageUrlListFromConfig(configuration) {\n  return configuration.imageLocation.split(',');\n}\n\nfunction getImageLoadDelay(domainObject) {\n  const imageLoadDelay = Math.trunc(\n    Number(domainObject.configuration.imageLoadDelayInMilliSeconds)\n  );\n  if (!imageLoadDelay) {\n    openmctInstance.objects.mutate(\n      domainObject,\n      'configuration.imageLoadDelayInMilliSeconds',\n      DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS\n    );\n\n    return DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS;\n  }\n\n  if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS) {\n    openmctInstance.objects.mutate(\n      domainObject,\n      'configuration.imageLoadDelayInMilliSeconds',\n      MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS\n    );\n\n    return MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS;\n  }\n\n  return imageLoadDelay;\n}\n\nfunction getRealtimeProvider(openmct) {\n  return {\n    supportsSubscribe: (domainObject) => domainObject.type === 'example.imagery',\n    subscribe: (domainObject, callback) => {\n      const delay = getImageLoadDelay(domainObject);\n      const interval = setInterval(() => {\n        const imageSamples = getImageSamples(domainObject.configuration);\n        const datum = pointForTimestamp(openmct.time.now(), domainObject.name, imageSamples, delay);\n        callback(datum);\n      }, delay);\n\n      return () => {\n        clearInterval(interval);\n      };\n    }\n  };\n}\n\nfunction getHistoricalProvider(openmct) {\n  return {\n    supportsRequest: (domainObject, options) => {\n      return domainObject.type === 'example.imagery' && options.strategy !== 'latest';\n    },\n    request: (domainObject, options) => {\n      const delay = getImageLoadDelay(domainObject);\n      let start = options.start;\n      const end = Math.min(options.end, openmct.time.now());\n      const data = [];\n      while (start <= end && data.length < delay) {\n        const imageSamples = getImageSamples(domainObject.configuration);\n        const generatedDataPoint = pointForTimestamp(start, domainObject.name, imageSamples, delay);\n        data.push(generatedDataPoint);\n        start += delay;\n      }\n\n      return Promise.resolve(data);\n    }\n  };\n}\n\nfunction getLadProvider(openmct) {\n  return {\n    supportsRequest: (domainObject, options) => {\n      return domainObject.type === 'example.imagery' && options.strategy === 'latest';\n    },\n    request: (domainObject, options) => {\n      const delay = getImageLoadDelay(domainObject);\n      const datum = pointForTimestamp(\n        openmct.time.now(),\n        domainObject.name,\n        getImageSamples(domainObject.configuration),\n        delay\n      );\n\n      return Promise.resolve([datum]);\n    }\n  };\n}\n\nfunction pointForTimestamp(timestamp, name, imageSamples, delay) {\n  const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length];\n  const urlItems = url.split('/');\n  const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;\n  const navCamTransformations = {\n    translateX: 0,\n    translateY: 18,\n    rotation: 0,\n    scale: 0.3,\n    cameraAngleOfView: 70\n  };\n\n  return {\n    name,\n    utc: Math.floor(timestamp / delay) * delay,\n    local: Math.floor(timestamp / delay) * delay,\n    url,\n    sunOrientation: getCompassValues(0, 360, timestamp),\n    cameraAzimuth: getCompassValues(0, 360, timestamp),\n    heading: getCompassValues(0, 360, timestamp),\n    transformations: navCamTransformations,\n    imageDownloadName\n  };\n}\n"
  },
  {
    "path": "index-test.cjs",
    "content": "const testsContext = require.context('.', true, /^\\.\\/(src|example)\\/.*Spec.js$/);\ntestsContext.keys().forEach(testsContext);\n"
  },
  {
    "path": "index.html",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <!-- Modified viewport meta tag to improve accessibility -->\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\" />\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n    <title>Open MCT</title>\n    <script src=\"dist/openmct.js\"></script>\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      href=\"dist/favicons/favicon-96x96.png\"\n      sizes=\"96x96\"\n      type=\"image/x-icon\"\n    />\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      href=\"dist/favicons/favicon-32x32.png\"\n      sizes=\"32x32\"\n      type=\"image/x-icon\"\n    />\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      href=\"dist/favicons/favicon-16x16.png\"\n      sizes=\"16x16\"\n      type=\"image/x-icon\"\n    />\n    <style>\n      @keyframes splash-spinner {\n        0% {\n          transform: translate(-50%, -50%) rotate(0deg);\n        }\n        100% {\n          transform: translate(-50%, -50%) rotate(360deg);\n        }\n      }\n\n      #splash-screen {\n        background-color: black;\n        position: absolute;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: 0;\n        z-index: 10000;\n      }\n\n      #splash-screen:before {\n        animation-name: splash-spinner;\n        animation-duration: 0.5s;\n        animation-iteration-count: infinite;\n        animation-timing-function: linear;\n        border-radius: 50%;\n        border-color: rgba(255, 255, 255, 0.25);\n        border-top-color: white;\n        border-style: solid;\n        border-width: 10px;\n        content: '';\n        display: block;\n        opacity: 0.25;\n        position: absolute;\n        left: 50%;\n        top: 50%;\n        height: 100px;\n        width: 100px;\n      }\n    </style>\n    <script defer>\n      const THIRTY_SECONDS = 30 * 1000;\n      const ONE_MINUTE = THIRTY_SECONDS * 2;\n      const FIVE_MINUTES = ONE_MINUTE * 5;\n      const FIFTEEN_MINUTES = FIVE_MINUTES * 3;\n      const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;\n      const ONE_HOUR = THIRTY_MINUTES * 2;\n      const TWO_HOURS = ONE_HOUR * 2;\n      const ONE_DAY = ONE_HOUR * 24;\n\n      openmct.install(openmct.plugins.LocalStorage());\n\n      openmct.install(openmct.plugins.example.Generator());\n      openmct.install(openmct.plugins.example.EventGeneratorPlugin());\n      openmct.install(openmct.plugins.example.ExampleImagery());\n      openmct.install(openmct.plugins.example.ExampleTags());\n\n      openmct.install(openmct.plugins.Espresso());\n      openmct.install(openmct.plugins.MyItems());\n      openmct.install(\n        openmct.plugins.PlanLayout({\n          creatable: true\n        })\n      );\n      const timeLinePlugin = openmct.plugins.Timeline();\n      openmct.install(timeLinePlugin);\n      openmct.install(openmct.plugins.Hyperlink());\n      openmct.install(openmct.plugins.UTCTimeSystem());\n      openmct.install(\n        openmct.plugins.AutoflowView({\n          type: 'telemetry.panel'\n        })\n      );\n      openmct.install(\n        openmct.plugins.DisplayLayout({\n          showAsView: ['summary-widget', 'example.imagery']\n        })\n      );\n      openmct.install(\n        openmct.plugins.Conductor({\n          menuOptions: [\n            {\n              name: 'Fixed',\n              timeSystem: 'utc',\n              bounds: {\n                start: Date.now() - THIRTY_MINUTES,\n                end: Date.now()\n              },\n              // commonly used bounds can be stored in history\n              // bounds (start and end) can accept either a milliseconds number\n              // or a callback function returning a milliseconds number\n              // a function is useful for invoking Date.now() at exact moment of preset selection\n              presets: [\n                {\n                  label: 'Last Day',\n                  bounds: {\n                    start: () => Date.now() - ONE_DAY,\n                    end: () => Date.now()\n                  }\n                },\n                {\n                  label: 'Last 2 hours',\n                  bounds: {\n                    start: () => Date.now() - TWO_HOURS,\n                    end: () => Date.now()\n                  }\n                },\n                {\n                  label: 'Last hour',\n                  bounds: {\n                    start: () => Date.now() - ONE_HOUR,\n                    end: () => Date.now()\n                  }\n                }\n              ],\n              // maximum recent bounds to retain in conductor history\n              records: 10\n              // maximum duration between start and end bounds\n              // for utc-based time systems this is in milliseconds\n              // limit: ONE_DAY\n            },\n            {\n              name: 'Realtime',\n              timeSystem: 'utc',\n              clock: 'local',\n              clockOffsets: {\n                start: -THIRTY_MINUTES,\n                end: THIRTY_SECONDS\n              },\n              presets: [\n                {\n                  label: '1 Hour',\n                  bounds: {\n                    start: -ONE_HOUR,\n                    end: THIRTY_SECONDS\n                  }\n                },\n                {\n                  label: '30 Minutes',\n                  bounds: {\n                    start: -THIRTY_MINUTES,\n                    end: THIRTY_SECONDS\n                  }\n                },\n                {\n                  label: '15 Minutes',\n                  bounds: {\n                    start: -FIFTEEN_MINUTES,\n                    end: THIRTY_SECONDS\n                  }\n                },\n                {\n                  label: '5 Minutes',\n                  bounds: {\n                    start: -FIVE_MINUTES,\n                    end: THIRTY_SECONDS\n                  }\n                },\n                {\n                  label: '1 Minute',\n                  bounds: {\n                    start: -ONE_MINUTE,\n                    end: THIRTY_SECONDS\n                  }\n                }\n              ]\n            }\n          ]\n        })\n      );\n      openmct.install(openmct.plugins.SummaryWidget());\n      openmct.install(openmct.plugins.Notebook());\n      openmct.install(openmct.plugins.LADTable());\n      openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));\n      openmct.install(openmct.plugins.ObjectMigration());\n      openmct.install(\n        openmct.plugins.ClearData(\n          ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],\n          { indicator: true }\n        )\n      );\n      openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));\n      openmct.install(openmct.plugins.Timer());\n      openmct.install(openmct.plugins.Timelist());\n      openmct.install(openmct.plugins.BarChart());\n      openmct.install(openmct.plugins.ScatterPlot());\n      openmct.install(openmct.plugins.EventTimestripPlugin(timeLinePlugin.extendedLinesBus));\n      document.addEventListener('DOMContentLoaded', function () {\n        openmct.start();\n      });\n    </script>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karma.conf.cjs",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// eslint-disable-next-line func-style\nconst loadWebpackConfig = async () => {\n  if (process.env.KARMA_DEBUG) {\n    return {\n      config: (await import('./.webpack/webpack.dev.mjs')).default,\n      browsers: ['ChromeDebugging'],\n      singleRun: false\n    };\n  } else {\n    return {\n      config: (await import('./.webpack/webpack.coverage.mjs')).default,\n      browsers: ['ChromeHeadless'],\n      singleRun: true\n    };\n  }\n};\n\nmodule.exports = async (config) => {\n  const { config: webpackConfig, browsers, singleRun } = await loadWebpackConfig();\n\n  // Adjust webpack config for Karma\n  delete webpackConfig.output;\n  delete webpackConfig.entry; // Karma doesn't support webpack entry\n\n  // Ensure source maps are enabled for better debugging\n  webpackConfig.devtool = 'inline-source-map';\n\n  config.set({\n    basePath: '',\n    frameworks: ['jasmine', 'webpack'],\n    files: [\n      'index-test.cjs',\n      // included means: should the files be included in the browser using <script> tag?\n      // We don't want them as a <script> because the shared worker source\n      // needs loaded remotely by the shared worker process.\n      {\n        pattern: 'dist/couchDBChangesFeed.js*',\n        included: false\n      },\n      {\n        pattern: 'dist/inMemorySearchWorker.js*',\n        included: false\n      },\n      {\n        pattern: 'dist/generatorWorker.js*',\n        included: false\n      }\n    ],\n    port: 9876,\n    reporters: ['spec', 'junit', 'coverage-istanbul'],\n    browsers,\n    client: {\n      jasmine: {\n        random: false,\n        timeoutInterval: 6000\n      }\n    },\n    customLaunchers: {\n      ChromeDebugging: {\n        base: 'Chrome',\n        flags: ['--remote-debugging-port=9222'],\n        debug: true\n      }\n    },\n    colors: true,\n    logLevel: config.LOG_INFO,\n    autoWatch: true,\n    junitReporter: {\n      outputDir: 'dist/reports/tests', //Useful for CircleCI\n      outputFile: 'test-results.xml', //Useful for CircleCI\n      useBrowserName: false //Disable since we only want chrome\n    },\n    coverageIstanbulReporter: {\n      fixWebpackSourcePaths: true,\n      skipFilesWithNoCoverage: true,\n      dir: 'coverage/unit', //Sets coverage file to be consumed by codecov.io\n      reports: ['lcovonly']\n    },\n    specReporter: {\n      maxLogLines: 5,\n      suppressErrorSummary: false,\n      suppressFailed: false,\n      suppressPassed: false,\n      suppressSkipped: true,\n      showSpecTiming: true,\n      failFast: false\n    },\n    preprocessors: {\n      'index-test.cjs': ['webpack', 'sourcemap']\n    },\n    webpack: webpackConfig,\n    webpackMiddleware: {\n      stats: 'detailed' // Changed to 'detailed' for more debugging info\n    },\n    concurrency: 1,\n    singleRun,\n    browserNoActivityTimeout: 400000\n  });\n};\n"
  },
  {
    "path": "openmct.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst matcher = /\\/openmct.js$/;\nif (document.currentScript) {\n  // @ts-ignore\n  let src = document.currentScript.src;\n  if (src && matcher.test(src)) {\n    // @ts-ignore\n    __webpack_public_path__ = src.replace(matcher, '') + '/';\n  }\n}\n\nimport { MCT } from './src/MCT.js';\n\nconst openmct = new MCT();\n\nexport default openmct;\n\n/**\n * @typedef {MCT} OpenMCT\n * @typedef {import('./src/api/objects/ObjectAPI.js').DomainObject} DomainObject\n * @typedef {import('./src/api/objects/ObjectAPI.js').Identifier} Identifier\n * @typedef {import('./src/api/objects/Transaction.js').default} Transaction\n * @typedef {import('./src/api/actions/ActionsAPI.js').Action} Action\n * @typedef {import('./src/api/actions/ActionCollection.js').default} ActionCollection\n * @typedef {import('./src/api/composition/CompositionCollection.js').default} CompositionCollection\n * @typedef {import('./src/api/composition/CompositionProvider.js').default} CompositionProvider\n * @typedef {import('./src/ui/registries/ViewRegistry.js').ViewProvider} ViewProvider\n * @typedef {import('./src/ui/registries/ViewRegistry.js').View} View\n *\n * @typedef {DomainObject[]} ObjectPath\n * @typedef {(...args: any[]) => (openmct: OpenMCT) => void} OpenMCTPlugin\n * An OpenMCT Plugin returns a function that receives an instance of\n * the OpenMCT API and uses it to install itself.\n */\n\n/**\n * @typedef {Object} BuildInfo\n * @property {string} version\n * @property {string} buildDate\n * @property {string} revision\n * @property {string} branch\n */\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"openmct\",\n  \"version\": \"4.1.0-next\",\n  \"description\": \"The Open MCT core platform\",\n  \"module\": \"dist/openmct.js\",\n  \"main\": \"dist/openmct.js\",\n  \"types\": \"dist/types/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/openmct.js\",\n      \"require\": \"./dist/openmct.js\"\n    }\n  },\n  \"workspaces\": [\n    \"e2e\"\n  ],\n  \"devDependencies\": {\n    \"@babel/eslint-parser\": \"7.23.3\",\n    \"@braintree/sanitize-url\": \"7.1.1\",\n    \"@types/d3-axis\": \"3.0.6\",\n    \"@types/d3-scale\": \"4.0.8\",\n    \"@types/d3-selection\": \"3.0.10\",\n    \"@types/d3-shape\": \"3.1.7\",\n    \"@types/eventemitter3\": \"1.2.0\",\n    \"@types/jasmine\": \"5.1.2\",\n    \"@types/lodash\": \"4.17.0\",\n    \"@vue/compiler-sfc\": \"3.4.3\",\n    \"babel-loader\": \"9.1.0\",\n    \"babel-plugin-istanbul\": \"7.0.0\",\n    \"comma-separated-values\": \"3.6.4\",\n    \"copy-webpack-plugin\": \"13.0.0\",\n    \"cspell\": \"7.3.8\",\n    \"css-loader\": \"6.10.0\",\n    \"d3-axis\": \"3.0.0\",\n    \"d3-scale\": \"4.0.2\",\n    \"d3-selection\": \"3.0.0\",\n    \"d3-shape\": \"3.2.0\",\n    \"eslint\": \"8.56.0\",\n    \"eslint-config-prettier\": \"9.1.0\",\n    \"eslint-plugin-compat\": \"4.2.0\",\n    \"eslint-plugin-no-unsanitized\": \"4.0.2\",\n    \"eslint-plugin-playwright\": \"1.5.2\",\n    \"eslint-plugin-prettier\": \"5.1.3\",\n    \"eslint-plugin-simple-import-sort\": \"10.0.0\",\n    \"eslint-plugin-unicorn\": \"49.0.0\",\n    \"eslint-plugin-vue\": \"9.22.0\",\n    \"eslint-plugin-you-dont-need-lodash-underscore\": \"6.13.0\",\n    \"eventemitter3\": \"5.0.1\",\n    \"file-saver\": \"2.0.5\",\n    \"flatbush\": \"4.2.0\",\n    \"git-rev-sync\": \"3.0.2\",\n    \"html2canvas\": \"1.4.1\",\n    \"imports-loader\": \"5.0.0\",\n    \"jasmine-core\": \"5.6.0\",\n    \"karma\": \"6.4.2\",\n    \"karma-chrome-launcher\": \"3.2.0\",\n    \"karma-cli\": \"2.0.0\",\n    \"karma-coverage\": \"2.2.0\",\n    \"karma-coverage-istanbul-reporter\": \"3.0.3\",\n    \"karma-jasmine\": \"5.1.0\",\n    \"karma-junit-reporter\": \"2.0.1\",\n    \"karma-sourcemap-loader\": \"0.4.0\",\n    \"karma-spec-reporter\": \"0.0.36\",\n    \"karma-webpack\": \"5.0.1\",\n    \"location-bar\": \"3.0.1\",\n    \"lodash\": \"4.17.21\",\n    \"marked\": \"15.0.7\",\n    \"mathjs\": \"13.1.1\",\n    \"mini-css-extract-plugin\": \"2.9.2\",\n    \"moment\": \"2.30.1\",\n    \"moment-duration-format\": \"2.3.2\",\n    \"moment-timezone\": \"0.5.41\",\n    \"nano\": \"10.1.4\",\n    \"npm-run-all2\": \"7.0.2\",\n    \"nyc\": \"17.1.0\",\n    \"painterro\": \"1.2.87\",\n    \"plotly.js-basic-dist-min\": \"2.29.1\",\n    \"plotly.js-gl2d-dist-min\": \"2.20.0\",\n    \"prettier\": \"3.2.5\",\n    \"prettier-eslint\": \"16.3.0\",\n    \"printj\": \"1.3.1\",\n    \"resolve-url-loader\": \"5.0.0\",\n    \"sanitize-html\": \"2.15.0\",\n    \"sass\": \"1.71.1\",\n    \"sass-loader\": \"14.1.1\",\n    \"style-loader\": \"4.0.0\",\n    \"terser-webpack-plugin\": \"5.3.9\",\n    \"tiny-emitter\": \"2.1.0\",\n    \"typescript\": \"5.3.3\",\n    \"uuid\": \"11.1.0\",\n    \"vue\": \"3.4.24\",\n    \"vue-eslint-parser\": \"9.4.2\",\n    \"vue-loader\": \"16.8.3\",\n    \"webpack\": \"5.98.0\",\n    \"webpack-cli\": \"5.1.1\",\n    \"webpack-dev-server\": \"5.2.2\",\n    \"webpack-merge\": \"6.0.1\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output\",\n    \"start\": \"npx webpack serve --config ./.webpack/webpack.dev.mjs\",\n    \"start:prod\": \"npx webpack serve --config ./.webpack/webpack.prod.mjs\",\n    \"start:coverage\": \"npx webpack serve --config ./.webpack/webpack.coverage.mjs\",\n    \"lint:js\": \"eslint \\\"example/**/*.js\\\" \\\"src/**/*.js\\\" \\\"e2e/**/*.js\\\" \\\"openmct.js\\\" --max-warnings=0\",\n    \"lint:vue\": \"eslint \\\"src/**/*.vue\\\"\",\n    \"lint:spelling\": \"cspell \\\"**/*.{js,md,vue}\\\" --show-context --gitignore --quiet\",\n    \"lint\": \"run-p \\\"lint:js -- {1}\\\" \\\"lint:vue -- {1}\\\" \\\"lint:spelling -- {1}\\\" --\",\n    \"lint:fix\": \"eslint example src e2e --ext .js,.vue openmct.js --fix\",\n    \"build:prod\": \"webpack --config ./.webpack/webpack.prod.mjs\",\n    \"build:dev\": \"webpack --config ./.webpack/webpack.dev.mjs\",\n    \"build:coverage\": \"webpack --config ./.webpack/webpack.coverage.mjs\",\n    \"build:watch\": \"webpack --config ./.webpack/webpack.dev.mjs --watch\",\n    \"info\": \"npx envinfo --system --browsers --npmPackages --binaries --languages --markdown\",\n    \"test\": \"karma start karma.conf.cjs\",\n    \"test:debug\": \"KARMA_DEBUG=true karma start karma.conf.cjs\",\n    \"test:e2e\": \"npm test --workspace e2e\",\n    \"test:e2e:a11y\": \"npm test --workspace e2e -- --config=playwright-visual-a11y.config.js --project=chrome --grep @a11y\",\n    \"test:e2e:mobile\": \"npm test --workspace e2e -- --config=playwright-mobile.config.js\",\n    \"test:e2e:couchdb\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @couchdb --workers=1\",\n    \"test:e2e:ci\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep-invert \\\"@couchdb|@generatedata\\\"\",\n    \"test:e2e:local\": \"npm test --workspace e2e -- --config=playwright-local.config.js --project=chrome\",\n    \"test:e2e:generatedata\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @generatedata\",\n    \"test:e2e:checksnapshots\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --retries=0\",\n    \"test:e2e:updatesnapshots\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots\",\n    \"test:e2e:visual:ci\": \"npm run test:visual --workspace e2e -- --config .percy.ci.yml --partial -- npx playwright test --config=playwright-visual-a11y.config.js --project=chrome\",\n    \"test:e2e:visual:full\": \"npm run test:visual --workspace e2e -- --config .percy.nightly.yml -- npx playwright test --config=playwright-visual-a11y.config.js\",\n    \"test:e2e:full\": \"npm test --workspace e2e -- --config=playwright-ci.config.js --grep-invert @couchdb\",\n    \"test:e2e:watch\": \"npm test --workspace e2e -- --ui --config=playwright-watch.config.js\",\n    \"test:perf:contract\": \"npm test --workspace e2e -- --config=playwright-performance-dev.config.js\",\n    \"test:perf:localhost\": \"npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome\",\n    \"test:perf:memory\": \"npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory\",\n    \"update-about-dialog-copyright\": \"perl -pi -e 's/20\\\\d\\\\d\\\\-202\\\\d/2014\\\\-2024/gm' ./src/ui/layout/AboutDialog.vue\",\n    \"update-copyright-date\": \"npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\\\s\\\\(c\\\\)\\\\s20\\\\d\\\\d\\\\-20\\\\d\\\\d/Copyright \\\\(c\\\\)\\\\ 2014\\\\-2024/gm'\",\n    \"cov:e2e:report\": \"nyc report --reporter=lcovonly --report-dir=./coverage/e2e\",\n    \"prepare\": \"npm run build:prod && npx tsc\"\n  },\n  \"homepage\": \"https://nasa.github.io/openmct\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/nasa/openmct.git\"\n  },\n  \"engines\": {\n    \"node\": \">=18.14.2 <23\"\n  },\n  \"browserslist\": [\n    \"Firefox ESR\",\n    \"not IE 11\",\n    \"last 2 Chrome versions\",\n    \"unreleased Chrome versions\",\n    \"ios_saf >= 16\",\n    \"Safari >= 16\"\n  ],\n  \"author\": {\n    \"name\": \"National Aeronautics and Space Administration\",\n    \"url\": \"https://www.nasa.gov\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"keywords\": [\n    \"nasa\"\n  ]\n}\n"
  },
  {
    "path": "src/MCT.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport { createApp, markRaw } from 'vue';\n\nimport ActionsAPI from './api/actions/ActionsAPI.js';\nimport AnnotationAPI from './api/annotation/AnnotationAPI.js';\nimport BrandingAPI from './api/Branding.js';\nimport CompositionAPI from './api/composition/CompositionAPI.js';\nimport EditorAPI from './api/Editor.js';\nimport FaultManagementAPI from './api/faultmanagement/FaultManagementAPI.js';\nimport FormsAPI from './api/forms/FormsAPI.js';\nimport IndicatorAPI from './api/indicators/IndicatorAPI.js';\nimport MenuAPI from './api/menu/MenuAPI.js';\nimport NotificationAPI from './api/notifications/NotificationAPI.js';\nimport ObjectAPI from './api/objects/ObjectAPI.js';\nimport OverlayAPI from './api/overlays/OverlayAPI.js';\nimport PriorityAPI from './api/priority/PriorityAPI.js';\nimport StatusAPI from './api/status/StatusAPI.js';\nimport TelemetryAPI from './api/telemetry/TelemetryAPI.js';\nimport TimeAPI from './api/time/TimeAPI.js';\nimport ToolTipAPI from './api/tooltips/ToolTipAPI.js';\nimport TypeRegistry from './api/types/TypeRegistry.js';\nimport UserAPI from './api/user/UserAPI.js';\nimport DuplicateActionPlugin from './plugins/duplicate/plugin.js';\nimport ExportAsJSONAction from './plugins/exportAsJSONAction/plugin.js';\nimport ImageryPlugin from './plugins/imagery/plugin.js';\nimport ImportFromJSONAction from './plugins/importFromJSONAction/plugin.js';\nimport LicensesPlugin from './plugins/licenses/plugin.js';\nimport LinkActionPlugin from './plugins/linkAction/plugin.js';\nimport MoveActionPlugin from './plugins/move/plugin.js';\nimport plugins from './plugins/plugins.js';\nimport RemoveActionPlugin from './plugins/remove/plugin.js';\nimport Selection from './selection/Selection.js';\nimport Layout from './ui/layout/AppLayout.vue';\nimport PreviewPlugin from './ui/preview/plugin.js';\nimport InspectorViewRegistry from './ui/registries/InspectorViewRegistry.js';\nimport ToolbarRegistry from './ui/registries/ToolbarRegistry.js';\nimport ViewRegistry from './ui/registries/ViewRegistry.js';\nimport ApplicationRouter from './ui/router/ApplicationRouter.js';\nimport Browse from './ui/router/Browse.js';\n\n/**\n * Open MCT is an extensible web application for building mission\n * control user interfaces. This module is itself an instance of\n * [MCT]{@link module:openmct.MCT}, which provides an interface for\n * configuring and executing the application.\n *\n * @exports openmct\n */\n\n/**\n * The Open MCT application. This may be configured by installing plugins\n * or registering extensions before the application is started.\n * @constructor\n */\nexport class MCT extends EventEmitter {\n  /**\n   * @type {import('openmct.js').BuildInfo}\n   */\n  buildInfo;\n  /**\n   * @type {string}\n   */\n  defaultClock;\n  /**\n   * @type {Record<string, OpenMCTPlugin>}\n   */\n  plugins;\n  /**\n   * Tracks current selection state of the application.\n   * @type {Selection}\n   */\n  selection;\n  constructor() {\n    super();\n\n    this.buildInfo = {\n      version: __OPENMCT_VERSION__,\n      buildDate: __OPENMCT_BUILD_DATE__,\n      revision: __OPENMCT_REVISION__,\n      branch: __OPENMCT_BUILD_BRANCH__\n    };\n\n    this.destroy = this.destroy.bind(this);\n    this.defaultClock = 'local';\n    this.plugins = plugins;\n    this.selection = new Selection(this);\n\n    /**\n     * @type {TimeAPI}\n     */\n    this.time = new TimeAPI(this);\n\n    /**\n     * An interface for interacting with the composition of domain objects.\n     * The composition of a domain object is the list of other domain\n     * objects it \"contains\" (for instance, that should be displayed\n     * beneath it in the tree.)\n     *\n     * `composition` may be called as a function, in which case it acts\n     * as [`composition.get`]{@link module:openmct.CompositionAPI#get}.\n     *\n     * @type {CompositionAPI}\n     */\n    this.composition = new CompositionAPI(this);\n\n    /**\n     * Registry for views of domain objects which should appear in the\n     * main viewing area.\n     *\n     * @type {ViewRegistry}\n     */\n    this.objectViews = new ViewRegistry();\n\n    /**\n     * Registry for views which should appear in the Inspector area.\n     * These views will be chosen based on the selection state.\n     *\n     * @type {InspectorViewRegistry}\n     */\n    this.inspectorViews = new InspectorViewRegistry();\n\n    /**\n     * Registry for views which should appear in Edit Properties\n     * dialogs, and similar user interface elements used for\n     * modifying domain objects external to its regular views.\n     *\n     * @type {ViewRegistry}\n     */\n    this.propertyEditors = new ViewRegistry();\n\n    /**\n     * Registry for views which should appear in the toolbar area while\n     * editing. These views will be chosen based on the selection state.\n     *\n     * @type {ToolbarRegistry}\n     */\n    this.toolbars = new ToolbarRegistry();\n\n    /**\n     * Registry for domain object types which may exist within this\n     * instance of Open MCT.\n     *\n     * @type {TypeRegistry}\n     */\n    this.types = new TypeRegistry();\n\n    /**\n     * An interface for interacting with domain objects and the domain\n     * object hierarchy.\n     *\n     * @type {ObjectAPI}\n     */\n    this.objects = new ObjectAPI(this.types, this);\n\n    /**\n     * An interface for retrieving and interpreting telemetry data associated\n     * with a domain object.\n     *\n     * @type {TelemetryAPI}\n     */\n    this.telemetry = new TelemetryAPI(this);\n\n    /**\n     * An interface for creating new indicators and changing them dynamically.\n     *\n     * @type {IndicatorAPI}\n     */\n    this.indicators = new IndicatorAPI(this);\n\n    /**\n     * MCT's user awareness management, to enable user and\n     * role specific functionality.\n     * @type {UserAPI}\n     */\n    this.user = new UserAPI(this);\n\n    /**\n     * An interface for managing notifications and alerts.\n     * @type {NotificationAPI}\n     */\n    this.notifications = new NotificationAPI();\n\n    /**\n     * An interface for editing domain objects.\n     * @type {EditorAPI}\n     */\n    this.editor = new EditorAPI(this);\n\n    /**\n     * An interface for managing overlays.\n     * @type {OverlayAPI}\n     */\n    this.overlays = new OverlayAPI();\n\n    /**\n     * An interface for managing tooltips.\n     * @type {ToolTipAPI}\n     */\n    this.tooltips = new ToolTipAPI();\n\n    /**\n     * An interface for managing menus.\n     * @type {MenuAPI}\n     */\n    this.menus = new MenuAPI(this);\n\n    /**\n     * An interface for managing menu actions.\n     * @type {ActionsAPI}\n     */\n    this.actions = new ActionsAPI(this);\n\n    /**\n     * An interface for managing statuses.\n     * @type {StatusAPI}\n     */\n    this.status = new StatusAPI(this);\n\n    /**\n     * An object defining constants for priority levels.\n     * @type {PriorityAPI}\n     */\n    this.priority = PriorityAPI;\n\n    /**\n     * An interface for routing application traffic.\n     * @type {ApplicationRouter}\n     */\n    this.router = new ApplicationRouter(this);\n\n    /**\n     * An interface for managing faults.\n     * @type {FaultManagementAPI}\n     */\n    this.faults = new FaultManagementAPI(this);\n\n    /**\n     * An interface for managing forms.\n     * @type {FormsAPI}\n     */\n    this.forms = new FormsAPI(this);\n\n    /**\n     * An interface for branding the application.\n     * @type {BrandingAPI}\n     */\n    this.branding = BrandingAPI;\n\n    /**\n     * MCT's annotation API that enables\n     * human-created comments and categorization linked to data products\n     * @type {AnnotationAPI}\n     */\n    this.annotation = new AnnotationAPI(this);\n\n    // Plugins that are installed by default\n    this.install(this.plugins.Plot());\n    this.install(this.plugins.TelemetryTable());\n    this.install(PreviewPlugin());\n    this.install(LicensesPlugin());\n    this.install(RemoveActionPlugin());\n    this.install(MoveActionPlugin());\n    this.install(LinkActionPlugin());\n    this.install(DuplicateActionPlugin());\n    this.install(ExportAsJSONAction());\n    this.install(ImportFromJSONAction());\n    this.install(this.plugins.FormActions());\n    this.install(this.plugins.FolderView());\n    this.install(this.plugins.Tabs());\n    this.install(ImageryPlugin());\n    this.install(this.plugins.FlexibleLayout());\n    this.install(this.plugins.GoToOriginalAction());\n    this.install(this.plugins.OpenInNewTabAction());\n    this.install(this.plugins.ReloadAction());\n    this.install(this.plugins.WebPage());\n    this.install(this.plugins.Condition());\n    this.install(this.plugins.ConditionWidget());\n    this.install(this.plugins.URLTimeSettingsSynchronizer());\n    this.install(this.plugins.NotificationIndicator());\n    this.install(this.plugins.NewFolderAction());\n    this.install(this.plugins.ViewDatumAction());\n    this.install(this.plugins.ViewLargeAction());\n    this.install(this.plugins.ObjectInterceptors());\n    this.install(this.plugins.DeviceClassifier());\n    this.install(this.plugins.UserIndicator());\n    this.install(this.plugins.Gauge());\n    this.install(this.plugins.InspectorViews());\n  }\n  /**\n   * Set path to where assets are hosted.  This should be the path to main.js.\n   * @method setAssetPath\n   */\n  setAssetPath(assetPath) {\n    this._assetPath = assetPath;\n  }\n  /**\n   * Get path to where assets are hosted.\n   * @method getAssetPath\n   */\n  getAssetPath() {\n    const assetPathLength = this._assetPath && this._assetPath.length;\n    if (!assetPathLength) {\n      return '/';\n    }\n\n    if (this._assetPath[assetPathLength - 1] !== '/') {\n      return this._assetPath + '/';\n    }\n\n    return this._assetPath;\n  }\n  #bootstrap(domElementOrSelector, isHeadlessMode) {\n    let domElement;\n    // Create element to mount Layout if it doesn't exist\n    if (domElementOrSelector === undefined) {\n      domElement = document.createElement('div');\n      document.body.appendChild(domElement);\n    } else if (typeof domElementOrSelector === 'string' && domElementOrSelector.trim().length > 0) {\n      domElement = document.querySelector(domElementOrSelector);\n      if (domElement === null) {\n        throw new Error(\n          `No element found with selector ${domElementOrSelector}. Unable to bootstrap Open MCT.`\n        );\n      }\n    } else if (domElementOrSelector instanceof HTMLElement) {\n      domElement = domElementOrSelector;\n    } else {\n      throw new Error(`Invalid HTML element or selector provided to Open MCT start function.`);\n    }\n\n    domElement.id = 'openmct-app';\n\n    if (this.types.get('layout') === undefined) {\n      this.install(\n        this.plugins.DisplayLayout({\n          showAsView: ['summary-widget']\n        })\n      );\n    }\n\n    this.element = domElement;\n\n    if (!this.time.getClock()) {\n      this.time.setClock(this.defaultClock);\n    }\n\n    this.router.route(/^\\/$/, () => {\n      this.router.setPath('/browse/');\n    });\n\n    /**\n     * Fired by [MCT]{@link module:openmct.MCT} when the application\n     * is started.\n     * @event start\n     */\n    if (!isHeadlessMode) {\n      const appLayout = createApp(Layout);\n      appLayout.provide('openmct', markRaw(this));\n      const component = appLayout.mount(domElement);\n      component.$nextTick(() => {\n        this.layout = component;\n        this.app = appLayout;\n        this.browseRoutes = new Browse(this);\n        window.addEventListener('beforeunload', this.destroy);\n        this.router.start();\n        this.emit('start');\n      });\n    } else {\n      window.addEventListener('beforeunload', this.destroy);\n\n      this.router.start();\n      this.emit('start');\n    }\n  }\n  /**\n   * Start running Open MCT. This should be called only after any plugins\n   * have been installed.\n   * @fires module:openmct.MCT~start\n   * @method start\n   * @param {Element?} domElementOrSelector the DOM element in which to run\n   *        MCT; if undefined, MCT will be run in the body of the document\n   */\n  start(domElementOrSelector, isHeadlessMode = false) {\n    if (document.readyState === 'loading') {\n      document.addEventListener(\n        'DOMContentLoaded',\n        () => {\n          this.#bootstrap(domElementOrSelector, isHeadlessMode);\n        },\n        { once: true }\n      );\n    } else {\n      this.#bootstrap(domElementOrSelector, isHeadlessMode);\n    }\n  }\n  startHeadless() {\n    let unreachableNode = document.createElement('div');\n\n    return this.start(unreachableNode, true);\n  }\n  /**\n   * Install a plugin in MCT.\n   *\n   * @param {Function} plugin a plugin install function which will be\n   *     invoked with the mct instance.\n   */\n  install(plugin) {\n    plugin(this);\n  }\n\n  destroy() {\n    window.removeEventListener('beforeunload', this.destroy);\n    this.emit('destroy');\n  }\n}\n\n/**\n * @typedef {import('../openmct.js').OpenMCTPlugin} OpenMCTPlugin\n */\n"
  },
  {
    "path": "src/MCTSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport * as testUtils from 'utils/testing';\n\nimport plugins from './plugins/plugins.js';\n\ndescribe('MCT', function () {\n  let openmct;\n  let mockPlugin;\n  let mockPlugin2;\n  let mockListener;\n\n  beforeEach(function () {\n    mockPlugin = jasmine.createSpy('plugin');\n    mockPlugin2 = jasmine.createSpy('plugin2');\n    mockListener = jasmine.createSpy('listener');\n\n    openmct = testUtils.createOpenMct();\n\n    openmct.install(mockPlugin);\n    openmct.install(mockPlugin2);\n    openmct.on('start', mockListener);\n  });\n\n  // Clean up the dirty singleton.\n  afterEach(function () {\n    return testUtils.resetApplicationState(openmct);\n  });\n\n  it('exposes plugins', function () {\n    expect(openmct.plugins).toEqual(plugins);\n  });\n\n  it('does not issue a start event before started', function () {\n    expect(mockListener).not.toHaveBeenCalled();\n  });\n\n  describe('start', function () {\n    let appHolder;\n    beforeEach(function (done) {\n      appHolder = document.createElement('div');\n      openmct.on('start', done);\n      openmct.start(appHolder);\n    });\n\n    it('calls plugins for configuration', function () {\n      expect(mockPlugin).toHaveBeenCalledWith(openmct);\n      expect(mockPlugin2).toHaveBeenCalledWith(openmct);\n    });\n\n    it('emits a start event', function () {\n      expect(mockListener).toHaveBeenCalled();\n    });\n\n    it('Renders the application into the provided container element', function () {\n      let openMctShellElements = appHolder.querySelectorAll('div.l-shell');\n      expect(openMctShellElements.length).toBe(1);\n    });\n  });\n\n  describe('startHeadless', function () {\n    beforeEach(function (done) {\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    it('calls plugins for configuration', function () {\n      expect(mockPlugin).toHaveBeenCalledWith(openmct);\n      expect(mockPlugin2).toHaveBeenCalledWith(openmct);\n    });\n\n    it('emits a start event', function () {\n      expect(mockListener).toHaveBeenCalled();\n    });\n\n    it('Does not render Open MCT', function () {\n      let openMctShellElements = document.body.querySelectorAll('div.l-shell');\n      expect(openMctShellElements.length).toBe(0);\n    });\n  });\n\n  describe('setAssetPath', function () {\n    let testAssetPath;\n\n    it('configures the path for assets', function () {\n      testAssetPath = 'some/path/';\n      openmct.setAssetPath(testAssetPath);\n      expect(openmct.getAssetPath()).toBe(testAssetPath);\n    });\n\n    it('adds a trailing /', function () {\n      testAssetPath = 'some/path';\n      openmct.setAssetPath(testAssetPath);\n      expect(openmct.getAssetPath()).toBe(testAssetPath + '/');\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/Branding.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nlet brandingOptions = {};\n\n/**\n * @typedef {Object} BrandingOptions\n * @property {string} smallLogoImage URL to the image to use as the applications logo.\n * This logo will appear on every screen and when clicked will launch the about dialog.\n * @property {string} aboutHtml Custom content for the about screen. When defined the\n * supplied content will be inserted at the start of the about dialog, and the default\n * Open MCT splash logo will be suppressed.\n */\n\n/**\n * Set branding options for the application. These will override certain visual elements\n * of the application and allow for customization of the application.\n * @param {BrandingOptions} options\n */\nexport default function Branding(options) {\n  if (arguments.length === 1) {\n    brandingOptions = options;\n  }\n\n  return brandingOptions;\n}\n"
  },
  {
    "path": "src/api/Editor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nexport default class Editor extends EventEmitter {\n  constructor(openmct) {\n    super();\n    this.editing = false;\n    this.openmct = openmct;\n  }\n\n  /**\n   * Initiate an editing session. This will start a transaction during\n   * which any persist operations will be deferred until either save()\n   * or finish() are called.\n   */\n  edit() {\n    if (this.editing === true) {\n      throw 'Already editing';\n    }\n\n    this.editing = true;\n    this.emit('isEditing', true);\n    this.openmct.objects.startTransaction();\n  }\n\n  /**\n   * @returns {boolean} true if the application is in edit mode, false otherwise.\n   */\n  isEditing() {\n    return this.editing;\n  }\n\n  /**\n   * Save any unsaved changes from this editing session. This will\n   * end the current transaction.\n   */\n  async save() {\n    const transaction = this.openmct.objects.getActiveTransaction();\n    await transaction.commit();\n    this.editing = false;\n    this.emit('isEditing', false);\n    this.openmct.objects.endTransaction();\n  }\n\n  /**\n   * End the currently active transaction and discard unsaved changes.\n   */\n  cancel() {\n    this.editing = false;\n    this.emit('isEditing', false);\n\n    return new Promise((resolve, reject) => {\n      const transaction = this.openmct.objects.getActiveTransaction();\n      if (!transaction) {\n        return resolve();\n      }\n\n      transaction\n        .cancel()\n        .then(resolve)\n        .catch(reject)\n        .finally(() => {\n          this.openmct.objects.endTransaction();\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/api/EditorSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../utils/testing.js';\n\ndescribe('The Editor API', () => {\n  let openmct;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.on('start', done);\n\n    spyOn(openmct.objects, 'endTransaction');\n\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('opens a transaction on edit', () => {\n    expect(openmct.objects.isTransactionActive()).toBeFalse();\n    openmct.editor.edit();\n    expect(openmct.objects.isTransactionActive()).toBeTrue();\n  });\n\n  it('closes an open transaction on successful save', async () => {\n    spyOn(openmct.objects, 'getActiveTransaction').and.returnValue({\n      commit: () => Promise.resolve(true)\n    });\n\n    openmct.editor.edit();\n    await openmct.editor.save();\n\n    expect(openmct.objects.endTransaction).toHaveBeenCalled();\n  });\n\n  it('does not close an open transaction on failed save', async () => {\n    spyOn(openmct.objects, 'getActiveTransaction').and.returnValue({\n      commit: () => Promise.reject()\n    });\n\n    openmct.editor.edit();\n    await openmct.editor.save().catch(() => {});\n\n    expect(openmct.objects.endTransaction).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/api/actions/ActionCollection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\n/**\n * A collection of actions applicable to a domain object.\n * @extends EventEmitter\n */\nclass ActionCollection extends EventEmitter {\n  /**\n   * Creates an instance of ActionCollection.\n   * @param {Object.<string, Action>} applicableActions - The actions applicable to the domain object.\n   * @param {import('openmct').ObjectPath} objectPath - The path to the domain object.\n   * @param {import('openmct').View} view - The view displaying the domain object.\n   * @param {import('openmct').OpenMCT} openmct - The Open MCT API.\n   * @param {boolean} skipEnvironmentObservers - Whether to skip setting up environment observers.\n   */\n  constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {\n    super();\n\n    this.applicableActions = applicableActions;\n    this.openmct = openmct;\n    this.objectPath = objectPath;\n    this.view = view;\n    this.skipEnvironmentObservers = skipEnvironmentObservers;\n    this.objectUnsubscribes = [];\n\n    let debounceOptions = {\n      leading: false,\n      trailing: true\n    };\n\n    this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);\n    this._update = _.debounce(this._update.bind(this), 150, debounceOptions);\n\n    if (!skipEnvironmentObservers) {\n      this._observeObjectPath();\n      this.openmct.editor.on('isEditing', this._updateActions);\n    }\n  }\n\n  /**\n   * Disables the specified actions.\n   * @param {string[]} actionKeys - The keys of the actions to disable.\n   */\n  disable(actionKeys) {\n    actionKeys.forEach((actionKey) => {\n      if (this.applicableActions[actionKey]) {\n        this.applicableActions[actionKey].isDisabled = true;\n      }\n    });\n    this._update();\n  }\n\n  /**\n   * Enables the specified actions.\n   * @param {string[]} actionKeys - The keys of the actions to enable.\n   */\n  enable(actionKeys) {\n    actionKeys.forEach((actionKey) => {\n      if (this.applicableActions[actionKey]) {\n        this.applicableActions[actionKey].isDisabled = false;\n      }\n    });\n    this._update();\n  }\n\n  /**\n   * Hides the specified actions.\n   * @param {string[]} actionKeys - The keys of the actions to hide.\n   */\n  hide(actionKeys) {\n    actionKeys.forEach((actionKey) => {\n      if (this.applicableActions[actionKey]) {\n        this.applicableActions[actionKey].isHidden = true;\n      }\n    });\n    this._update();\n  }\n\n  /**\n   * Shows the specified actions.\n   * @param {string[]} actionKeys - The keys of the actions to show.\n   */\n  show(actionKeys) {\n    actionKeys.forEach((actionKey) => {\n      if (this.applicableActions[actionKey]) {\n        this.applicableActions[actionKey].isHidden = false;\n      }\n    });\n    this._update();\n  }\n\n  /**\n   * Destroys the action collection, removing all listeners and observers.\n   */\n  destroy() {\n    if (!this.skipEnvironmentObservers) {\n      this.objectUnsubscribes.forEach((unsubscribe) => {\n        unsubscribe();\n      });\n\n      this.openmct.editor.off('isEditing', this._updateActions);\n    }\n\n    this.emit('destroy', this.view);\n    this.removeAllListeners();\n  }\n\n  /**\n   * Gets all visible actions.\n   * @returns {Action[]} An array of visible actions.\n   */\n  getVisibleActions() {\n    let actionsArray = Object.keys(this.applicableActions);\n    let visibleActions = [];\n\n    actionsArray.forEach((actionKey) => {\n      let action = this.applicableActions[actionKey];\n\n      if (!action.isHidden) {\n        visibleActions.push(action);\n      }\n    });\n\n    return visibleActions;\n  }\n\n  /**\n   * Gets all actions that should be shown in the status bar.\n   * @returns {Action[]} An array of status bar actions.\n   */\n  getStatusBarActions() {\n    let actionsArray = Object.keys(this.applicableActions);\n    let statusBarActions = [];\n\n    actionsArray.forEach((actionKey) => {\n      let action = this.applicableActions[actionKey];\n\n      if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {\n        statusBarActions.push(action);\n      }\n    });\n\n    return statusBarActions;\n  }\n\n  /**\n   * Gets the object containing all applicable actions.\n   * @returns {Object.<string, Action>} The object of applicable actions.\n   */\n  getActionsObject() {\n    return this.applicableActions;\n  }\n\n  /**\n   * Emits an update event with the current applicable actions.\n   * @private\n   */\n  _update() {\n    this.emit('update', this.applicableActions);\n  }\n\n  /**\n   * Sets up observers for the object path.\n   * @private\n   */\n  _observeObjectPath() {\n    let actionCollection = this;\n\n    /**\n     * Updates an object with new properties.\n     * @param {Object} oldObject - The object to update.\n     * @param {Object} newObject - The object containing new properties.\n     */\n    function updateObject(oldObject, newObject) {\n      Object.assign(oldObject, newObject);\n\n      actionCollection._updateActions();\n    }\n\n    this.objectPath.forEach((object) => {\n      if (object) {\n        let unsubscribe = this.openmct.objects.observe(\n          object,\n          '*',\n          updateObject.bind(this, object)\n        );\n\n        this.objectUnsubscribes.push(unsubscribe);\n      }\n    });\n  }\n\n  /**\n   * Updates the applicable actions.\n   * @private\n   */\n  _updateActions() {\n    let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);\n\n    this.applicableActions = this._mergeOldAndNewActions(\n      this.applicableActions,\n      newApplicableActions\n    );\n    this._update();\n  }\n\n  /**\n   * Merges old and new actions, preserving existing action states.\n   * @param {Object.<string, Action>} oldActions - The existing actions.\n   * @param {Object.<string, Action>} newActions - The new actions.\n   * @returns {Object.<string, Action>} The merged actions.\n   * @private\n   */\n  _mergeOldAndNewActions(oldActions, newActions) {\n    let mergedActions = {};\n    Object.keys(newActions).forEach((key) => {\n      if (oldActions[key]) {\n        mergedActions[key] = oldActions[key];\n      } else {\n        mergedActions[key] = newActions[key];\n      }\n    });\n\n    return mergedActions;\n  }\n}\n\nexport default ActionCollection;\n\n/**\n * @typedef {import('openmct').Action} Action\n */\n"
  },
  {
    "path": "src/api/actions/ActionCollectionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport ActionCollection from './ActionCollection.js';\n\ndescribe('The ActionCollection', () => {\n  let openmct;\n  let actionCollection;\n  let mockApplicableActions;\n  let mockObjectPath;\n  let mockView;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    openmct.objects.addProvider(\n      '',\n      jasmine.createSpyObj('mockMutableObjectProvider', ['create', 'update'])\n    );\n    mockView = {\n      getViewContext: () => {\n        return {\n          onlyAppliesToTestCase: true\n        };\n      }\n    };\n    mockApplicableActions = {\n      'test-action-object-path': {\n        name: 'Test Action Object Path',\n        key: 'test-action-object-path',\n        cssClass: 'test-action-object-path',\n        description: 'This is a test action for object path',\n        group: 'action',\n        priority: 9,\n        appliesTo: (objectPath) => {\n          if (objectPath.length) {\n            return objectPath[0].type === 'fake-folder';\n          }\n\n          return false;\n        },\n        invoke: () => {}\n      },\n      'test-action-view': {\n        name: 'Test Action View',\n        key: 'test-action-view',\n        cssClass: 'test-action-view',\n        description: 'This is a test action for view',\n        group: 'action',\n        priority: 9,\n        showInStatusBar: true,\n        appliesTo: (objectPath, view = {}) => {\n          if (view.getViewContext) {\n            let viewContext = view.getViewContext();\n\n            return viewContext.onlyAppliesToTestCase;\n          }\n\n          return false;\n        },\n        invoke: () => {}\n      }\n    };\n\n    actionCollection = new ActionCollection(\n      mockApplicableActions,\n      mockObjectPath,\n      mockView,\n      openmct\n    );\n  });\n\n  afterEach(() => {\n    actionCollection.destroy();\n\n    return resetApplicationState(openmct);\n  });\n\n  describe('disable method invoked with action keys', () => {\n    it('marks those actions as isDisabled', () => {\n      let actionKey = 'test-action-object-path';\n      let actionsObject = actionCollection.getActionsObject();\n      let action = actionsObject[actionKey];\n\n      expect(action.isDisabled).toBeFalsy();\n\n      actionCollection.disable([actionKey]);\n      actionsObject = actionCollection.getActionsObject();\n      action = actionsObject[actionKey];\n\n      expect(action.isDisabled).toBeTrue();\n    });\n  });\n\n  describe('enable method invoked with action keys', () => {\n    it('marks the isDisabled property as false', () => {\n      let actionKey = 'test-action-object-path';\n\n      actionCollection.disable([actionKey]);\n\n      let actionsObject = actionCollection.getActionsObject();\n      let action = actionsObject[actionKey];\n\n      expect(action.isDisabled).toBeTrue();\n\n      actionCollection.enable([actionKey]);\n      actionsObject = actionCollection.getActionsObject();\n      action = actionsObject[actionKey];\n\n      expect(action.isDisabled).toBeFalse();\n    });\n  });\n\n  describe('hide method invoked with action keys', () => {\n    it('marks those actions as isHidden', () => {\n      let actionKey = 'test-action-object-path';\n      let actionsObject = actionCollection.getActionsObject();\n      let action = actionsObject[actionKey];\n\n      expect(action.isHidden).toBeFalsy();\n\n      actionCollection.hide([actionKey]);\n      actionsObject = actionCollection.getActionsObject();\n      action = actionsObject[actionKey];\n\n      expect(action.isHidden).toBeTrue();\n    });\n  });\n\n  describe('show method invoked with action keys', () => {\n    it('marks the isHidden property as false', () => {\n      let actionKey = 'test-action-object-path';\n\n      actionCollection.hide([actionKey]);\n\n      let actionsObject = actionCollection.getActionsObject();\n      let action = actionsObject[actionKey];\n\n      expect(action.isHidden).toBeTrue();\n\n      actionCollection.show([actionKey]);\n      actionsObject = actionCollection.getActionsObject();\n      action = actionsObject[actionKey];\n\n      expect(action.isHidden).toBeFalse();\n    });\n  });\n\n  describe('getVisibleActions method', () => {\n    it('returns an array of non hidden actions', () => {\n      let action1Key = 'test-action-object-path';\n      let action2Key = 'test-action-view';\n\n      actionCollection.hide([action1Key]);\n\n      let visibleActions = actionCollection.getVisibleActions();\n\n      expect(Array.isArray(visibleActions)).toBeTrue();\n      expect(visibleActions.length).toEqual(1);\n      expect(visibleActions[0].key).toEqual(action2Key);\n\n      actionCollection.show([action1Key]);\n      visibleActions = actionCollection.getVisibleActions();\n\n      expect(visibleActions.length).toEqual(2);\n    });\n  });\n\n  describe('getStatusBarActions method', () => {\n    it('returns an array of non disabled, non hidden statusBar actions', () => {\n      let action2Key = 'test-action-view';\n\n      let statusBarActions = actionCollection.getStatusBarActions();\n\n      expect(Array.isArray(statusBarActions)).toBeTrue();\n      expect(statusBarActions.length).toEqual(1);\n      expect(statusBarActions[0].key).toEqual(action2Key);\n\n      actionCollection.disable([action2Key]);\n      statusBarActions = actionCollection.getStatusBarActions();\n\n      expect(statusBarActions.length).toEqual(0);\n\n      actionCollection.enable([action2Key]);\n      statusBarActions = actionCollection.getStatusBarActions();\n\n      expect(statusBarActions.length).toEqual(1);\n      expect(statusBarActions[0].key).toEqual(action2Key);\n\n      actionCollection.hide([action2Key]);\n      statusBarActions = actionCollection.getStatusBarActions();\n\n      expect(statusBarActions.length).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/actions/ActionsAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport ActionCollection from './ActionCollection.js';\n\n/**\n * The ActionsAPI manages the registration and retrieval of actions in Open MCT.\n * @extends EventEmitter\n */\nclass ActionsAPI extends EventEmitter {\n  /**\n   * @param {import('openmct').OpenMCT} openmct - The Open MCT instance\n   */\n  constructor(openmct) {\n    super();\n\n    /** @type {Object<string, Action>} */\n    this._allActions = {};\n    /** @type {WeakMap<Object, ActionCollection>} */\n    this._actionCollections = new WeakMap();\n    /** @type {import('openmct').OpenMCT} */\n    this._openmct = openmct;\n\n    /** @type {string[]} */\n    this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];\n\n    this.register = this.register.bind(this);\n    this.getActionsCollection = this.getActionsCollection.bind(this);\n    this._applicableActions = this._applicableActions.bind(this);\n    this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);\n  }\n\n  /**\n   * Register an action with the API.\n   * @param {Action} actionDefinition - The definition of the action to register\n   */\n  register(actionDefinition) {\n    this._allActions[actionDefinition.key] = actionDefinition;\n  }\n\n  /**\n   * Get an action by its key.\n   * @param {string} key - The key of the action to retrieve\n   * @returns {Action|undefined} The action definition, or undefined if not found\n   */\n  getAction(key) {\n    return this._allActions[key];\n  }\n\n  /**\n   * Get or create an ActionCollection for a given object path and view.\n   * @param {import('openmct').ObjectPath} objectPath - The path of the object\n   * @param {import('openmct').View} [view] - The view object\n   * @returns {ActionCollection} The ActionCollection for the given object path and view\n   */\n  getActionsCollection(objectPath, view) {\n    if (view) {\n      return (\n        this._getCachedActionCollection(objectPath, view) ||\n        this._newActionCollection(objectPath, view, true)\n      );\n    } else {\n      return this._newActionCollection(objectPath, view, true);\n    }\n  }\n\n  /**\n   * Update the order in which action groups are displayed.\n   * @param {string[]} groupArray - An array of group names in the desired order\n   */\n  updateGroupOrder(groupArray) {\n    this._groupOrder = groupArray;\n  }\n\n  /**\n   * Get a cached ActionCollection for a given view.\n   * @param {import('openmct').ObjectPath} objectPath - The path of the object\n   * @param {Object} view - The view object\n   * @returns {ActionCollection|undefined} The cached ActionCollection, or undefined if not found\n   */\n  _getCachedActionCollection(objectPath, view) {\n    return this._actionCollections.get(view);\n  }\n\n  /**\n   * Create a new ActionCollection.\n   * @param {import('openmct').ObjectPath} objectPath - The path of the object\n   * @param {import('openmct').View} [view] - The view object\n   * @param {boolean} skipEnvironmentObservers - Whether to skip environment observers\n   * @returns {ActionCollection} The new ActionCollection\n   */\n  _newActionCollection(objectPath, view, skipEnvironmentObservers) {\n    let applicableActions = this._applicableActions(objectPath, view);\n\n    const actionCollection = new ActionCollection(\n      applicableActions,\n      objectPath,\n      view,\n      this._openmct,\n      skipEnvironmentObservers\n    );\n    if (view) {\n      this._cacheActionCollection(view, actionCollection);\n    }\n\n    return actionCollection;\n  }\n\n  /**\n   * Cache an ActionCollection for a given view.\n   * @param {import('openmct').View} view - The view object\n   * @param {ActionCollection} actionCollection - The ActionCollection to cache\n   */\n  _cacheActionCollection(view, actionCollection) {\n    this._actionCollections.set(view, actionCollection);\n    actionCollection.on('destroy', this._updateCachedActionCollections);\n  }\n\n  /**\n   * Update cached ActionCollections when destroyed.\n   * @param {import('openmct').View} view - The key (View object)of the destroyed ActionCollection\n   */\n  _updateCachedActionCollections(view) {\n    if (this._actionCollections.has(view)) {\n      let actionCollection = this._actionCollections.get(view);\n      actionCollection.off('destroy', this._updateCachedActionCollections);\n      delete actionCollection.applicableActions;\n      this._actionCollections.delete(view);\n    }\n  }\n\n  /**\n   * Get applicable actions for a given object path and view.\n   * @param {import('openmct').ObjectPath} objectPath - The path of the object\n   * @param {import('openmct').View} [view] - The view object\n   * @returns {Object<string, Action>} A dictionary of applicable actions keyed by action key\n   */\n  _applicableActions(objectPath, view) {\n    let actionsObject = {};\n\n    let keys = Object.keys(this._allActions).filter((key) => {\n      let actionDefinition = this._allActions[key];\n\n      if (actionDefinition.appliesTo === undefined) {\n        return true;\n      } else {\n        return actionDefinition.appliesTo(objectPath, view);\n      }\n    });\n\n    keys.forEach((key) => {\n      let action = _.clone(this._allActions[key]);\n\n      actionsObject[key] = action;\n    });\n\n    return actionsObject;\n  }\n\n  /**\n   * Group and sort actions based on their group and priority.\n   * @param {Action[]|Object<string, Action>} actionsArray - An array or object of actions to group and sort\n   * @returns {Action[][]} An array of grouped and sorted action arrays\n   */\n  _groupAndSortActions(actionsArray = []) {\n    if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {\n      actionsArray = Object.keys(actionsArray).map((key) => actionsArray[key]);\n    }\n\n    let actionsObject = {};\n    let groupedSortedActionsArray = [];\n\n    function sortDescending(a, b) {\n      return b.priority - a.priority;\n    }\n\n    actionsArray.forEach((action) => {\n      if (actionsObject[action.group] === undefined) {\n        actionsObject[action.group] = [action];\n      } else {\n        actionsObject[action.group].push(action);\n      }\n    });\n\n    this._groupOrder.forEach((group) => {\n      let groupArray = actionsObject[group];\n\n      if (groupArray) {\n        groupedSortedActionsArray.push(groupArray.sort(sortDescending));\n      }\n    });\n\n    return groupedSortedActionsArray;\n  }\n}\n\nexport default ActionsAPI;\n\n/**\n * @typedef {Object} Action\n * @property {string} name - The display name of the action.\n * @property {string} key - A unique identifier for the action.\n * @property {string} description - A brief description of what the action does.\n * @property {string} cssClass - The CSS class for the action's icon.\n * @property {string} [group] - The group this action belongs to (e.g., 'action', 'import').\n * @property {number} [priority] - The priority of the action within its group (controls the order of the actions in the menu).\n * @property {boolean} [isHidden] - Whether the action should be hidden from menus.\n * @property {(objectPath: ObjectPath, view: View) => void} invoke - Executes the action.\n * @property {(objectPath: ObjectPath, view: View) => boolean} appliesTo - Determines if the action is applicable to the given object path.\n */\n\n/** @typedef {import('openmct').ObjectPath} ObjectPath */\n/** @typedef {import('openmct').View} View */\n"
  },
  {
    "path": "src/api/actions/ActionsAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport ActionCollection from './ActionCollection.js';\nimport ActionsAPI from './ActionsAPI.js';\n\ndescribe('The Actions API', () => {\n  let openmct;\n  let actionsAPI;\n  let mockAction;\n  let mockObjectPath;\n  let mockObjectPathAction;\n  let mockViewContext1;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n    actionsAPI = new ActionsAPI(openmct);\n    mockObjectPathAction = {\n      name: 'Test Action Object Path',\n      key: 'test-action-object-path',\n      cssClass: 'test-action-object-path',\n      description: 'This is a test action for object path',\n      group: 'action',\n      priority: 9,\n      appliesTo: (objectPath) => {\n        if (objectPath.length) {\n          return objectPath[0].type === 'fake-folder';\n        }\n\n        return false;\n      },\n      invoke: () => {}\n    };\n    mockAction = {\n      name: 'Test Action View',\n      key: 'test-action-view',\n      cssClass: 'test-action-view',\n      description: 'This is a test action for view',\n      group: 'action',\n      priority: 9,\n      appliesTo: (objectPath, view = {}) => {\n        if (view.getViewContext) {\n          let viewContext = view.getViewContext();\n\n          return viewContext.onlyAppliesToTestCase;\n        }\n\n        return false;\n      },\n      invoke: () => {}\n    };\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    mockViewContext1 = {\n      getViewContext: () => {\n        return {\n          onlyAppliesToTestCase: true\n        };\n      }\n    };\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('register method', () => {\n    it('adds action to ActionsAPI', () => {\n      actionsAPI.register(mockAction);\n\n      let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);\n      let action = actionCollection.getActionsObject()[mockAction.key];\n\n      expect(action.key).toEqual(mockAction.key);\n      expect(action.name).toEqual(mockAction.name);\n    });\n  });\n\n  describe('get method', () => {\n    beforeEach(() => {\n      actionsAPI.register(mockAction);\n      actionsAPI.register(mockObjectPathAction);\n    });\n\n    it('returns an ActionCollection when invoked with an objectPath only', () => {\n      let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);\n      let instanceOfActionCollection = actionCollection instanceof ActionCollection;\n\n      expect(instanceOfActionCollection).toBeTrue();\n    });\n\n    it('returns an ActionCollection when invoked with an objectPath and view', () => {\n      let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);\n      let instanceOfActionCollection = actionCollection instanceof ActionCollection;\n\n      expect(instanceOfActionCollection).toBeTrue();\n    });\n\n    it('returns relevant actions when invoked with objectPath only', () => {\n      let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);\n      let action = actionCollection.getActionsObject()[mockObjectPathAction.key];\n\n      expect(action.key).toEqual(mockObjectPathAction.key);\n      expect(action.name).toEqual(mockObjectPathAction.name);\n    });\n\n    it('returns relevant actions when invoked with objectPath and view', () => {\n      let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);\n      let action = actionCollection.getActionsObject()[mockAction.key];\n\n      expect(action.key).toEqual(mockAction.key);\n      expect(action.name).toEqual(mockAction.name);\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/annotation/AnnotationAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\nimport { v4 as uuid } from 'uuid';\n\n/**\n * @readonly\n * @enum {string} AnnotationType\n * @property {string} NOTEBOOK The notebook annotation type\n * @property {string} GEOSPATIAL The geospatial annotation type\n * @property {string} PIXEL_SPATIAL The pixel-spatial annotation type\n * @property {string} TEMPORAL The temporal annotation type\n * @property {string} PLOT_SPATIAL The plot-spatial annotation type\n */\nconst ANNOTATION_TYPES = Object.freeze({\n  NOTEBOOK: 'NOTEBOOK',\n  GEOSPATIAL: 'GEOSPATIAL',\n  PIXEL_SPATIAL: 'PIXEL_SPATIAL',\n  TEMPORAL: 'TEMPORAL',\n  PLOT_SPATIAL: 'PLOT_SPATIAL'\n});\n\n/**\n * @type {string}\n */\nconst ANNOTATION_TYPE = 'annotation';\n\n/**\n * @type {string}\n */\nconst ANNOTATION_LAST_CREATED = 'annotationLastCreated';\n\n/**\n * @typedef {Object} Tag\n * @property {string} key a unique identifier for the tag\n * @property {string} backgroundColor eg. \"#cc0000\"\n * @property {string} foregroundColor eg. \"#ffffff\"\n */\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('openmct').Identifier} Identifier\n */\n\n/**\n * @typedef {import('openmct').OpenMCT} OpenMCT\n */\n\n/**\n * An interface for interacting with annotations of domain objects.\n * An annotation of a domain object is an operator created object for the purposes\n * of further describing data in plots, notebooks, maps, etc. For example, an annotation\n * could be a tag on a plot notating an interesting set of points labeled SCIENCE. It could\n * also be set of notebook entries the operator has tagged DRIVING when a robot monitored by OpenMCT\n * about rationals behind why the robot has taken a certain path.\n * Annotations are discoverable using search, and are typically rendered in OpenMCT views to bring attention\n * to other users.\n * @class AnnotationAPI\n * @extends {EventEmitter}\n */\nexport default class AnnotationAPI extends EventEmitter {\n  /** @type {Map<ANNOTATION_TYPES, Array<(a, b) => boolean >>} */\n  #targetComparatorMap;\n\n  /**\n   * @param {OpenMCT} openmct\n   */\n  constructor(openmct) {\n    super();\n    this.openmct = openmct;\n    this.availableTags = {};\n    this.namespaceToSaveAnnotations = '';\n    this.#targetComparatorMap = new Map();\n\n    this.ANNOTATION_TYPES = ANNOTATION_TYPES;\n    this.ANNOTATION_TYPE = ANNOTATION_TYPE;\n    this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;\n\n    this.openmct.types.addType(ANNOTATION_TYPE, {\n      name: 'Annotation',\n      description:\n        'A user created note or comment about time ranges, pixel space, and geospatial features.',\n      creatable: false,\n      cssClass: 'icon-notebook',\n      initialize: function (domainObject) {\n        domainObject.targets = domainObject.targets || [];\n        domainObject._deleted = domainObject._deleted || false;\n        domainObject.originalContextPath = domainObject.originalContextPath || '';\n        domainObject.tags = domainObject.tags || [];\n        domainObject.contentText = domainObject.contentText || '';\n        domainObject.annotationType = domainObject.annotationType || 'plotspatial';\n      }\n    });\n  }\n  /**\n   * Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)\n   * @typedef {Object} CreateAnnotationOptions\n   * @property {string} name a name for the new annotation (e.g., \"Plot annnotation\")\n   * @property {DomainObject} domainObject the domain object this annotation was created with\n   * @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)\n   * @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations\n   * @property {string} contentText Some text to add to the annotation, e.g. (\"This annotation is about science\")\n   * @property {Array<Object>} targets The targets ID keystrings and their specific properties.\n   * For plots, this will be a bounding box, e.g.: {keyString: \"d8385009-789d-457b-acc7-d50ba2fd55ea\", maxY: 100, minY: 0, maxX: 100, minX: 0}\n   * For notebooks, this will be an entry ID, e.g.: {entryId: \"entry-ecb158f5-d23c-45e1-a704-649b382622ba\"}\n   * @property {DomainObject[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)\n   */\n  /**\n   * @param {CreateAnnotationOptions} options\n   * @returns {Promise<DomainObject>} a promise which will resolve when the domain object\n   *          has been created, or be rejected if it cannot be saved\n   */\n  async create({\n    name,\n    domainObject,\n    annotationType,\n    tags,\n    contentText,\n    targets,\n    targetDomainObjects\n  }) {\n    if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {\n      throw new Error(`Unknown annotation type: ${annotationType}`);\n    }\n\n    if (!targets.length) {\n      throw new Error(`At least one target is required to create an annotation`);\n    }\n\n    if (targets.some((target) => !target.keyString)) {\n      throw new Error(`All targets require a keyString to create an annotation`);\n    }\n\n    if (!targetDomainObjects.length) {\n      throw new Error(`At least one targetDomainObject is required to create an annotation`);\n    }\n\n    const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n    const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);\n    const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);\n    const namespace = this.namespaceToSaveAnnotations;\n    const type = 'annotation';\n    const typeDefinition = this.openmct.types.get(type);\n    const definition = typeDefinition.definition;\n\n    const createdObject = {\n      name,\n      type,\n      identifier: {\n        key: uuid(),\n        namespace\n      },\n      tags,\n      _deleted: false,\n      annotationType,\n      contentText,\n      originalContextPath\n    };\n\n    if (definition.initialize) {\n      definition.initialize(createdObject);\n    }\n\n    createdObject.targets = targets;\n    createdObject.originalContextPath = originalContextPath;\n\n    const success = await this.openmct.objects.save(createdObject);\n    if (success) {\n      this.emit('annotationCreated', createdObject);\n      targetDomainObjects.forEach((targetDomainObject) => {\n        this.#updateAnnotationModified(targetDomainObject);\n      });\n\n      return createdObject;\n    } else {\n      throw new Error('Failed to create object');\n    }\n  }\n\n  /**\n   * Updates the annotation modified timestamp for a target domain object\n   * @param {DomainObject} targetDomainObject The target domain object to update\n   */\n  #updateAnnotationModified(targetDomainObject) {\n    // As certain telemetry objects are immutable, we'll need to check here first\n    // to see if we can add the annotation last created property.\n    // TODO: This should be removed once we have a better way to handle immutable telemetry objects\n    if (targetDomainObject.isMutable) {\n      this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());\n    } else {\n      this.emit('targetDomainObjectAnnotated', targetDomainObject);\n    }\n  }\n\n  /**\n   * Defines a new tag\n   * @param {string} tagKey a unique identifier for the tag\n   * @param {Tag} tagsDefinition the definition of the tag to add\n   */\n  defineTag(tagKey, tagsDefinition) {\n    this.availableTags[tagKey] = tagsDefinition;\n  }\n\n  /**\n   * Sets the namespace to save new annotations to\n   * @param {string} namespace the namespace to save new annotations to\n   */\n  setNamespaceToSaveAnnotations(namespace) {\n    this.namespaceToSaveAnnotations = namespace;\n  }\n\n  /**\n   * Checks if a domain object is an annotation\n   * @param {DomainObject} domainObject the domainObject in question\n   * @returns {boolean} Returns true if the domain object is an annotation\n   */\n  isAnnotation(domainObject) {\n    return domainObject && domainObject.type === ANNOTATION_TYPE;\n  }\n\n  /**\n   * Gets the available tags that have been loaded\n   * @returns {Tag[]} Returns an array of the available tags that have been loaded\n   */\n  getAvailableTags() {\n    if (this.availableTags) {\n      const rearrangedToArray = Object.keys(this.availableTags).map((tagKey) => {\n        return {\n          id: tagKey,\n          ...this.availableTags[tagKey]\n        };\n      });\n\n      return rearrangedToArray;\n    } else {\n      return [];\n    }\n  }\n\n  /**\n   * Gets annotations for a given domain object identifier\n   * @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.\n   * @param {AbortSignal} [abortSignal] - An abort signal to cancel the search\n   * @returns {Promise<DomainObject[]>} Returns a promise that resolves to an array of annotations that match the search query\n   */\n  async getAnnotations(domainObjectIdentifier, abortSignal = null) {\n    const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);\n    const searchResults = (\n      await Promise.all(\n        this.openmct.objects.search(\n          keyStringQuery,\n          abortSignal,\n          this.openmct.objects.SEARCH_TYPES.ANNOTATIONS\n        )\n      )\n    ).flat();\n\n    return searchResults;\n  }\n\n  /**\n   * Deletes (marks as deleted) the given annotations\n   * @param {DomainObject[]} annotations - An array of annotations to delete (set _deleted to true)\n   */\n  deleteAnnotations(annotations) {\n    if (!annotations) {\n      throw new Error('Asked to delete null annotations! 🙅‍♂️');\n    }\n\n    annotations.forEach((annotation) => {\n      if (!annotation._deleted) {\n        this.openmct.objects.mutate(annotation, '_deleted', true);\n      }\n    });\n  }\n\n  /**\n   * Undeletes (marks as not deleted) the given annotation\n   * @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)\n   */\n  unDeleteAnnotation(annotation) {\n    if (!annotation) {\n      throw new Error('Asked to undelete null annotation! 🙅‍♂️');\n    }\n\n    this.openmct.objects.mutate(annotation, '_deleted', false);\n  }\n\n  /**\n   * Gets tags from the given annotations\n   * @param {DomainObject[]} annotations - The annotations to get tags from\n   * @param {boolean} [filterDuplicates=true] - Whether to filter out duplicate tags\n   * @returns {Tag[]} An array of tags from the given annotations\n   */\n  getTagsFromAnnotations(annotations, filterDuplicates = true) {\n    if (!annotations) {\n      return [];\n    }\n\n    let tagsFromAnnotations = annotations.flatMap((annotation) => {\n      if (annotation._deleted) {\n        return [];\n      } else {\n        return annotation.tags;\n      }\n    });\n\n    if (filterDuplicates) {\n      tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {\n        return tagArray.indexOf(tag) === index;\n      });\n    }\n\n    const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);\n\n    return fullTagModels;\n  }\n\n  /**\n   * Adds meta information to the given tags\n   * @param {string[]} tags - The tags to add meta information to\n   * @returns {Tag[]} An array of tags with meta information added\n   */\n  #addTagMetaInformationToTags(tags) {\n    // Convert to Set and back to Array to remove duplicates\n    const uniqueTags = [...new Set(tags)];\n\n    return uniqueTags.map((tagKey) => {\n      const tagModel = this.availableTags[tagKey];\n      tagModel.tagID = tagKey;\n\n      return tagModel;\n    });\n  }\n\n  /**\n   * Gets tags that match the given query\n   * @param {string} query - The query to match tags against\n   * @returns {string[]} An array of tag keys that match the query\n   */\n  #getMatchingTags(query) {\n    if (!query) {\n      return [];\n    }\n\n    const matchingTags = Object.keys(this.availableTags).filter((tagKey) => {\n      if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {\n        return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());\n      }\n\n      return false;\n    });\n\n    return matchingTags;\n  }\n\n  /**\n   * @typedef {Object} AnnotationTarget\n   * @property {string} keyString - The key string of the target\n   * @property {*} [additionalProperties] - Additional properties depending on the annotation type\n   */\n\n  /**\n   * @typedef {Object} TargetModel\n   * @property {import('openmct').DomainObject[]} originalPath - The original path of the target object\n   * @property {*} [additionalProperties] - Additional properties of the target domain object\n   */\n\n  /**\n   * @typedef {Object} AnnotationResult\n   * @property {string} name - The name of the annotation\n   * @property {string} type - The type of the object (always 'annotation')\n   * @property {{key: string, namespace: string}} identifier - The identifier of the annotation\n   * @property {string[]} tags - Array of tag keys associated with the annotation\n   * @property {boolean} _deleted - Whether the annotation is marked as deleted\n   * @property {ANNOTATION_TYPES} annotationType - The type of the annotation\n   * @property {string} contentText - The text content of the annotation\n   * @property {string} originalContextPath - The original context path of the annotation\n   * @property {AnnotationTarget[]} targets - Array of targets for the annotation\n   * @property {Tag[]} fullTagModels - Full tag models including metadata\n   * @property {string[]} matchingTagKeys - Array of tag keys that matched the search query\n   * @property {TargetModel[]} targetModels - Array of target models with additional information\n   */\n\n  /**\n   * Combines annotations with the same targets\n   * @param {AnnotationResult[]} results - The results to combine\n   * @returns {AnnotationResult[]} The combined results\n   */\n  #combineSameTargets(results) {\n    const combinedResults = [];\n    results.forEach((currentAnnotation) => {\n      const existingAnnotation = combinedResults.find((annotationToFind) => {\n        const { annotationType, targets } = currentAnnotation;\n        return this.areAnnotationTargetsEqual(annotationType, targets, annotationToFind.targets);\n      });\n      if (!existingAnnotation) {\n        combinedResults.push(currentAnnotation);\n      } else {\n        existingAnnotation.tags.push(...currentAnnotation.tags);\n      }\n    });\n\n    return combinedResults;\n  }\n\n  /**\n   * Breaks apart annotations with multiple targets into separate results\n   * @param {AnnotationResult[]} results - The results to break apart\n   * @returns {AnnotationResult[]} The separated results\n   */\n  #breakApartSeparateTargets(results) {\n    const separateResults = [];\n    results.forEach((result) => {\n      result.targets.forEach((target) => {\n        const targetID = target.keyString;\n        const separatedResult = {\n          ...result\n        };\n        separatedResult.targets = [target];\n        separatedResult.targetModels = result.targetModels.filter((targetModel) => {\n          const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);\n\n          return targetKeyString === targetID;\n        });\n        separateResults.push(separatedResult);\n      });\n    });\n\n    return separateResults;\n  }\n\n  /**\n   * Adds tag meta information to the given results\n   * @param {AnnotationResult[]} results - The results to add tag meta information to\n   * @param {string[]} matchingTagKeys - The matching tag keys\n   * @returns {AnnotationResult[]} The results with tag meta information added\n   */\n  #addTagMetaInformationToResults(results, matchingTagKeys) {\n    const tagsAddedToResults = results.map((result) => {\n      const fullTagModels = this.#addTagMetaInformationToTags(result.tags);\n\n      return {\n        fullTagModels,\n        matchingTagKeys,\n        ...result\n      };\n    });\n\n    return tagsAddedToResults;\n  }\n\n  /**\n   * Adds target models to the results\n   * @param {AnnotationResult[]} results - The results to add target models to\n   * @param {AbortSignal} abortSignal - The abort signal\n   * @returns {Promise<AnnotationResult[]>} The results with target models added\n   */\n  async #addTargetModelsToResults(results, abortSignal) {\n    const modelAddedToResults = await Promise.all(\n      results.map(async (result) => {\n        const targetModels = await Promise.all(\n          result.targets.map(async (target) => {\n            const targetID = target.keyString;\n            const targetModel = await this.openmct.objects.get(targetID, abortSignal);\n            const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);\n            const originalPathObjects = await this.openmct.objects.getOriginalPath(\n              targetKeyString,\n              [],\n              abortSignal\n            );\n\n            return {\n              originalPath: originalPathObjects,\n              ...targetModel\n            };\n          })\n        );\n\n        return {\n          targetModels,\n          ...result\n        };\n      })\n    );\n\n    return modelAddedToResults;\n  }\n\n  /**\n   * Searches for tags matching the given query\n   * @param {string} query - A query to match against tags\n   * @param {AbortSignal} [abortSignal] - An optional abort signal to stop the query\n   * @returns {Promise<AnnotationResult[]>} A promise that resolves to an array of matching annotation results\n   */\n  async searchForTags(query, abortSignal) {\n    const matchingTagKeys = this.#getMatchingTags(query);\n    if (!matchingTagKeys.length) {\n      return [];\n    }\n\n    const searchResults = (\n      await Promise.all(\n        this.openmct.objects.search(\n          matchingTagKeys,\n          abortSignal,\n          this.openmct.objects.SEARCH_TYPES.TAGS\n        )\n      )\n    ).flat();\n    const filteredDeletedResults = searchResults.filter((result) => {\n      return !result._deleted;\n    });\n    const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);\n    const appliedTagSearchResults = this.#addTagMetaInformationToResults(\n      combinedSameTargets,\n      matchingTagKeys\n    );\n    const appliedTargetsModels = await this.#addTargetModelsToResults(\n      appliedTagSearchResults,\n      abortSignal\n    );\n    const resultsWithValidPath = appliedTargetsModels.filter((result) => {\n      return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);\n    });\n    const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);\n\n    return breakApartSeparateTargets;\n  }\n\n  /**\n   * Adds a comparator function for a given annotation type.\n   * The comparator functions will be used to determine if two annotations\n   * have the same target.\n   * @param {ANNOTATION_TYPES} annotationType\n   * @param {(t1, t2) => boolean} comparator\n   */\n  addTargetComparator(annotationType, comparator) {\n    const comparatorList = this.#targetComparatorMap.get(annotationType) ?? [];\n    comparatorList.push(comparator);\n    this.#targetComparatorMap.set(annotationType, comparatorList);\n  }\n\n  /**\n   * Compare two sets of targets to see if they are equal. First checks if\n   * any targets comparators evaluate to true, then falls back to a deep\n   * equality check.\n   * @param {ANNOTATION_TYPES} annotationType\n   * @param {*} targets\n   * @param {*} otherTargets\n   * @returns true if the targets are equal, false otherwise\n   */\n  areAnnotationTargetsEqual(annotationType, targets, otherTargets) {\n    const targetComparatorList = this.#targetComparatorMap.get(annotationType);\n    return (\n      (targetComparatorList?.length &&\n        targetComparatorList.some((targetComparator) => targetComparator(targets, otherTargets))) ||\n      _.isEqual(targets, otherTargets)\n    );\n  }\n\n  /**\n   * Checks if the given type is annotatable\n   * @param {string} type The type to check\n   * @returns {boolean} Returns true if the type is annotatable\n   */\n  isAnnotatableType(type) {\n    const types = this.openmct.types.getAllTypes();\n\n    return types[type]?.definition?.annotatable;\n  }\n}\n"
  },
  {
    "path": "src/api/annotation/AnnotationAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ExampleTagsPlugin from '../../../example/exampleTags/plugin.js';\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\n\ndescribe('The Annotation API', () => {\n  let openmct;\n  let mockObjectProvider;\n  let mockImmutableObjectProvider;\n  let mockDomainObject;\n  let mockFolderObject;\n  let mockAnnotationObject;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(new ExampleTagsPlugin());\n    const availableTags = openmct.annotation.getAvailableTags();\n    mockFolderObject = {\n      type: 'root',\n      name: 'folderFoo',\n      location: '',\n      identifier: {\n        key: 'someParent',\n        namespace: 'fooNameSpace'\n      }\n    };\n    mockDomainObject = {\n      type: 'notebook',\n      name: 'fooRabbitNotebook',\n      location: 'fooNameSpace:someParent',\n      identifier: {\n        key: 'some-object',\n        namespace: 'fooNameSpace'\n      }\n    };\n    mockAnnotationObject = {\n      type: 'annotation',\n      name: 'Some Notebook Annotation',\n      annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n      tags: [availableTags[0].id, availableTags[1].id],\n      identifier: {\n        key: 'anAnnotationKey',\n        namespace: 'fooNameSpace'\n      },\n      targets: [\n        {\n          keyString: 'fooNameSpace:some-object',\n          entryId: 'fooBarEntry'\n        }\n      ]\n    };\n\n    mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);\n    // eslint-disable-next-line require-await\n    mockObjectProvider.get = async (identifier) => {\n      if (identifier.key === mockDomainObject.identifier.key) {\n        return mockDomainObject;\n      } else if (identifier.key === mockAnnotationObject.identifier.key) {\n        return mockAnnotationObject;\n      } else if (identifier.key === mockFolderObject.identifier.key) {\n        return mockFolderObject;\n      } else {\n        return null;\n      }\n    };\n\n    mockObjectProvider.create.and.returnValue(Promise.resolve(true));\n    mockObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n    mockImmutableObjectProvider = jasmine.createSpyObj('mock immutable provider', ['get']);\n    // eslint-disable-next-line require-await\n    mockImmutableObjectProvider.get = async (identifier) => {\n      if (identifier.key === mockDomainObject.identifier.key) {\n        return mockDomainObject;\n      } else if (identifier.key === mockAnnotationObject.identifier.key) {\n        return mockAnnotationObject;\n      } else if (identifier.key === mockFolderObject.identifier.key) {\n        return mockFolderObject;\n      } else {\n        return null;\n      }\n    };\n\n    openmct.objects.addProvider('immutableProvider', mockImmutableObjectProvider);\n    openmct.objects.addProvider('fooNameSpace', mockObjectProvider);\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n  afterEach(async () => {\n    await resetApplicationState(openmct);\n  });\n  it('is defined', () => {\n    expect(openmct.annotation).toBeDefined();\n  });\n\n  describe('Creation', () => {\n    it('can create annotations', async () => {\n      const annotationCreationArguments = {\n        name: 'Test Annotation',\n        domainObject: mockDomainObject,\n        annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n        tags: ['sometag'],\n        contentText: 'fooContext',\n        targetDomainObjects: [mockDomainObject],\n        targets: [{ keyString: 'fooTarget' }]\n      };\n      const annotationObject = await openmct.annotation.create(annotationCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(annotationObject.type).toEqual('annotation');\n    });\n    it('can create annotations if domain object is immutable', async () => {\n      mockDomainObject.identifier.namespace = 'immutableProvider';\n      const annotationCreationArguments = {\n        name: 'Test Annotation',\n        domainObject: mockDomainObject,\n        annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n        tags: ['sometag'],\n        contentText: 'fooContext',\n        targetDomainObjects: [mockDomainObject],\n        targets: [{ keyString: 'fooTarget' }]\n      };\n      openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');\n      const annotationObject = await openmct.annotation.create(annotationCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(annotationObject.type).toEqual('annotation');\n    });\n    it('fails if annotation is an unknown type', async () => {\n      try {\n        await openmct.annotation.create(\n          'Garbage Annotation',\n          mockDomainObject,\n          'garbageAnnotation',\n          ['sometag'],\n          'fooContext',\n          { fooTarget: {} }\n        );\n      } catch (error) {\n        expect(error).toBeDefined();\n      }\n    });\n    it('fails if annotation if given an immutable namespace to save to', async () => {\n      try {\n        const annotationCreationArguments = {\n          name: 'Test Annotation',\n          domainObject: mockDomainObject,\n          annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n          tags: ['sometag'],\n          contentText: 'fooContext',\n          targetDomainObjects: [mockDomainObject],\n          targets: [{ keyString: 'fooTarget' }]\n        };\n        openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist');\n        await openmct.annotation.create(annotationCreationArguments);\n      } catch (error) {\n        expect(error).toBeDefined();\n      }\n    });\n    it('fails if annotation if given an undefined namespace to save to', async () => {\n      try {\n        const annotationCreationArguments = {\n          name: 'Test Annotation',\n          domainObject: mockDomainObject,\n          annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n          tags: ['sometag'],\n          contentText: 'fooContext',\n          targetDomainObjects: [mockDomainObject],\n          targets: [{ keyString: 'fooTarget' }]\n        };\n        openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');\n        await openmct.annotation.create(annotationCreationArguments);\n      } catch (error) {\n        expect(error).toBeDefined();\n      }\n    });\n  });\n\n  describe('Tagging', () => {\n    let tagCreationArguments;\n    beforeEach(() => {\n      tagCreationArguments = {\n        name: 'Test Annotation',\n        domainObject: mockDomainObject,\n        annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n        tags: ['aWonderfulTag'],\n        contentText: 'fooContext',\n        targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],\n        targetDomainObjects: [mockDomainObject]\n      };\n    });\n    it('can create a tag', async () => {\n      const annotationObject = await openmct.annotation.create(tagCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(annotationObject.type).toEqual('annotation');\n      expect(annotationObject.tags).toContain('aWonderfulTag');\n    });\n    it('can delete a tag', async () => {\n      const annotationObject = await openmct.annotation.create(tagCreationArguments);\n      expect(annotationObject).toBeDefined();\n      openmct.annotation.deleteAnnotations([annotationObject]);\n      expect(annotationObject._deleted).toBeTrue();\n    });\n    it('can remove all tags', async () => {\n      const annotationObject = await openmct.annotation.create(tagCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(() => {\n        openmct.annotation.deleteAnnotations([annotationObject]);\n      }).not.toThrow();\n      expect(annotationObject._deleted).toBeTrue();\n    });\n    it('can add/delete/add a tag', async () => {\n      let annotationObject = await openmct.annotation.create(tagCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(annotationObject.type).toEqual('annotation');\n      expect(annotationObject.tags).toContain('aWonderfulTag');\n      openmct.annotation.deleteAnnotations([annotationObject]);\n      expect(annotationObject._deleted).toBeTrue();\n      annotationObject = await openmct.annotation.create(tagCreationArguments);\n      expect(annotationObject).toBeDefined();\n      expect(annotationObject.type).toEqual('annotation');\n      expect(annotationObject.tags).toContain('aWonderfulTag');\n      expect(annotationObject._deleted).toBeFalse();\n    });\n  });\n\n  describe('Search', () => {\n    let sharedWorkerToRestore;\n    beforeEach(async () => {\n      // use local worker\n      sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;\n      openmct.objects.inMemorySearchProvider.worker = null;\n      await openmct.objects.inMemorySearchProvider.index(mockFolderObject);\n      await openmct.objects.inMemorySearchProvider.index(mockDomainObject);\n      await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);\n    });\n    afterEach(() => {\n      openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;\n    });\n    it('can search for tags', async () => {\n      const results = await openmct.annotation.searchForTags('S');\n      expect(results).toBeDefined();\n      expect(results.length).toEqual(1);\n    });\n    it('returns no tags for empty search', async () => {\n      const results = await openmct.annotation.searchForTags('q');\n      expect(results).toBeDefined();\n      expect(results.length).toEqual(0);\n    });\n  });\n\n  describe('Target Comparators', () => {\n    let targets;\n    let otherTargets;\n    let comparator;\n\n    beforeEach(() => {\n      targets = [\n        {\n          keyString: 'fooTarget',\n          foo: 42\n        }\n      ];\n      otherTargets = [\n        {\n          keyString: 'fooTarget',\n          bar: 42\n        }\n      ];\n      comparator = (t1, t2) => t1[0].foo === t2[0].bar;\n    });\n\n    it('can add a comparator function', () => {\n      const notebookAnnotationType = openmct.annotation.ANNOTATION_TYPES.NOTEBOOK;\n      expect(\n        openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)\n      ).toBeFalse(); // without a comparator, these should NOT be equal\n      // Register a comparator function for the notebook annotation type\n      openmct.annotation.addTargetComparator(notebookAnnotationType, comparator);\n      expect(\n        openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)\n      ).toBeTrue(); // the comparator should make these equal\n    });\n\n    it('falls back to deep equality check if no comparator functions', () => {\n      const annotationTypeWithoutComparator = openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL;\n      const areEqual = openmct.annotation.areAnnotationTargetsEqual(\n        annotationTypeWithoutComparator,\n        targets,\n        targets\n      );\n      const areNotEqual = openmct.annotation.areAnnotationTargetsEqual(\n        annotationTypeWithoutComparator,\n        targets,\n        otherTargets\n      );\n      expect(areEqual).toBeTrue();\n      expect(areNotEqual).toBeFalse();\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/api.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ActionsAPI from './actions/ActionsAPI.js';\nimport AnnotationAPI from './annotation/AnnotationAPI.js';\nimport CompositionAPI from './composition/CompositionAPI.js';\nimport EditorAPI from './Editor.js';\nimport FaultManagementAPI from './faultmanagement/FaultManagementAPI.js';\nimport FormsAPI from './forms/FormsAPI.js';\nimport IndicatorAPI from './indicators/IndicatorAPI.js';\nimport MenuAPI from './menu/MenuAPI.js';\nimport NotificationAPI from './notifications/NotificationAPI.js';\nimport ObjectAPI from './objects/ObjectAPI.js';\nimport PriorityAPI from './priority/PriorityAPI.js';\nimport StatusAPI from './status/StatusAPI.js';\nimport TelemetryAPI from './telemetry/TelemetryAPI.js';\nimport TimeAPI from './time/TimeAPI.js';\nimport TypeRegistry from './types/TypeRegistry.js';\nimport UserAPI from './user/UserAPI.js';\n\nexport default {\n  ActionsAPI,\n  CompositionAPI,\n  EditorAPI,\n  FaultManagementAPI,\n  FormsAPI,\n  IndicatorAPI,\n  MenuAPI,\n  NotificationAPI,\n  ObjectAPI,\n  PriorityAPI,\n  StatusAPI,\n  TelemetryAPI,\n  TimeAPI,\n  TypeRegistry,\n  UserAPI,\n  AnnotationAPI\n};\n"
  },
  {
    "path": "src/api/composition/CompositionAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport CompositionCollection from './CompositionCollection.js';\nimport DefaultCompositionProvider from './DefaultCompositionProvider.js';\n\n/**\n * @typedef {import('./CompositionProvider').default} CompositionProvider\n */\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('../../../openmct').OpenMCT} OpenMCT\n */\n\n/**\n * An interface for interacting with the composition of domain objects.\n * The composition of a domain object is the list of other domain objects\n * it \"contains\" (for instance, that should be displayed beneath it\n * in the tree.)\n * @constructor\n */\nexport default class CompositionAPI {\n  /**\n   * @param {OpenMCT} publicAPI\n   */\n  constructor(publicAPI) {\n    /** @type {CompositionProvider[]} */\n    this.registry = [];\n    /** @type {CompositionPolicy[]} */\n    this.policies = [];\n    this.addProvider(new DefaultCompositionProvider(publicAPI, this));\n    /** @type {OpenMCT} */\n    this.publicAPI = publicAPI;\n  }\n  /**\n   * Add a composition provider.\n   *\n   * Plugins can add new composition providers to change the loading\n   * behavior for certain domain objects.\n   *\n   * @method addProvider\n   * @param {CompositionProvider} provider the provider to add\n   */\n  addProvider(provider) {\n    this.registry.unshift(provider);\n  }\n  /**\n   * Retrieve the composition (if any) of this domain object.\n   *\n   * @method get\n   * @param {DomainObject} domainObject\n   * @returns {CompositionCollection | undefined}\n   */\n  get(domainObject) {\n    const provider = this.registry.find((p) => {\n      return p.appliesTo(domainObject);\n    });\n\n    if (!provider) {\n      return;\n    }\n\n    return new CompositionCollection(domainObject, provider, this.publicAPI);\n  }\n  /**\n   * A composition policy is a function which either allows or disallows\n   * placing one object in another's composition.\n   *\n   * Open MCT's policy model requires consensus, so any one policy may\n   * reject composition by returning false. As such, policies should\n   * generally be written to return true in the default case.\n   *\n   * @callback CompositionPolicy\n   * @param {DomainObject} containingObject the object which\n   *        would act as a container\n   * @param {DomainObject} containedObject the object which\n   *        would be contained\n   * @returns {boolean} false if this composition should be disallowed\n   */\n  /**\n   * Add a composition policy. Composition policies may disallow domain\n   * objects from containing other domain objects.\n   *\n   * @method addPolicy\n   * @param {CompositionPolicy} policy\n   *        the policy to add\n   */\n  addPolicy(policy) {\n    this.policies.push(policy);\n  }\n  /**\n   * Check whether or not a domain object is allowed to contain another\n   * domain object.\n   *\n   * @private\n   * @method checkPolicy\n   * @param {DomainObject} container the object which\n   *        would act as a container\n   * @param {DomainObject} containee the object which\n   *        would be contained\n   * @returns {boolean} false if this composition should be disallowed\n   * @param {CompositionPolicy} policy\n   *        the policy to add\n   */\n  checkPolicy(container, containee) {\n    return this.policies.every(function (policy) {\n      return policy(container, containee);\n    });\n  }\n\n  /**\n   * Check whether or not a domainObject supports composition\n   *\n   * @param {DomainObject} domainObject\n   * @returns {boolean} true if the domainObject supports composition\n   */\n  supportsComposition(domainObject) {\n    return this.get(domainObject) !== undefined;\n  }\n}\n"
  },
  {
    "path": "src/api/composition/CompositionAPISpec.js",
    "content": "import { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport CompositionCollection from './CompositionCollection.js';\n\ndescribe('The Composition API', function () {\n  let publicAPI;\n  let compositionAPI;\n\n  beforeEach(function (done) {\n    publicAPI = createOpenMct();\n    compositionAPI = publicAPI.composition;\n\n    const mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);\n\n    mockObjectProvider.create.and.returnValue(Promise.resolve(true));\n    mockObjectProvider.update.and.returnValue(Promise.resolve(true));\n    mockObjectProvider.get.and.callFake((identifier) => {\n      return Promise.resolve({ identifier });\n    });\n\n    publicAPI.objects.addProvider('test', mockObjectProvider);\n    publicAPI.objects.addProvider('custom', mockObjectProvider);\n\n    publicAPI.on('start', done);\n    publicAPI.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(publicAPI);\n  });\n\n  it('returns falsy if an object does not support composition', function () {\n    expect(compositionAPI.get({})).toBeFalsy();\n  });\n\n  describe('default composition', function () {\n    let domainObject;\n    let composition;\n\n    beforeEach(function () {\n      domainObject = {\n        name: 'test folder',\n        identifier: {\n          namespace: 'test',\n          key: '1'\n        },\n        composition: [\n          {\n            namespace: 'test',\n            key: 'a'\n          },\n          {\n            namespace: 'test',\n            key: 'b'\n          },\n          {\n            namespace: 'test',\n            key: 'c'\n          }\n        ]\n      };\n      composition = compositionAPI.get(domainObject);\n    });\n\n    it('returns composition collection', function () {\n      expect(composition).toBeDefined();\n      expect(composition).toEqual(jasmine.any(CompositionCollection));\n    });\n\n    it('correctly reflects composability', function () {\n      expect(compositionAPI.supportsComposition(domainObject)).toBe(true);\n      delete domainObject.composition;\n      expect(compositionAPI.supportsComposition(domainObject)).toBe(false);\n    });\n\n    it('loads composition from domain object', function () {\n      const listener = jasmine.createSpy('addListener');\n      composition.on('add', listener);\n\n      return composition.load().then(function () {\n        expect(listener.calls.count()).toBe(3);\n        expect(listener).toHaveBeenCalledWith({\n          identifier: {\n            namespace: 'test',\n            key: 'a'\n          }\n        });\n      });\n    });\n    describe('supports reordering of composition', function () {\n      let listener;\n      beforeEach(function () {\n        listener = jasmine.createSpy('reorderListener');\n        spyOn(publicAPI.objects, 'mutate');\n        publicAPI.objects.mutate.and.callThrough();\n\n        composition.on('reorder', listener);\n\n        return composition.load();\n      });\n      it('', function () {\n        composition.reorder(1, 0);\n        let newComposition = publicAPI.objects.mutate.calls.mostRecent().args[2];\n        let reorderPlan = listener.calls.mostRecent().args[0][0];\n\n        expect(reorderPlan.oldIndex).toBe(1);\n        expect(reorderPlan.newIndex).toBe(0);\n        expect(newComposition[0].key).toEqual('b');\n        expect(newComposition[1].key).toEqual('a');\n        expect(newComposition[2].key).toEqual('c');\n      });\n      it('', function () {\n        composition.reorder(0, 2);\n        let newComposition = publicAPI.objects.mutate.calls.mostRecent().args[2];\n        let reorderPlan = listener.calls.mostRecent().args[0][0];\n\n        expect(reorderPlan.oldIndex).toBe(0);\n        expect(reorderPlan.newIndex).toBe(2);\n        expect(newComposition[0].key).toEqual('b');\n        expect(newComposition[1].key).toEqual('c');\n        expect(newComposition[2].key).toEqual('a');\n      });\n    });\n    it('supports adding an object to composition', function () {\n      let mockChildObject = {\n        identifier: {\n          key: 'mock-key',\n          namespace: ''\n        }\n      };\n\n      return new Promise((resolve) => {\n        composition.on('add', resolve);\n        composition.add(mockChildObject);\n      }).then(() => {\n        expect(domainObject.composition.length).toBe(4);\n        expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);\n      });\n    });\n  });\n\n  describe('static custom composition', function () {\n    let customProvider;\n    let domainObject;\n    let composition;\n\n    beforeEach(function () {\n      // A simple custom provider, returns the same composition for\n      // all objects of a given type.\n      customProvider = {\n        appliesTo: function (object) {\n          return object.type === 'custom-object-type';\n        },\n        load: function (object) {\n          return Promise.resolve([\n            {\n              namespace: 'custom',\n              key: 'thing'\n            }\n          ]);\n        },\n        add: jasmine.createSpy('add'),\n        remove: jasmine.createSpy('remove')\n      };\n      domainObject = {\n        identifier: {\n          namespace: 'test',\n          key: '1'\n        },\n        type: 'custom-object-type'\n      };\n      compositionAPI.addProvider(customProvider);\n      composition = compositionAPI.get(domainObject);\n    });\n\n    it('supports listening and loading', function () {\n      const addListener = jasmine.createSpy('addListener');\n      composition.on('add', addListener);\n\n      return composition.load().then(function (children) {\n        let listenObject;\n        const loadedObject = children[0];\n\n        expect(addListener).toHaveBeenCalled();\n\n        listenObject = addListener.calls.mostRecent().args[0];\n        expect(listenObject).toEqual(loadedObject);\n        expect(loadedObject).toEqual({\n          identifier: {\n            namespace: 'custom',\n            key: 'thing'\n          }\n        });\n      });\n    });\n    describe('Calling add or remove', function () {\n      let mockChildObject;\n\n      beforeEach(function () {\n        mockChildObject = {\n          identifier: {\n            key: 'mock-key',\n            namespace: ''\n          }\n        };\n        composition.add(mockChildObject);\n      });\n\n      it('calls add on the provider', function () {\n        expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);\n      });\n\n      it('calls remove on the provider', function () {\n        composition.remove(mockChildObject);\n        expect(customProvider.remove).toHaveBeenCalledWith(\n          domainObject,\n          mockChildObject.identifier\n        );\n      });\n    });\n  });\n\n  describe('dynamic custom composition', function () {\n    let customProvider;\n    let domainObject;\n    let composition;\n\n    beforeEach(function () {\n      // A dynamic provider, loads an empty composition and exposes\n      // listener functions.\n      customProvider = jasmine.createSpyObj('dynamicProvider', ['appliesTo', 'load', 'on', 'off']);\n\n      customProvider.appliesTo.and.returnValue('true');\n      customProvider.load.and.returnValue(Promise.resolve([]));\n\n      domainObject = {\n        identifier: {\n          namespace: 'test',\n          key: '1'\n        },\n        type: 'custom-object-type'\n      };\n      compositionAPI.addProvider(customProvider);\n      composition = compositionAPI.get(domainObject);\n    });\n\n    it('supports listening and loading', function () {\n      const addListener = jasmine.createSpy('addListener');\n      const removeListener = jasmine.createSpy('removeListener');\n      const addPromise = new Promise(function (resolve) {\n        addListener.and.callFake(resolve);\n      });\n      const removePromise = new Promise(function (resolve) {\n        removeListener.and.callFake(resolve);\n      });\n\n      composition.on('add', addListener);\n      composition.on('remove', removeListener);\n\n      expect(customProvider.on).toHaveBeenCalledWith(\n        domainObject,\n        'add',\n        jasmine.any(Function),\n        jasmine.any(CompositionCollection)\n      );\n      expect(customProvider.on).toHaveBeenCalledWith(\n        domainObject,\n        'remove',\n        jasmine.any(Function),\n        jasmine.any(CompositionCollection)\n      );\n      const add = customProvider.on.calls.all()[0].args[2];\n      const remove = customProvider.on.calls.all()[1].args[2];\n\n      return composition\n        .load()\n        .then(function () {\n          expect(addListener).not.toHaveBeenCalled();\n          expect(removeListener).not.toHaveBeenCalled();\n          add({\n            namespace: 'custom',\n            key: 'thing'\n          });\n\n          return addPromise;\n        })\n        .then(function () {\n          expect(addListener).toHaveBeenCalledWith({\n            identifier: {\n              namespace: 'custom',\n              key: 'thing'\n            }\n          });\n          remove(addListener.calls.mostRecent().args[0]);\n\n          return removePromise;\n        })\n        .then(function () {\n          expect(removeListener).toHaveBeenCalledWith({\n            identifier: {\n              namespace: 'custom',\n              key: 'thing'\n            }\n          });\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/composition/CompositionCollection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { isIdentifier } from '../objects/object-utils';\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('./CompositionAPI').default} CompositionAPI\n */\n\n/**\n * @typedef {import('../../../openmct').OpenMCT} OpenMCT\n */\n\n/**\n * @typedef {Object} ListenerMap\n * @property {Array.<any>} add\n * @property {Array.<any>} remove\n * @property {Array.<any>} load\n * @property {Array.<any>} reorder\n */\n\n/**\n * A CompositionCollection represents the list of domain objects contained\n * by another domain object. It provides methods for loading this\n * list asynchronously, modifying this list, and listening for changes to\n * this list.\n *\n * Usage:\n * ```javascript\n *  var myViewComposition = MCT.composition.get(myViewObject);\n *  myViewComposition.on('add', addObjectToView);\n *  myViewComposition.on('remove', removeObjectFromView);\n *  myViewComposition.load(); // will trigger `add` for all loaded objects.\n *  ```\n */\nexport default class CompositionCollection {\n  domainObject;\n  #provider;\n  #publicAPI;\n  #listeners;\n  #mutables;\n  /**\n   * @constructor\n   * @param {DomainObject} domainObject the domain object\n   *        whose composition will be contained\n   * @param {import('./CompositionProvider').default} provider the provider\n   *        to use to retrieve other domain objects\n   * @param {OpenMCT} publicAPI the composition API, for\n   *        policy checks\n   */\n  constructor(domainObject, provider, publicAPI) {\n    this.domainObject = domainObject;\n    /** @type {import('./CompositionProvider').default} */\n    this.#provider = provider;\n    /** @type {OpenMCT} */\n    this.#publicAPI = publicAPI;\n    /** @type {ListenerMap} */\n    this.#listeners = {\n      add: [],\n      remove: [],\n      load: [],\n      reorder: []\n    };\n    this.onProviderAdd = this.#onProviderAdd.bind(this);\n    this.onProviderRemove = this.#onProviderRemove.bind(this);\n    this.#mutables = {};\n\n    if (this.domainObject.isMutable) {\n      this.returnMutables = true;\n      let unobserve = this.domainObject.$on('$_destroy', () => {\n        Object.values(this.#mutables).forEach((mutable) => {\n          this.#publicAPI.objects.destroyMutable(mutable);\n        });\n        unobserve();\n      });\n    }\n  }\n  /**\n   * Listen for changes to this composition.  Supports 'add', 'remove', and\n   * 'load' events.\n   *\n   * @param {string} event event to listen for, either 'add', 'remove' or 'load'.\n   * @param {(...args: any[]) => void} callback to trigger when event occurs.\n   * @param {any} [context] to use when invoking callback, optional.\n   */\n  on(event, callback, context) {\n    if (!this.#listeners[event]) {\n      throw new Error('Event not supported by composition: ' + event);\n    }\n\n    if (this.#provider.on && this.#provider.off) {\n      if (event === 'add') {\n        this.#provider.on(this.domainObject, 'add', this.onProviderAdd, this);\n      }\n\n      if (event === 'remove') {\n        this.#provider.on(this.domainObject, 'remove', this.onProviderRemove, this);\n      }\n\n      if (event === 'reorder') {\n        this.#provider.on(this.domainObject, 'reorder', this.#onProviderReorder, this);\n      }\n    }\n\n    this.#listeners[event].push({\n      callback: callback,\n      context: context\n    });\n  }\n  /**\n   * Remove a listener.  Must be called with same exact parameters as\n   * `off`.\n   *\n   * @param {string} event\n   * @param {(...args: any[]) => void} callback\n   * @param {any} [context]\n   */\n  off(event, callback, context) {\n    if (!this.#listeners[event]) {\n      throw new Error('Event not supported by composition: ' + event);\n    }\n\n    const index = this.#listeners[event].findIndex((l) => {\n      return l.callback === callback && l.context === context;\n    });\n\n    if (index === -1) {\n      throw new Error('Tried to remove a listener that does not exist');\n    }\n\n    this.#listeners[event].splice(index, 1);\n    if (this.#listeners[event].length === 0) {\n      this._destroy();\n\n      // Remove provider listener if this is the last callback to\n      // be removed.\n      if (this.#provider.off && this.#provider.on) {\n        if (event === 'add') {\n          this.#provider.off(this.domainObject, 'add', this.onProviderAdd, this);\n        } else if (event === 'remove') {\n          this.#provider.off(this.domainObject, 'remove', this.onProviderRemove, this);\n        } else if (event === 'reorder') {\n          this.#provider.off(this.domainObject, 'reorder', this.#onProviderReorder, this);\n        }\n      }\n    }\n  }\n  /**\n   * Add a domain object to this composition.\n   *\n   * A call to [load]{@link module:openmct.CompositionCollection#load}\n   * must have resolved before using this method.\n   *\n   * **TODO:** Remove `skipMutate` parameter.\n   *\n   * @param {DomainObject} child the domain object to add\n   * @param {boolean} skipMutate\n   * **Intended for internal use ONLY.**\n   * true if the underlying provider should not be updated.\n   */\n  add(child, skipMutate) {\n    if (!skipMutate) {\n      if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {\n        throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;\n      }\n\n      this.#provider.add(this.domainObject, child.identifier);\n    } else {\n      if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {\n        let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);\n\n        child = this.#publicAPI.objects.toMutable(child);\n        this.#mutables[keyString] = child;\n      }\n\n      this.#emit('add', child);\n    }\n  }\n  /**\n   * Load the domain objects in this composition.\n   *\n   * @param {AbortSignal} [abortSignal]\n   * @returns {Promise.<Array.<DomainObject>>} a promise for\n   *          the domain objects in this composition\n   * @name load\n   */\n  async load(abortSignal) {\n    this.#cleanUpMutables();\n    const children = await this.#provider.load(this.domainObject);\n    const childObjects = await Promise.all(\n      children.map((child) => {\n        if (isIdentifier(child)) {\n          return this.#publicAPI.objects.get(child, abortSignal);\n        } else {\n          return Promise.resolve(child);\n        }\n      })\n    );\n    childObjects.forEach((child) => this.add(child, true));\n    this.#emit('load');\n\n    return childObjects;\n  }\n  /**\n   * Remove a domain object from this composition.\n   *\n   * A call to [load]{@link module:openmct.CompositionCollection#load}\n   * must have resolved before using this method.\n   *\n   * **TODO:** Remove `skipMutate` parameter.\n   *\n   * @param {DomainObject} child the domain object to remove\n   * @param {boolean} skipMutate\n   * **Intended for internal use ONLY.**\n   * true if the underlying provider should not be updated.\n   * @name remove\n   */\n  remove(child, skipMutate) {\n    if (!skipMutate) {\n      this.#provider.remove(this.domainObject, child.identifier);\n    } else {\n      if (this.returnMutables) {\n        let keyString = this.#publicAPI.objects.makeKeyString(child);\n        if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {\n          this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);\n          delete this.#mutables[keyString];\n        }\n      }\n\n      this.#emit('remove', child);\n    }\n  }\n  /**\n   * Reorder the domain objects in this composition.\n   *\n   * A call to [load]{@link module:openmct.CompositionCollection#load}\n   * must have resolved before using this method.\n   *\n   * @param {number} oldIndex\n   * @param {number} newIndex\n   * @name remove\n   */\n  reorder(oldIndex, newIndex, _skipMutate) {\n    this.#provider.reorder(this.domainObject, oldIndex, newIndex);\n  }\n  /**\n   * Destroy mutationListener\n   */\n  _destroy() {\n    if (this.mutationListener) {\n      this.mutationListener();\n      delete this.mutationListener;\n    }\n  }\n  /**\n   * Handle reorder from provider.\n   * @private\n   * @param {Object} reorderMap\n   */\n  #onProviderReorder(reorderMap) {\n    this.#emit('reorder', reorderMap);\n  }\n\n  /**\n   * Handle adds from provider.\n   * @private\n   * @param {import('openmct').Identifier} childId\n   * @returns {DomainObject}\n   */\n  #onProviderAdd(childId) {\n    return this.#publicAPI.objects.get(childId).then(\n      function (child) {\n        this.add(child, true);\n\n        return child;\n      }.bind(this)\n    );\n  }\n\n  /**\n   * Handle removal from provider.\n   * @param {DomainObject} child\n   */\n  #onProviderRemove(child) {\n    this.remove(child, true);\n  }\n\n  /**\n   * Emit events.\n   *\n   * @private\n   * @param {string} event\n   * @param {...args.<any>} payload\n   */\n  #emit(event, ...payload) {\n    this.#listeners[event].forEach(function (l) {\n      if (l.context) {\n        l.callback.apply(l.context, payload);\n      } else {\n        l.callback(...payload);\n      }\n    });\n  }\n\n  /**\n   * Destroy all mutables.\n   * @private\n   */\n  #cleanUpMutables() {\n    Object.values(this.#mutables).forEach((mutable) => {\n      this.#publicAPI.objects.destroyMutable(mutable);\n    });\n  }\n}\n"
  },
  {
    "path": "src/api/composition/CompositionProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport _ from 'lodash';\n\nimport { makeKeyString, parseKeyString } from '../objects/object-utils.js';\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('openmct').Identifier} Identifier\n */\n\n/**\n * @typedef {import('./CompositionAPI').default} CompositionAPI\n */\n\n/**\n * @typedef {import('openmct').OpenMCT} OpenMCT\n */\n\n/**\n * A CompositionProvider provides the underlying implementation of\n * composition-related behavior for certain types of domain object.\n *\n * By default, a composition provider will not support composition\n * modification.  You can add support for mutation of composition by\n * defining `add` and/or `remove` methods.\n *\n * If the composition of an object can change over time-- perhaps via\n * server updates or mutation via the add/remove methods, then one must\n * trigger events as necessary.\n *\n */\nexport default class CompositionProvider {\n  #publicAPI;\n  #listeningTo;\n\n  /**\n   * @param {OpenMCT} publicAPI\n   * @param {CompositionAPI} compositionAPI\n   */\n  constructor(publicAPI, compositionAPI) {\n    this.#publicAPI = publicAPI;\n    this.#listeningTo = {};\n\n    compositionAPI.addPolicy(this.#cannotContainItself.bind(this));\n    compositionAPI.addPolicy(this.#supportsComposition.bind(this));\n  }\n\n  get listeningTo() {\n    return this.#listeningTo;\n  }\n\n  get establishTopicListener() {\n    return this.#establishTopicListener.bind(this);\n  }\n\n  get publicAPI() {\n    return this.#publicAPI;\n  }\n\n  /**\n   * Check if this provider should be used to load composition for a\n   * particular domain object.\n   * @method appliesTo\n   * @param {DomainObject} domainObject the domain object\n   *        to check\n   * @returns {boolean} true if this provider can provide composition for a given domain object\n   */\n  appliesTo(domainObject) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n  /**\n   * Load any domain objects contained in the composition of this domain\n   * object.\n   * @param {DomainObject} domainObject the domain object\n   *        for which to load composition\n   * @returns {Promise<Identifier[] | DomainObject[]>} a promise for\n   *          the Identifiers or Domain Objects in this composition. If Identifiers are returned,\n   *          they will be automatically resolved to domain objects by the API.\n   */\n  load(domainObject) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n  /**\n   * Attach listeners for changes to the composition of a given domain object.\n   * Supports `add` and `remove` events.\n   *\n   * @param {DomainObject} domainObject to listen to\n   * @param {string} event the event to bind to, either `add` or `remove`.\n   * @param {Function} callback callback to invoke when event is triggered.\n   * @param {any} [context] to use when invoking callback.\n   */\n  on(domainObject, event, callback, context) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n  /**\n   * Remove a listener that was previously added for a given domain object.\n   * event name, callback, and context must be the same as when the listener\n   * was originally attached.\n   *\n   * @param {DomainObject} domainObject to remove listener for\n   * @param {string} event event to stop listening to: `add` or `remove`.\n   * @param {Function} callback callback to remove.\n   * @param {any} context of callback to remove.\n   */\n  off(domainObject, event, callback, context) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n  /**\n   * Remove a domain object from another domain object's composition.\n   *\n   * This method is optional; if not present, adding to a domain object's\n   * composition using this provider will be disallowed.\n   *\n   * @param {DomainObject} domainObject the domain object\n   *        which should have its composition modified\n   * @param {Identifier} childId the domain object to remove\n   */\n  remove(domainObject, childId) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n  /**\n   * Add a domain object to another domain object's composition.\n   *\n   * This method is optional; if not present, adding to a domain object's\n   * composition using this provider will be disallowed.\n   *\n   * @param {DomainObject} parent the domain object\n   *        which should have its composition modified\n   * @param {Identifier} childId the domain object to add\n   */\n  add(parent, childId) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n\n  /**\n   * @param {DomainObject} parent\n   * @param {Identifier} childId\n   * @returns {boolean}\n   */\n  includes(parent, childId) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n\n  /**\n   * @param {DomainObject} domainObject\n   * @param {number} oldIndex\n   * @param {number} newIndex\n   * @returns\n   */\n  reorder(domainObject, oldIndex, newIndex) {\n    throw new Error('This method must be implemented by a subclass.');\n  }\n\n  /**\n   * Listens on general mutation topic, using injector to fetch to avoid\n   * circular dependencies.\n   */\n  #establishTopicListener() {\n    if (this.topicListener) {\n      return;\n    }\n\n    const onMutation = this.#onMutation.bind(this);\n    this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);\n    this.topicListener = () => {\n      this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);\n    };\n  }\n\n  /**\n   * @param {DomainObject} parent\n   * @param {DomainObject} child\n   * @returns {boolean}\n   */\n  #cannotContainItself(parent, child) {\n    return !(\n      parent.identifier.namespace === child.identifier.namespace &&\n      parent.identifier.key === child.identifier.key\n    );\n  }\n\n  /**\n   * @param {DomainObject} parent\n   * @returns {boolean}\n   */\n  #supportsComposition(parent, _child) {\n    return this.#publicAPI.composition.supportsComposition(parent);\n  }\n\n  /**\n   * Handles mutation events.  If there are active listeners for the mutated\n   * object, detects changes to composition and triggers necessary events.\n   *\n   * @param {DomainObject} oldDomainObject\n   */\n  #onMutation(newDomainObject, oldDomainObject) {\n    const id = makeKeyString(oldDomainObject.identifier);\n    const listeners = this.#listeningTo[id];\n\n    if (!listeners) {\n      return;\n    }\n\n    if (oldDomainObject.composition === undefined || newDomainObject.composition === undefined) {\n      return;\n    }\n\n    const oldComposition = oldDomainObject.composition.map(makeKeyString);\n    const newComposition = newDomainObject.composition.map(makeKeyString);\n\n    const added = _.difference(newComposition, oldComposition).map(parseKeyString);\n    const removed = _.difference(oldComposition, newComposition).map(parseKeyString);\n\n    function notify(value) {\n      return function (listener) {\n        if (listener.context) {\n          listener.callback.call(listener.context, value);\n        } else {\n          listener.callback(value);\n        }\n      };\n    }\n\n    added.forEach(function (addedChild) {\n      listeners.add.forEach(notify(addedChild));\n    });\n\n    removed.forEach(function (removedChild) {\n      listeners.remove.forEach(notify(removedChild));\n    });\n  }\n}\n"
  },
  {
    "path": "src/api/composition/DefaultCompositionProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { toRaw } from 'vue';\n\nimport { makeKeyString, parseKeyString } from '../objects/object-utils.js';\nimport CompositionProvider from './CompositionProvider.js';\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('openmct').Identifier} Identifier\n */\n\n/**\n * @typedef {import('./CompositionAPI').default} CompositionAPI\n */\n\n/**\n * @typedef {import('../../../openmct').OpenMCT} OpenMCT\n */\n\n/**\n * A CompositionProvider provides the underlying implementation of\n * composition-related behavior for certain types of domain object.\n *\n * By default, a composition provider will not support composition\n * modification.  You can add support for mutation of composition by\n * defining `add` and/or `remove` methods.\n *\n * If the composition of an object can change over time-- perhaps via\n * server updates or mutation via the add/remove methods, then one must\n * trigger events as necessary.\n * @extends CompositionProvider\n */\nexport default class DefaultCompositionProvider extends CompositionProvider {\n  /**\n   * Check if this provider should be used to load composition for a\n   * particular domain object.\n   * @override\n   * @param {DomainObject} domainObject the domain object\n   *        to check\n   * @returns {boolean} true if this provider can provide composition for a given domain object\n   */\n  appliesTo(domainObject) {\n    return Boolean(domainObject.composition);\n  }\n  /**\n   * Load any domain objects contained in the composition of this domain\n   * object.\n   * @override\n   * @param {DomainObject} domainObject the domain object\n   *        for which to load composition\n   * @returns {Promise<Identifier[]>} a promise for\n   *          the Identifiers in this composition\n   */\n  load(domainObject) {\n    const identifiers = domainObject.composition\n      .filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)\n      .map((idOrKeystring) => parseKeyString(idOrKeystring));\n\n    return Promise.all(identifiers);\n  }\n  /**\n   * Attach listeners for changes to the composition of a given domain object.\n   * Supports `add` and `remove` events.\n   *\n   * @override\n   * @param {DomainObject} domainObject to listen to\n   * @param {string} event the event to bind to, either `add` or `remove`.\n   * @param {Function} callback callback to invoke when event is triggered.\n   * @param {any} [context] to use when invoking callback.\n   */\n  on(domainObject, event, callback, context) {\n    this.establishTopicListener();\n\n    /** @type {string} */\n    const keyString = makeKeyString(domainObject.identifier);\n    let objectListeners = this.listeningTo[keyString];\n\n    if (!objectListeners) {\n      objectListeners = this.listeningTo[keyString] = {\n        add: [],\n        remove: [],\n        reorder: []\n      };\n    }\n\n    objectListeners[event].push({\n      callback: callback,\n      context: context\n    });\n  }\n  /**\n   * Remove a listener that was previously added for a given domain object.\n   * event name, callback, and context must be the same as when the listener\n   * was originally attached.\n   *\n   * @override\n   * @param {DomainObject} domainObject to remove listener for\n   * @param {string} event event to stop listening to: `add` or `remove`.\n   * @param {Function} callback callback to remove.\n   * @param {any} context of callback to remove.\n   */\n  off(domainObject, event, callback, context) {\n    /** @type {string} */\n    const keyString = makeKeyString(domainObject.identifier);\n    const objectListeners = this.listeningTo[keyString];\n\n    const index = objectListeners[event].findIndex((l) => {\n      return l.callback === callback && l.context === context;\n    });\n\n    objectListeners[event].splice(index, 1);\n    if (\n      !objectListeners.add.length &&\n      !objectListeners.remove.length &&\n      !objectListeners.reorder.length\n    ) {\n      delete this.listeningTo[keyString];\n    }\n  }\n  /**\n   * Remove a domain object from another domain object's composition.\n   *\n   * This method is optional; if not present, adding to a domain object's\n   * composition using this provider will be disallowed.\n   *\n   * @override\n   * @param {DomainObject} domainObject the domain object\n   *        which should have its composition modified\n   * @param {Identifier} childId the domain object to remove\n   * @method remove\n   */\n  remove(domainObject, childId) {\n    let composition = domainObject.composition.filter(function (child) {\n      return !(childId.namespace === child.namespace && childId.key === child.key);\n    });\n\n    this.publicAPI.objects.mutate(domainObject, 'composition', composition);\n  }\n  /**\n   * Add a domain object to another domain object's composition.\n   *\n   * This method is optional; if not present, adding to a domain object's\n   * composition using this provider will be disallowed.\n   *\n   * @override\n   * @param {DomainObject} parent the domain object\n   *        which should have its composition modified\n   * @param {Identifier} childId the domain object to add\n   * @method add\n   */\n  add(parent, childId) {\n    if (!this.includes(parent, childId)) {\n      const composition = structuredClone(toRaw(parent.composition));\n      composition.push(childId);\n      this.publicAPI.objects.mutate(parent, 'composition', composition);\n    }\n  }\n\n  /**\n   * @override\n   * @param {DomainObject} parent\n   * @param {Identifier} childId\n   * @returns {boolean}\n   */\n  includes(parent, childId) {\n    return parent.composition.some((composee) =>\n      this.publicAPI.objects.areIdsEqual(composee, childId)\n    );\n  }\n\n  /**\n   * @override\n   * @param {DomainObject} domainObject\n   * @param {number} oldIndex\n   * @param {number} newIndex\n   * @returns\n   */\n  reorder(domainObject, oldIndex, newIndex) {\n    let newComposition = domainObject.composition.slice();\n    let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;\n    let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;\n    //Insert object in new position\n    newComposition.splice(insertPosition, 0, domainObject.composition[oldIndex]);\n    newComposition.splice(removeId, 1);\n\n    let reorderPlan = [\n      {\n        oldIndex,\n        newIndex\n      }\n    ];\n\n    if (oldIndex > newIndex) {\n      for (let i = newIndex; i < oldIndex; i++) {\n        reorderPlan.push({\n          oldIndex: i,\n          newIndex: i + 1\n        });\n      }\n    } else {\n      for (let i = oldIndex + 1; i <= newIndex; i++) {\n        reorderPlan.push({\n          oldIndex: i,\n          newIndex: i - 1\n        });\n      }\n    }\n\n    this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);\n\n    /** @type {string} */\n    let id = makeKeyString(domainObject.identifier);\n    const listeners = this.listeningTo[id];\n\n    if (!listeners) {\n      return;\n    }\n\n    listeners.reorder.forEach(notify);\n\n    function notify(listener) {\n      if (listener.context) {\n        listener.callback.call(listener.context, reorderPlan);\n      } else {\n        listener.callback(reorderPlan);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/api/faultmanagement/FaultManagementAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/** @type {ShelveDuration[]} */\nexport const DEFAULT_SHELVE_DURATIONS = [\n  {\n    name: '5 Minutes',\n    value: 300000\n  },\n  {\n    name: '10 Minutes',\n    value: 600000\n  },\n  {\n    name: '15 Minutes',\n    value: 900000\n  },\n  {\n    name: 'Unlimited',\n    value: null\n  }\n];\n\n/**\n * Provides an API for managing faults within Open MCT.\n * It allows for the addition of a fault provider, checking for provider support, and\n * performing various operations such as requesting, subscribing to, acknowledging,\n * and shelving faults.\n */\nexport default class FaultManagementAPI {\n  /**\n   * @param {import(\"openmct\").OpenMCT} openmct\n   */\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  /**\n   * Sets the provider for the Fault Management API.\n   * The provider should implement methods for acknowledging and shelving faults.\n   *\n   * @param {*} provider - The provider to be set.\n   */\n  addProvider(provider) {\n    this.provider = provider;\n  }\n\n  /**\n   * Checks if the current provider supports fault management actions.\n   * Specifically, it checks if the provider has methods for acknowledging and shelving faults.\n   *\n   * @returns {boolean} - Returns true if the provider supports fault management actions, otherwise false.\n   */\n  supportsActions() {\n    return (\n      this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined\n    );\n  }\n\n  /**\n   * Requests fault data for a given domain object.\n   * This method checks if the current provider supports the request operation for the given domain object.\n   * If supported, it delegates the request to the provider's request method.\n   * If not supported, it returns a rejected promise.\n   *\n   * @param {import('openmct').DomainObject} domainObject - The domain object for which fault data is requested.\n   * @returns {Promise.<FaultAPIResponse[]>} - A promise that resolves to an array of fault API responses.\n   */\n  request(domainObject) {\n    if (!this.provider?.supportsRequest(domainObject)) {\n      return Promise.reject('Provider does not support request operation');\n    }\n\n    return this.provider.request(domainObject);\n  }\n\n  /**\n   * Subscribes to fault data updates for a given domain object.\n   * This method checks if the current provider supports the subscribe operation for the given domain object.\n   * If supported, it delegates the subscription to the provider's subscribe method.\n   * If not supported, it returns a rejected promise.\n   *\n   * @param {import('openmct').DomainObject} domainObject - The domain object for which to subscribe to fault data updates.\n   * @param {Function} callback - The callback function to be called with fault data updates.\n   * @returns {Function} unsubscribe - A function to unsubscribe from the fault data updates.\n   */\n  subscribe(domainObject, callback) {\n    if (!this.provider?.supportsSubscribe(domainObject)) {\n      return Promise.reject('Provider does not support subscribe operation');\n    }\n\n    return this.provider.subscribe(domainObject, callback);\n  }\n\n  /**\n   * Acknowledges a fault using the provider's acknowledgeFault method.\n   *\n   * @param {Fault} fault - The fault object to be acknowledged.\n   * @param {*} ackData - Additional data required for acknowledging the fault.\n   * @returns {Promise.<T>} - A promise that resolves when the fault is acknowledged.\n   */\n  acknowledgeFault(fault, ackData) {\n    return this.provider.acknowledgeFault(fault, ackData);\n  }\n\n  /**\n   * Shelves a fault using the provider's shelveFault method.\n   *\n   * @param {Fault} fault - The fault object to be shelved.\n   * @param {*} shelveData - Additional data required for shelving the fault.\n   * @returns {Promise.<T>} - A promise that resolves when the fault is shelved.\n   */\n  shelveFault(fault, shelveData) {\n    return this.provider.shelveFault(fault, shelveData);\n  }\n\n  /**\n   * Retrieves the available shelve durations from the provider, or the default durations if the\n   * provider does not provide any.\n   * @returns {ShelveDuration[] | undefined}\n   */\n  getShelveDurations() {\n    if (!this.provider) {\n      return;\n    }\n\n    return this.provider.getShelveDurations?.() ?? DEFAULT_SHELVE_DURATIONS;\n  }\n}\n\n/**\n * @typedef {Object} ShelveDuration\n * @property {string} name - The name of the shelve duration\n * @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited\n */\n\n/**\n * @typedef {Object} TriggerValueInfo\n * @property {number} value\n * @property {string} rangeCondition\n * @property {string} monitoringResult\n */\n\n/**\n * @typedef {Object} CurrentValueInfo\n * @property {number} value\n * @property {string} rangeCondition\n * @property {string} monitoringResult\n */\n\n/**\n * @typedef {Object} Fault\n * @property {boolean} acknowledged\n * @property {CurrentValueInfo} currentValueInfo\n * @property {string} id\n * @property {string} name\n * @property {string} namespace\n * @property {number} seqNum\n * @property {string} severity\n * @property {boolean} shelved\n * @property {string} shortDescription\n * @property {string} triggerTime\n * @property {TriggerValueInfo} triggerValueInfo\n */\n\n/**\n * @typedef {Object} FaultAPIResponse\n * @property {string} type\n * @property {Fault} fault\n */\n"
  },
  {
    "path": "src/api/faultmanagement/FaultManagementAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * License); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an AS IS BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\n\nconst faultName = 'super duper fault';\nconst aFault = {\n  type: '',\n  fault: {\n    acknowledged: true,\n    currentValueInfo: {\n      value: 0,\n      rangeCondition: '',\n      monitoringResult: ''\n    },\n    id: '',\n    name: faultName,\n    namespace: '',\n    seqNum: 0,\n    severity: '',\n    shelved: true,\n    shortDescription: '',\n    triggerTime: '',\n    triggerValueInfo: {\n      value: 0,\n      rangeCondition: '',\n      monitoringResult: ''\n    }\n  }\n};\nconst faultDomainObject = {\n  name: 'it is not your fault',\n  type: 'faultManagement',\n  identifier: {\n    key: 'nobodies',\n    namespace: 'fault'\n  }\n};\nconst aComment = 'THIS is my fault.';\nconst faultManagementProvider = {\n  request() {\n    return Promise.resolve([aFault]);\n  },\n  subscribe(domainObject, callback) {\n    return () => {};\n  },\n  supportsRequest(domainObject) {\n    return domainObject.type === 'faultManagement';\n  },\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'faultManagement';\n  },\n  acknowledgeFault(fault, { comment = '' }) {\n    return Promise.resolve({\n      success: true\n    });\n  },\n  shelveFault(fault, shelveData) {\n    return Promise.resolve({\n      success: true\n    });\n  }\n};\n\ndescribe('The Fault Management API', () => {\n  let openmct;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n    openmct.install(openmct.plugins.FaultManagement());\n    // openmct.install(openmct.plugins.example.ExampleFaultSource());\n    openmct.faults.addProvider(faultManagementProvider);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('allows you to request a fault', async () => {\n    spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();\n\n    let faultResponse = await openmct.faults.request(faultDomainObject);\n\n    expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);\n    expect(faultResponse[0].fault.name).toEqual(faultName);\n  });\n\n  it('allows you to subscribe to a fault', () => {\n    spyOn(faultManagementProvider, 'subscribe').and.callThrough();\n    spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();\n\n    let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});\n\n    expect(unsubscribe).toEqual(jasmine.any(Function));\n    expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);\n    expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(\n      faultDomainObject,\n      jasmine.any(Function)\n    );\n  });\n\n  it('will tell you if the fault management provider supports actions', () => {\n    expect(openmct.faults.supportsActions()).toBeTrue();\n  });\n\n  it('will allow you to acknowledge a fault', async () => {\n    spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();\n\n    let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);\n\n    expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);\n    expect(ackResponse.success).toBeTrue();\n  });\n\n  it('will allow you to shelve a fault', async () => {\n    spyOn(faultManagementProvider, 'shelveFault').and.callThrough();\n\n    let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);\n\n    expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);\n    expect(shelveResponse.success).toBeTrue();\n  });\n});\n"
  },
  {
    "path": "src/api/forms/FormController.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport AutoCompleteField from './components/controls/AutoCompleteField.vue';\nimport CheckBoxField from './components/controls/CheckBoxField.vue';\nimport ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';\nimport Datetime from './components/controls/DatetimeField.vue';\nimport FileInput from './components/controls/FileInput.vue';\nimport Locator from './components/controls/LocatorField.vue';\nimport NumberField from './components/controls/NumberField.vue';\nimport SelectField from './components/controls/SelectField.vue';\nimport TextAreaField from './components/controls/TextAreaField.vue';\nimport TextField from './components/controls/TextField.vue';\nimport ToggleSwitchField from './components/controls/ToggleSwitchField.vue';\n\n/** @type {Record<string, import('vue').Component>} */\nexport const DEFAULT_CONTROLS_MAP = {\n  autocomplete: AutoCompleteField,\n  checkbox: CheckBoxField,\n  composite: ClockDisplayFormatField,\n  datetime: Datetime,\n  'file-input': FileInput,\n  locator: Locator,\n  numberfield: NumberField,\n  select: SelectField,\n  textarea: TextAreaField,\n  textfield: TextField,\n  toggleSwitch: ToggleSwitchField\n};\n\nexport default class FormControl {\n  /** @type {Record<string, ControlViewProvider>} */\n  controls;\n\n  /**\n   * @param {OpenMCT} openmct\n   */\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.controls = {};\n\n    this._addDefaultFormControls();\n  }\n\n  /**\n   * @param {string} controlName\n   * @param {ControlViewProvider} controlViewProvider\n   */\n  addControl(controlName, controlViewProvider) {\n    const control = this.controls[controlName];\n    if (control) {\n      console.warn(`Error: provided form control '${controlName}', already exists`);\n\n      return;\n    }\n\n    this.controls[controlName] = controlViewProvider;\n  }\n\n  /**\n   * @param {string} controlName\n   * @returns {ControlViewProvider | undefined}\n   */\n  getControl(controlName) {\n    const control = this.controls[controlName];\n    if (!control) {\n      console.error(`Error: form control '${controlName}', does not exist`);\n    }\n\n    return control;\n  }\n\n  /**\n   * @private\n   */\n  _addDefaultFormControls() {\n    Object.keys(DEFAULT_CONTROLS_MAP).forEach((control) => {\n      const controlViewProvider = this._getControlViewProvider(control);\n      this.addControl(control, controlViewProvider);\n    });\n  }\n\n  /**\n   * @private\n   * @param {string} control\n   * @returns {ControlViewProvider}\n   */\n  _getControlViewProvider(control) {\n    const self = this;\n    let _destroy = null;\n\n    return {\n      show(element, model, onChange) {\n        const { vNode, destroy } = mount(\n          {\n            el: element,\n            components: {\n              FormControlComponent: DEFAULT_CONTROLS_MAP[control]\n            },\n            provide: {\n              openmct: self.openmct\n            },\n            data() {\n              return {\n                model,\n                onChange\n              };\n            },\n            template: `<FormControlComponent :model=\"model\" @on-change=\"onChange\"></FormControlComponent>`\n          },\n          {\n            element,\n            app: self.openmct.app\n          }\n        );\n        _destroy = destroy;\n\n        return vNode;\n      },\n      destroy() {\n        if (_destroy) {\n          _destroy();\n        }\n      }\n    };\n  }\n}\n\n/**\n * @typedef {import('openmct')} OpenMCT\n */\n\n/**\n * @typedef {Object} ControlViewProvider\n * @property {(element: HTMLElement, model: any, onChange: Function) => any} show\n * @property {() => void} destroy\n */\n"
  },
  {
    "path": "src/api/forms/FormsAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\nimport mount from 'utils/mount';\n\nimport FormProperties from './components/FormProperties.vue';\nimport FormController from './FormController.js';\n\n/**\n * The FormsAPI provides methods for creating and managing forms in Open MCT.\n */\nexport default class FormsAPI {\n  /**\n   * Creates an instance of FormsAPI.\n   * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n   */\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.formController = new FormController(openmct);\n  }\n\n  /**\n   * Control View Provider definition for a form control\n   * @typedef ControlViewProvider\n   * @property {function} show a function renders view in place of given element\n   *   This function accepts element, model and onChange function\n   *   element - html element (place holder) to render a row view\n   *   model - row data for rendering name, value etc for given row type\n   *   onChange - an onChange event callback function to keep track of any change in value\n   * @property {function} destroy a callback function when a vue component gets destroyed\n   */\n\n  /**\n   * Create a new form control definition with a formControlViewProvider\n   *      this formControlViewProvider is used inside form overlay to show/render a form row\n   *\n   * @param {string} controlName a form structure, array of section\n   * @param {ControlViewProvider} controlViewProvider\n   */\n  addNewFormControl(controlName, controlViewProvider) {\n    this.formController.addControl(controlName, controlViewProvider);\n  }\n\n  /**\n   * Get a ControlViewProvider for a given/stored form controlName\n   *\n   * @param {string} controlName a form structure, array of section\n   * @return {ControlViewProvider}\n   */\n  getFormControl(controlName) {\n    return this.formController.getControl(controlName);\n  }\n\n  /**\n   * Section definition for formStructure\n   * @typedef Section\n   * @property {Object} name Name of the section to display on Form\n   * @property {string} cssClass class name for styling section\n   * @property {array<Row>} rows collection of rows inside a section\n   */\n\n  /**\n   * Row definition for Section\n   * @typedef Row\n   * @property {string} control represents type of row to render\n   *     eg:autocomplete,composite,datetime,file-input,locator,numberfield,select,textarea,textfield\n   * @property {string} cssClass class name for styling this row\n   * @property {import('openmct').DomainObject} domainObject object to be used by row\n   * @property {string} key id for this row\n   * @property {string} name Name of the row to display on Form\n   * @property {import('openmct').DomainObject} parent parent object to be used by row\n   * @property {boolean} required is this row mandatory\n   * @property {function} validate a function to validate this row on any changes\n   */\n\n  /**\n   * Show form inside an Overlay dialog with given form structure\n   * @param {Array<Section>} formStructure a form structure, array of section\n   * @param {Object} options\n   * @param {() => void} [options.onChange] a callback function when any changes detected\n   * @returns {Promise<Object>} A promise that resolves with the form data when saved, or rejects when cancelled\n   */\n  showForm(formStructure, { onChange } = {}) {\n    let overlay;\n\n    const self = this;\n\n    const overlayEl = document.createElement('div');\n    overlayEl.classList.add('u-contents');\n\n    overlay = self.openmct.overlays.overlay({\n      element: overlayEl,\n      size: 'dialog'\n    });\n\n    let formSave;\n    let formCancel;\n    const promise = new Promise((resolve, reject) => {\n      formSave = resolve;\n      formCancel = reject;\n    });\n\n    this.showCustomForm(formStructure, {\n      element: overlayEl,\n      onChange\n    })\n      .then((response) => {\n        overlay.dismiss();\n        formSave(response);\n      })\n      .catch((response) => {\n        overlay.dismiss();\n        formCancel(response);\n      });\n\n    return promise;\n  }\n\n  /**\n   * Show form as a child of the element provided with given form structure\n   *\n   * @param {Array<Section>} formStructure a form structure, array of section\n   * @param {Object} options\n   * @param {HTMLElement} options.element Parent Element to render a Form\n   * @param {() => void} [options.onChange] a callback function when any changes detected\n   * @returns {Promise<Object>} A promise that resolves with the form data when saved, or rejects when cancelled\n   */\n  showCustomForm(formStructure, { element, onChange } = {}) {\n    if (element === undefined) {\n      throw Error('Required element parameter not provided');\n    }\n\n    const self = this;\n\n    const changes = {};\n    let formSave;\n    let formCancel;\n\n    const promise = new Promise((resolve, reject) => {\n      formSave = onFormAction(resolve);\n      formCancel = onFormAction(reject);\n    });\n\n    const { destroy } = mount(\n      {\n        components: { FormProperties },\n        provide: {\n          openmct: self.openmct\n        },\n        data() {\n          return {\n            formStructure,\n            onChange: onFormPropertyChange,\n            onCancel: formCancel,\n            onSave: formSave\n          };\n        },\n        template:\n          '<FormProperties :model=\"formStructure\" @on-change=\"onChange\" @on-cancel=\"onCancel\" @on-save=\"onSave\"></FormProperties>'\n      },\n      {\n        element,\n        app: self.openmct.app\n      }\n    );\n\n    /**\n     * Handles form property changes\n     * @param {Object} data - The changed form data\n     */\n    function onFormPropertyChange(data) {\n      if (onChange) {\n        onChange(data);\n      }\n\n      if (data.model) {\n        const property = data.model.property;\n        let key = data.model.key;\n\n        if (property && property.length) {\n          key = property.join('.');\n        }\n\n        _.set(changes, key, data.value);\n      }\n    }\n\n    /**\n     * Creates a form action handler\n     * @param {() => void} callback - The callback to be called when the form action is triggered\n     * @returns {(...args: any[]) => void} The form action handler\n     */\n    function onFormAction(callback) {\n      return () => {\n        destroy();\n\n        if (callback) {\n          callback(changes);\n        }\n      };\n    }\n\n    return promise;\n  }\n}\n"
  },
  {
    "path": "src/api/forms/FormsAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\n\ndescribe('The Forms API', () => {\n  let openmct;\n  let element;\n\n  beforeEach((done) => {\n    element = document.createElement('div');\n    element.style.display = 'block';\n    element.style.width = '1920px';\n    element.style.height = '1080px';\n\n    openmct = createOpenMct();\n    openmct.on('start', done);\n\n    openmct.startHeadless(element);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('openmct supports form API', () => {\n    expect(openmct.forms).not.toBe(null);\n  });\n\n  describe('check default form controls exists', () => {\n    it('autocomplete', () => {\n      const control = openmct.forms.getFormControl('autocomplete');\n      expect(control).not.toBe(null);\n    });\n\n    it('clock', () => {\n      const control = openmct.forms.getFormControl('composite');\n      expect(control).not.toBe(null);\n    });\n\n    it('datetime', () => {\n      const control = openmct.forms.getFormControl('datetime');\n      expect(control).not.toBe(null);\n    });\n\n    it('file-input', () => {\n      const control = openmct.forms.getFormControl('file-input');\n      expect(control).not.toBe(null);\n    });\n\n    it('locator', () => {\n      const control = openmct.forms.getFormControl('locator');\n      expect(control).not.toBe(null);\n    });\n\n    it('numberfield', () => {\n      const control = openmct.forms.getFormControl('numberfield');\n      expect(control).not.toBe(null);\n    });\n\n    it('select', () => {\n      const control = openmct.forms.getFormControl('select');\n      expect(control).not.toBe(null);\n    });\n\n    it('textarea', () => {\n      const control = openmct.forms.getFormControl('textarea');\n      expect(control).not.toBe(null);\n    });\n\n    it('textfield', () => {\n      const control = openmct.forms.getFormControl('textfield');\n      expect(control).not.toBe(null);\n    });\n  });\n\n  it('supports user defined form controls', () => {\n    const newFormControl = {\n      show: () => {\n        console.log('show new control');\n      },\n      destroy: () => {\n        console.log('destroy');\n      }\n    };\n    openmct.forms.addNewFormControl('newFormControl', newFormControl);\n    const control = openmct.forms.getFormControl('newFormControl');\n    expect(control).not.toBe(null);\n    expect(control.show).not.toBe(null);\n    expect(control.destroy).not.toBe(null);\n  });\n\n  describe('show form on UI', () => {\n    let formStructure;\n\n    beforeEach(() => {\n      formStructure = {\n        title: 'Test Show Form',\n        sections: [\n          {\n            rows: [\n              {\n                key: 'name',\n                control: 'textfield',\n                name: 'Title',\n                pattern: '\\\\S+',\n                required: false,\n                cssClass: 'l-input-lg',\n                value: 'Test Name'\n              }\n            ]\n          }\n        ]\n      };\n    });\n\n    it('when container element is provided', (done) => {\n      openmct.forms.showCustomForm(formStructure, { element }).catch(() => {\n        done();\n      });\n      const titleElement = element.querySelector('.c-overlay__dialog-title');\n      expect(titleElement.textContent).toBe(formStructure.title);\n\n      element.querySelector('.js-cancel-button').click();\n    });\n\n    it('when container element is not provided', (done) => {\n      openmct.forms.showForm(formStructure).catch(() => {\n        done();\n      });\n\n      const titleElement = document.querySelector('.c-overlay__dialog-title');\n      const title = titleElement.textContent;\n\n      expect(title).toBe(formStructure.title);\n      document.querySelector('.js-cancel-button').click();\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/forms/components/FormProperties.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-form js-form\">\n    <div class=\"c-overlay__top-bar c-form__top-bar\">\n      <div class=\"c-overlay__dialog-title js-form-title\">{{ model.title }}</div>\n      <div v-if=\"hasRequiredFields\" class=\"c-overlay__dialog-hint hint\">\n        All fields marked <span class=\"req icon-asterisk\"></span> are required.\n      </div>\n    </div>\n    <form name=\"mctForm\" class=\"c-form__contents\" autocomplete=\"off\" @submit.prevent>\n      <div\n        v-for=\"section in formSections\"\n        :key=\"section.id\"\n        class=\"c-form__section\"\n        :class=\"section.cssClass\"\n      >\n        <h2 v-if=\"section.name\" class=\"c-form__section-header\">\n          {{ section.name }}\n        </h2>\n        <FormRow\n          v-for=\"(row, index) in section.rows\"\n          :key=\"row.id\"\n          :css-class=\"row.cssClass\"\n          :first=\"index < 1\"\n          :row=\"row\"\n          @on-change=\"onChange\"\n        />\n      </div>\n    </form>\n\n    <div class=\"mct-form__controls c-overlay__button-bar c-form__bottom-bar\">\n      <button\n        tabindex=\"0\"\n        :disabled=\"isInvalid\"\n        class=\"c-button c-button--major\"\n        aria-label=\"Save\"\n        @click=\"onSave\"\n      >\n        {{ submitLabel }}\n      </button>\n      <button\n        v-if=\"!shouldHideCancelButton\"\n        tabindex=\"0\"\n        class=\"c-button js-cancel-button\"\n        aria-label=\"Cancel\"\n        @click=\"onCancel\"\n      >\n        {{ cancelLabel }}\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { v4 as uuid } from 'uuid';\n\nimport FormRow from '@/api/forms/components/FormRow.vue';\n\nexport default {\n  components: {\n    FormRow\n  },\n  inject: ['openmct'],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    },\n    value: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['on-change', 'on-save', 'on-cancel'],\n  data() {\n    return {\n      invalidProperties: {},\n      formSections: []\n    };\n  },\n  computed: {\n    hasRequiredFields() {\n      return this.model.sections.some((section) => section.rows.some((row) => row.required));\n    },\n    isInvalid() {\n      return Object.entries(this.invalidProperties).some(([key, value]) => {\n        return value;\n      });\n    },\n    submitLabel() {\n      if (this.model.buttons && this.model.buttons.submit && this.model.buttons.submit.label) {\n        return this.model.buttons.submit.label;\n      }\n\n      return 'Ok';\n    },\n    cancelLabel() {\n      if (this.model.buttons && this.model.buttons.cancel && this.model.buttons.cancel.label) {\n        return this.model.buttons.submit.label;\n      }\n\n      return 'Cancel';\n    },\n    shouldHideCancelButton() {\n      return this.model.buttons?.cancel?.hide === true;\n    }\n  },\n  mounted() {\n    this.formSections = this.model.sections.map((section) => {\n      section.id = uuid();\n\n      section.rows = section.rows.map((row) => {\n        row.id = uuid();\n\n        return row;\n      });\n\n      return section;\n    });\n  },\n  methods: {\n    onChange(data) {\n      this.invalidProperties[data.model.key] = data.invalid;\n\n      this.$emit('on-change', data);\n    },\n    onCancel() {\n      this.$emit('on-cancel');\n    },\n    onSave() {\n      this.$emit('on-save');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/FormRow.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"form-row c-form__row\" :class=\"[{ first: first }, cssClass]\" @on-change=\"onChange\">\n    <label class=\"c-form-row__label\" :title=\"row.description\" :for=\"`form-${row.key}`\">\n      {{ row.name }}\n    </label>\n    <div class=\"c-form-row__state-indicator\" :class=\"reqClass\"></div>\n    <div v-if=\"row.control\" ref=\"rowElement\" class=\"c-form-row__controls\"></div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'FormRow',\n  components: {},\n  inject: ['openmct'],\n  props: {\n    cssClass: {\n      type: String,\n      default: '',\n      required: true\n    },\n    first: {\n      type: Boolean,\n      default: false,\n      required: true\n    },\n    row: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      formControl: this.openmct.forms.getFormControl(this.row.control),\n      valid: undefined,\n      visited: false\n    };\n  },\n  computed: {\n    reqClass() {\n      let reqClass = 'req';\n\n      if (!this.row.required) {\n        return;\n      }\n\n      if (this.visited && this.valid !== undefined) {\n        if (this.valid === true) {\n          reqClass = 'valid';\n        } else {\n          reqClass = 'invalid';\n        }\n      }\n\n      return reqClass;\n    }\n  },\n  mounted() {\n    if (this.row.required) {\n      const data = {\n        model: this.row,\n        value: this.row.value\n      };\n\n      this.onChange(data, false);\n    }\n\n    this.formControl.show(this.$refs.rowElement, this.row, this.onChange);\n  },\n  unmounted() {\n    const destroy = this.formControl.destroy;\n    if (destroy) {\n      destroy();\n    }\n  },\n  methods: {\n    onChange(data, visited = true) {\n      this.visited = visited;\n      this.valid = this.validateRow(data);\n      data.invalid = !this.valid;\n\n      this.$emit('on-change', data);\n    },\n    validateRow(data) {\n      let valid = true;\n      if (this.row.required) {\n        valid = data.value !== undefined && data.value !== null && data.value !== '';\n      }\n\n      if (this.row.required && !valid) {\n        return false;\n      }\n\n      const pattern = data.model.pattern;\n      if (valid && pattern) {\n        const regex = new RegExp(pattern);\n        valid = regex.test(data.value);\n      }\n\n      const validate = data.model.validate;\n      if (valid && validate) {\n        valid = validate(data);\n      }\n\n      return Boolean(valid);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/AutoCompleteField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"autoCompleteForm\" class=\"form-control c-input--autocomplete js-autocomplete\">\n    <div class=\"c-input--autocomplete__wrapper\">\n      <input\n        ref=\"autoCompleteInput\"\n        v-model=\"field\"\n        class=\"c-input--autocomplete__input js-autocomplete__input\"\n        type=\"text\"\n        :placeholder=\"placeHolderText\"\n        @click=\"inputClicked()\"\n        @keydown=\"keyDown($event)\"\n      />\n      <div\n        class=\"icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow\"\n        @click=\"arrowClicked()\"\n      ></div>\n    </div>\n    <div\n      v-if=\"!hideOptions && filteredOptions.length > 0\"\n      class=\"c-menu c-input--autocomplete__options js-autocomplete-options\"\n      aria-label=\"Autocomplete Options\"\n      @blur=\"hideOptions = true\"\n    >\n      <ul>\n        <li\n          v-for=\"opt in filteredOptions\"\n          :key=\"opt.optionId\"\n          :class=\"[{ optionPreSelected: optionIndex === opt.optionId }, itemCssClass]\"\n          :style=\"itemStyle(opt)\"\n          @click=\"fillInputWithString(opt.name)\"\n          @mouseover=\"optionMouseover(opt.optionId)\"\n        >\n          {{ opt.name }}\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nconst key = {\n  down: 40,\n  up: 38,\n  enter: 13\n};\n\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    },\n    placeHolderText: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    itemCssClass: {\n      type: String,\n      required: false,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      hideOptions: true,\n      showFilteredOptions: false,\n      optionIndex: 0,\n      field: this.model.value\n    };\n  },\n  computed: {\n    filteredOptions() {\n      const fullOptions = this.options || [];\n      if (this.showFilteredOptions) {\n        const optionsFiltered = fullOptions\n          .filter((option) => {\n            if (option.name && this.field) {\n              return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;\n            }\n\n            return false;\n          })\n          .map((option, index) => {\n            return {\n              optionId: index,\n              name: option.name,\n              color: option.color\n            };\n          });\n\n        return optionsFiltered;\n      }\n\n      const optionsFiltered = fullOptions.map((option, index) => {\n        return {\n          optionId: index,\n          name: option.name,\n          color: option.color\n        };\n      });\n\n      return optionsFiltered;\n    }\n  },\n  watch: {\n    field(newValue, oldValue) {\n      if (newValue !== oldValue) {\n        const data = {\n          model: this.model,\n          value: newValue\n        };\n\n        this.$emit('on-change', data);\n      }\n    },\n    hideOptions(newValue) {\n      if (!newValue) {\n        // adding a event listener when the hideOptions is false (dropdown is visible)\n        // handleoutsideclick can collapse the dropdown when clicked outside autocomplete\n        document.body.addEventListener('click', this.handleOutsideClick);\n      } else {\n        //removing event listener when hideOptions become true (dropdown is collapsed)\n        document.body.removeEventListener('click', this.handleOutsideClick);\n      }\n    }\n  },\n  mounted() {\n    this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;\n    this.autocompleteInputElement = this.$refs.autoCompleteInput;\n    if (this.model.options && this.model.options.length && !this.model.options[0].name) {\n      // If options is only an array of string.\n      this.options = this.model.options.map((option) => {\n        return {\n          name: option\n        };\n      });\n    } else {\n      this.options = this.model.options;\n    }\n  },\n  unmounted() {\n    document.body.removeEventListener('click', this.handleOutsideClick);\n  },\n  methods: {\n    decrementOptionIndex() {\n      if (this.optionIndex === 0) {\n        this.optionIndex = this.filteredOptions.length;\n      }\n\n      this.optionIndex--;\n      this.scrollIntoView();\n    },\n    incrementOptionIndex() {\n      if (this.optionIndex === this.filteredOptions.length - 1) {\n        this.optionIndex = -1;\n      }\n\n      this.optionIndex++;\n      this.scrollIntoView();\n    },\n    fillInputWithString(string) {\n      this.hideOptions = true;\n      this.field = string;\n    },\n    showOptions() {\n      this.hideOptions = false;\n      this.optionIndex = 0;\n    },\n    keyDown($event) {\n      this.showFilteredOptions = true;\n      if (this.filteredOptions) {\n        let keyCode = $event.keyCode;\n        switch (keyCode) {\n          case key.down:\n            this.incrementOptionIndex();\n            break;\n          case key.up:\n            $event.preventDefault(); // Prevents cursor jumping back and forth\n            this.decrementOptionIndex();\n            break;\n          case key.enter:\n            if (this.filteredOptions[this.optionIndex]) {\n              this.fillInputWithString(this.filteredOptions[this.optionIndex].name);\n            }\n        }\n      }\n    },\n    inputClicked() {\n      this.autocompleteInputElement.select();\n      this.showOptions();\n    },\n    arrowClicked() {\n      // if the user clicked the arrow, we want\n      // to show them all the options\n      this.showFilteredOptions = false;\n      this.autocompleteInputElement.select();\n\n      if (!this.hideOptions && this.filteredOptions.length > 0) {\n        this.hideOptions = true;\n      } else {\n        this.showOptions();\n      }\n    },\n    handleOutsideClick(event) {\n      // if click event is detected outside autocomplete (both input & arrow) while the\n      // dropdown is visible, this will collapse the dropdown.\n      const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);\n      if (!clickedInsideAutocomplete && !this.hideOptions) {\n        this.hideOptions = true;\n      }\n    },\n    optionMouseover(optionId) {\n      this.optionIndex = optionId;\n    },\n    scrollIntoView() {\n      setTimeout(() => {\n        const element = this.$el.querySelector('.optionPreSelected');\n        if (element) {\n          element.scrollIntoView({\n            behavior: 'smooth',\n            block: 'center',\n            inline: 'nearest'\n          });\n        }\n      });\n    },\n    itemStyle(option) {\n      if (option.color) {\n        return { '--optionIconColor': option.color };\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/CheckBoxField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <input type=\"checkbox\" :checked=\"isChecked\" @input=\"toggleCheckBox\" />\n    </span>\n  </span>\n</template>\n\n<script>\nimport toggleMixin from '../../toggle-check-box-mixin.js';\n\nexport default {\n  mixins: [toggleMixin],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      isChecked: this.model.value\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/ClockDisplayFormatField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-form-control--clock-display-format-fields\">\n    <SelectField v-for=\"item in items\" :key=\"item.key\" :model=\"item\" @on-change=\"onChange\" />\n  </div>\n</template>\n\n<script>\nimport SelectField from '@/api/forms/components/controls/SelectField.vue';\n\nexport default {\n  components: {\n    SelectField\n  },\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      items: []\n    };\n  },\n  mounted() {\n    const values = this.model.value || [];\n    this.items = this.model.items.map((item, index) => {\n      item.value = values[index];\n      item.key = `${this.model.key}.${index}`;\n\n      return item;\n    });\n  },\n  methods: {\n    onChange(data) {\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/CompositeContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span>\n    <CompositeItem\n      v-for=\"(item, index) in model.items\"\n      :key=\"item.name\"\n      :first=\"index < 1\"\n      :value=\"JSON.stringify(model.value[index])\"\n      :item=\"item\"\n      @on-change=\"onChange\"\n    />\n  </span>\n</template>\n\n<script>\nimport CompositeItem from '@/api/forms/components/controls/CompositeItem.vue';\n\nexport default {\n  components: {\n    CompositeItem\n  },\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  mounted() {\n    this.model.items.forEach((item, index) => (item.key = `${this.model.key}.${index}`));\n  },\n  methods: {\n    onChange(data) {\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/CompositeItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div :class=\"compositeCssClass\">\n    <FormRow :css-class=\"item.cssClass\" :first=\"first\" :row=\"row\" @on-change=\"onChange\" />\n    <span class=\"composite-control-label\">\n      {{ item.name }}\n    </span>\n  </div>\n</template>\n\n<script>\nexport default {\n  components: {\n    FormRow: () => import('@/api/forms/components/FormRow.vue')\n  },\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    first: {\n      type: Boolean,\n      required: true\n    },\n    value: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['on-change'],\n  computed: {\n    compositeCssClass() {\n      return `l-composite-control l-${this.item.control}`;\n    },\n    row() {\n      const row = this.item;\n      row.value = JSON.parse(this.value);\n\n      return row;\n    }\n  },\n  methods: {\n    onChange(data) {\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/DatetimeField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-form-control--datetime\">\n    <div class=\"hint date\">Date</div>\n    <div class=\"hint time sm\">Hour</div>\n    <div class=\"hint time sm\">Min</div>\n    <div class=\"hint time sm\">Sec</div>\n    <div class=\"hint timezone\">Timezone</div>\n    <form ref=\"dateTimeForm\" prevent class=\"u-contents\">\n      <input\n        v-model=\"date\"\n        class=\"field control date\"\n        :pattern=\"/\\d{4}-\\d{2}-\\d{2}/\"\n        :placeholder=\"format\"\n        type=\"date\"\n        name=\"date\"\n        @change=\"onChange\"\n      />\n      <input\n        v-model=\"hour\"\n        class=\"field control hour c-input--sm\"\n        :pattern=\"/\\d+/\"\n        type=\"number\"\n        name=\"hour\"\n        maxlength=\"10\"\n        min=\"0\"\n        max=\"23\"\n        @change=\"onChange\"\n      />\n      <input\n        v-model=\"min\"\n        class=\"field control min c-input--sm\"\n        :pattern=\"/\\d+/\"\n        type=\"number\"\n        name=\"min\"\n        maxlength=\"2\"\n        min=\"0\"\n        max=\"59\"\n        @change=\"onChange\"\n      />\n      <input\n        v-model=\"sec\"\n        class=\"field control sec c-input--sm\"\n        :pattern=\"/\\d+/\"\n        type=\"number\"\n        name=\"sec\"\n        maxlength=\"2\"\n        min=\"0\"\n        max=\"59\"\n        @change=\"onChange\"\n      />\n      <div class=\"field control hint timezone\">UTC</div>\n    </form>\n  </div>\n</template>\n\n<script>\nconst DATE_FORMAT = 'YYYY-MM-DD';\n\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      format: DATE_FORMAT,\n      date: '',\n      hour: 0,\n      min: 0,\n      sec: 0\n    };\n  },\n  mounted() {\n    this.formatDatetime();\n  },\n  methods: {\n    convertToDatetime(timestamp) {\n      const dateValue = new Date(timestamp);\n      const date = dateValue.toISOString().slice(0, 10);\n      const hour = dateValue.getUTCHours() || 0;\n      const min = dateValue.getUTCMinutes() || 0;\n      const sec = dateValue.getUTCSeconds() || 0;\n\n      return {\n        date,\n        hour,\n        min,\n        sec\n      };\n    },\n    convertToTimestamp() {\n      const date = new Date(this.date);\n      date.setUTCHours(this.hour || 0);\n      date.setUTCMinutes(this.min || 0);\n      date.setUTCSeconds(this.sec || 0);\n\n      return date.getTime();\n    },\n    formatDatetime(timestamp = this.model.value) {\n      if (!timestamp) {\n        this.resetValues();\n        return;\n      }\n\n      const datetime = this.convertToDatetime(timestamp);\n      this.setDatetime(datetime.date, datetime.hour, datetime.min, datetime.sec);\n    },\n    onChange() {\n      const timestamp = this.convertToTimestamp();\n      const model = this.model;\n      model.validate = () => this.validate(timestamp);\n\n      const data = {\n        model,\n        value: new Date(timestamp).toISOString()\n      };\n\n      this.$emit('on-change', data);\n    },\n    resetValues() {\n      this.setDatetime();\n    },\n    setDatetime(date = '', hour = 0, min = 0, sec = 0) {\n      this.date = date.toString();\n      this.hour = hour;\n      this.min = min;\n      this.sec = sec;\n    },\n    validate(timestamp) {\n      const valid = timestamp > 0 && this.$refs.dateTimeForm.checkValidity();\n      if (!valid) {\n        this.$refs.dateTimeForm.reportValidity();\n      }\n\n      return valid;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/FileInput.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <input\n        id=\"fileElem\"\n        ref=\"fileInput\"\n        type=\"file\"\n        :accept=\"acceptableFileTypes\"\n        style=\"display: none\"\n        aria-labelledby=\"fileSelect\"\n      />\n      <button id=\"fileSelect\" class=\"c-button\" @click=\"selectFile\">\n        {{ name }}\n      </button>\n      <button\n        v-if=\"removable\"\n        class=\"c-button icon-trash\"\n        title=\"Remove file\"\n        @click=\"removeFile\"\n      ></button>\n    </span>\n  </span>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      fileInfo: undefined\n    };\n  },\n  computed: {\n    name() {\n      const fileInfo = this.fileInfo || this.model.value;\n\n      return (fileInfo && fileInfo.name) || this.model.text;\n    },\n    removable() {\n      return (this.fileInfo || this.model.value) && this.model.removable;\n    },\n    acceptableFileTypes() {\n      if (this.model.type) {\n        return this.model.type;\n      }\n\n      return 'application/json';\n    }\n  },\n  mounted() {\n    this.$refs.fileInput.addEventListener('change', this.handleFiles, false);\n  },\n  methods: {\n    handleFiles() {\n      const fileList = this.$refs.fileInput.files;\n      const file = fileList[0];\n\n      if (this.acceptableFileTypes === 'application/json') {\n        this.readFile(file);\n      } else {\n        this.handleRawFile(file);\n      }\n    },\n    readFile(file) {\n      const self = this;\n      const fileReader = new FileReader();\n      const fileInfo = {};\n      fileInfo.name = file.name;\n      fileReader.onload = function (event) {\n        fileInfo.body = event.target.result;\n        self.fileInfo = fileInfo;\n\n        const data = {\n          model: self.model,\n          value: fileInfo\n        };\n        self.$emit('on-change', data);\n      };\n\n      fileReader.onerror = function (error) {\n        console.error('fileReader error', error);\n      };\n\n      fileReader.readAsText(file);\n    },\n    handleRawFile(file) {\n      const fileInfo = {\n        name: file.name,\n        body: file\n      };\n\n      this.fileInfo = Object.assign({}, fileInfo);\n\n      const data = {\n        model: this.model,\n        value: fileInfo\n      };\n\n      this.$emit('on-change', data);\n    },\n    selectFile() {\n      this.$refs.fileInput.click();\n    },\n    removeFile() {\n      this.model.value = undefined;\n      this.fileInfo = undefined;\n      const data = {\n        model: this.model,\n        value: undefined\n      };\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/LocatorField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <MctTree\n    :is-selector-tree=\"true\"\n    :initial-selection=\"initialSelection\"\n    @tree-item-selection=\"handleItemSelection\"\n  />\n</template>\n\n<script>\nimport MctTree from '@/ui/layout/MctTree.vue';\n\nexport default {\n  components: {\n    MctTree\n  },\n  inject: ['openmct'],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  computed: {\n    initialSelection() {\n      return this.model.parent || this.model.value?.[0];\n    }\n  },\n  methods: {\n    handleItemSelection(item) {\n      const data = {\n        model: this.model,\n        value: item.objectPath\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/NumberField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <input\n        v-model=\"field\"\n        :aria-label=\"model.name\"\n        type=\"number\"\n        :min=\"model.min\"\n        :max=\"model.max\"\n        :step=\"model.step\"\n        @input=\"updateText()\"\n      />\n    </span>\n  </span>\n</template>\n\n<script>\nimport { throttle } from 'lodash';\n\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      field: this.model.value\n    };\n  },\n  mounted() {\n    this.updateText = throttle(this.updateText.bind(this), 200);\n  },\n  methods: {\n    updateText() {\n      const data = {\n        model: this.model,\n        value: this.field\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/SelectField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"form-control select-field\">\n    <select\n      v-model=\"selected\"\n      required=\"model.required\"\n      name=\"mctControl\"\n      :aria-label=\"model.ariaLabel || model.name\"\n      @change=\"onChange($event)\"\n    >\n      <option v-for=\"option in model.options\" :key=\"option.name\" :value=\"option.value\">\n        {{ option.name }}\n      </option>\n    </select>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      selected: this.model.value\n    };\n  },\n  methods: {\n    onChange() {\n      const data = {\n        model: this.model,\n        value: this.selected\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/TextAreaField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <textarea\n        :id=\"`${model.key}-textarea`\"\n        v-model=\"field\"\n        type=\"text\"\n        :size=\"model.size\"\n        @input=\"updateText()\"\n      >\n      </textarea>\n    </span>\n  </span>\n</template>\n\n<script>\nimport { throttle } from 'lodash';\n\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      field: this.model.value\n    };\n  },\n  mounted() {\n    this.updateText = throttle(this.updateText.bind(this), 500);\n  },\n  methods: {\n    updateText() {\n      const data = {\n        model: this.model,\n        value: this.field\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/TextField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <input\n        :id=\"`form-${model.key}`\"\n        v-model=\"field\"\n        :name=\"model.key\"\n        type=\"text\"\n        :size=\"model.size\"\n        @input=\"updateText()\"\n      />\n    </span>\n  </span>\n</template>\n\n<script>\nimport { throttle } from 'lodash';\n\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      field: this.model.value\n    };\n  },\n  mounted() {\n    this.updateText = throttle(this.updateText.bind(this), 500);\n  },\n  methods: {\n    updateText() {\n      const data = {\n        model: this.model,\n        value: this.field\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/components/controls/ToggleSwitchField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control shell\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <ToggleSwitch\n        id=\"switchId\"\n        :checked=\"isChecked\"\n        :name=\"model.name\"\n        @change=\"toggleCheckBox\"\n      />\n    </span>\n  </span>\n</template>\n\n<script>\nimport { v4 as uuid } from 'uuid';\n\nimport ToggleSwitch from '@/ui/components/ToggleSwitch.vue';\n\nimport toggleMixin from '../../toggle-check-box-mixin.js';\n\nexport default {\n  components: {\n    ToggleSwitch\n  },\n  mixins: [toggleMixin],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      switchId: `toggleSwitch-${uuid}`,\n      isChecked: this.model.value\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/forms/toggle-check-box-mixin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default {\n  emits: ['on-change'],\n  data() {\n    return {\n      isChecked: false\n    };\n  },\n  methods: {\n    toggleCheckBox(event) {\n      this.isChecked = !this.isChecked;\n\n      const data = {\n        model: this.model,\n        value: this.isChecked\n      };\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n"
  },
  {
    "path": "src/api/indicators/IndicatorAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js';\nimport SimpleIndicator from './SimpleIndicator.js';\n\n/**\n * The Indicator API is used to add indicators to the Open MCT UI.\n * An indicator appears in the top navigation bar and can be used to\n * display information or trigger actions.\n *\n * @extends EventEmitter\n */\nclass IndicatorAPI extends EventEmitter {\n  /** @type {import('../../../openmct.js').OpenMCT} */\n  openmct;\n  constructor(openmct) {\n    super();\n\n    this.openmct = openmct;\n    this.indicatorObjects = [];\n  }\n\n  getIndicatorObjectsByPriority() {\n    const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);\n\n    return sortedIndicators;\n  }\n\n  simpleIndicator() {\n    return new SimpleIndicator(this.openmct);\n  }\n\n  /**\n   * @typedef {import('vue').Component} VueComponent\n   */\n\n  /**\n   * @typedef {Object} Indicator\n   * @property {HTMLElement} [element] - The HTML element of the indicator. Optional if using vueComponent.\n   * @property {VueComponent|Promise<VueComponent>} [vueComponent] - The Vue component for the indicator. Optional if using element.\n   * @property {string} key - The unique key for the indicator.\n   * @property {number} priority - The priority of the indicator (default: -1).\n   */\n\n  /**\n   * Adds an indicator to the API.\n   *\n   * @param {Indicator} indicator - The indicator object to add.\n   *\n   * @description\n   * The indicator object is a simple object with two main attributes:\n   * - 'element': An HTMLElement (optional if using vueComponent).\n   * - 'priority': An integer specifying its order in the layout. Lower priority\n   *   places the element further to the right. If undefined, defaults to -1.\n   *\n   * A convenience function `.simpleIndicator()` is provided to create a default\n   * Open MCT indicator that can be passed to `.add(indicator)`. This indicator\n   * exposes functions for customizing its appearance and behavior.\n   *\n   * Example usage:\n   * ```\n   * const myIndicator = openmct.indicators.simpleIndicator();\n   * openmct.indicators.add(myIndicator);\n   *\n   * myIndicator.text(\"Hello World!\");\n   * myIndicator.iconClass(\"icon-info\");\n   * ```\n   *\n   * For Vue components, pass the component directly as the 'vueComponent'\n   * attribute. This can be a Vue component or a promise resolving to a\n   * Vue component for asynchronous rendering.\n   */\n  add(indicator) {\n    if (!indicator.priority) {\n      indicator.priority = this.openmct.priority.DEFAULT;\n    }\n    if (!indicator.vueComponent) {\n      indicator.vueComponent = vueWrapHtmlElement(indicator.element);\n    }\n\n    this.indicatorObjects.push(indicator);\n\n    this.emit('addIndicator', indicator);\n  }\n}\n\nexport default IndicatorAPI;\n"
  },
  {
    "path": "src/api/indicators/IndicatorAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { defineComponent } from 'vue';\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport SimpleIndicator from './SimpleIndicator.js';\n\ndescribe('The Indicator API', () => {\n  let openmct;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  function generateHTMLIndicator(className, label, priority) {\n    const element = document.createElement('div');\n    element.classList.add(className);\n    const textNode = document.createTextNode(label);\n    element.appendChild(textNode);\n    const testIndicator = {\n      element,\n      priority\n    };\n\n    return testIndicator;\n  }\n\n  function generateVueIndicator(priority) {\n    return {\n      vueComponent: defineComponent({\n        template: '<div class=\"test-indicator\">This is a test indicator</div>'\n      }),\n      priority\n    };\n  }\n\n  it('can register an HTML indicator', () => {\n    const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2);\n    openmct.indicators.add(testIndicator);\n    expect(openmct.indicators.indicatorObjects).toBeDefined();\n    // notifier indicator is installed by default\n    expect(openmct.indicators.indicatorObjects.length).toBe(2);\n  });\n\n  it('can register a Vue indicator', () => {\n    const testIndicator = generateVueIndicator(2);\n    openmct.indicators.add(testIndicator);\n    expect(openmct.indicators.indicatorObjects).toBeDefined();\n    // notifier indicator is installed by default\n    expect(openmct.indicators.indicatorObjects.length).toBe(2);\n  });\n\n  it('can order indicators based on priority', () => {\n    const testIndicator1 = generateHTMLIndicator(\n      'test-indicator-1',\n      'This is a test indicator',\n      openmct.priority.LOW\n    );\n    openmct.indicators.add(testIndicator1);\n\n    const testIndicator2 = generateHTMLIndicator(\n      'test-indicator-2',\n      'This is another test indicator',\n      openmct.priority.DEFAULT\n    );\n    openmct.indicators.add(testIndicator2);\n\n    const testIndicator3 = generateHTMLIndicator(\n      'test-indicator-3',\n      'This is yet another test indicator',\n      openmct.priority.LOW\n    );\n    openmct.indicators.add(testIndicator3);\n\n    const testIndicator4 = generateHTMLIndicator(\n      'test-indicator-4',\n      'This is yet another test indicator',\n      openmct.priority.HIGH\n    );\n    openmct.indicators.add(testIndicator4);\n\n    const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT);\n    openmct.indicators.add(testIndicator5);\n\n    expect(openmct.indicators.indicatorObjects.length).toBe(6);\n    const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();\n    expect(indicatorObjectsByPriority.length).toBe(6);\n    expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);\n  });\n\n  it('the simple indicator can be added', () => {\n    const simpleIndicator = new SimpleIndicator(openmct);\n    openmct.indicators.add(simpleIndicator);\n\n    expect(openmct.indicators.indicatorObjects.length).toBe(2);\n  });\n});\n"
  },
  {
    "path": "src/api/indicators/SimpleIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport { convertTemplateToHTML } from '@/utils/template/templateHelpers';\n\nimport indicatorTemplate from './res/indicator-template.html';\n\nconst DEFAULT_ICON_CLASS = 'icon-info';\n\nclass SimpleIndicator extends EventEmitter {\n  constructor(openmct) {\n    super();\n\n    this.openmct = openmct;\n    this.element = convertTemplateToHTML(indicatorTemplate)[0];\n    this.priority = openmct.priority.DEFAULT;\n\n    this.textElement = this.element.querySelector('.js-indicator-text');\n\n    //Set defaults\n    this.text('New Indicator');\n    this.description('');\n    this.iconClass(DEFAULT_ICON_CLASS);\n\n    this.click = this.click.bind(this);\n\n    this.element.addEventListener('click', this.click);\n    openmct.once('destroy', () => {\n      this.removeAllListeners();\n      this.element.removeEventListener('click', this.click);\n    });\n  }\n\n  text(text) {\n    if (text !== undefined && text !== this.textValue) {\n      this.textValue = text;\n      this.textElement.innerText = text;\n\n      if (!text) {\n        this.element.classList.add('hidden');\n      } else {\n        this.element.classList.remove('hidden');\n      }\n    }\n\n    return this.textValue;\n  }\n\n  description(description) {\n    if (description !== undefined && description !== this.descriptionValue) {\n      this.descriptionValue = description;\n      this.element.title = description;\n    }\n\n    return this.descriptionValue;\n  }\n\n  iconClass(iconClass) {\n    if (iconClass !== undefined && iconClass !== this.iconClassValue) {\n      // element.classList is precious and throws errors if you try and add\n      // or remove empty strings\n      if (this.iconClassValue) {\n        this.element.classList.remove(this.iconClassValue);\n      }\n\n      if (iconClass) {\n        this.element.classList.add(iconClass);\n      }\n\n      this.iconClassValue = iconClass;\n    }\n\n    return this.iconClassValue;\n  }\n\n  statusClass(statusClass) {\n    if (arguments.length === 1 && statusClass !== this.statusClassValue) {\n      if (this.statusClassValue) {\n        this.element.classList.remove(this.statusClassValue);\n      }\n\n      if (statusClass !== undefined) {\n        this.element.classList.add(statusClass);\n      }\n\n      this.statusClassValue = statusClass;\n    }\n\n    return this.statusClassValue;\n  }\n\n  click(event) {\n    this.emit('click', event);\n  }\n\n  getElement() {\n    return this.element;\n  }\n}\n\nexport default SimpleIndicator;\n"
  },
  {
    "path": "src/api/indicators/res/indicator-template.html",
    "content": "<div class=\"c-indicator c-indicator--clickable c-indicator--simple\" title=\"\">\n  <span class=\"label js-indicator-text c-indicator__label\"></span>\n</div>\n"
  },
  {
    "path": "src/api/menu/MenuAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Menu, { MENU_PLACEMENT } from './menu.js';\n\n/**\n * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from\n * custom HTML elements.\n */\nclass MenuAPI {\n  /**\n   * @param {import('openmct').OpenMCT} openmct\n   */\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.menuPlacement = MENU_PLACEMENT;\n    this.showMenu = this.showMenu.bind(this);\n    this.showSuperMenu = this.showSuperMenu.bind(this);\n\n    this._clearMenuComponent = this._clearMenuComponent.bind(this);\n    this._showObjectMenu = this._showObjectMenu.bind(this);\n  }\n\n  /**\n   * Show popup menu\n   * @param {number} x - x-coordinates for popup\n   * @param {number} y - y-coordinates for popup\n   * @param {Action[]|Action[][]} items - collection of actions or collection of groups of actions\n   * @param {MenuOptions} [menuOptions] - The options for Menu\n   */\n  showMenu(x, y, items, menuOptions) {\n    this._createMenuComponent(x, y, items, menuOptions);\n\n    this.menuComponent.showMenu();\n  }\n\n  /**\n   * Convert actions to menu items\n   * @param {Action[]} actions - collection of actions\n   * @param {import('openmct').ObjectPath} objectPath - The object path\n   * @param {import('openmct').ViewProvider} view - The view provider\n   * @returns {Action[]}\n   */\n  actionsToMenuItems(actions, objectPath, view) {\n    return actions.map((action) => {\n      const isActionGroup = Array.isArray(action);\n      if (isActionGroup) {\n        action = this.actionsToMenuItems(action, objectPath, view);\n      } else {\n        action.onItemClicked = () => action.invoke(objectPath, view);\n      }\n\n      return action;\n    });\n  }\n\n  /**\n   * Show popup menu with description of item on hover\n   * @param {number} x - x-coordinates for popup\n   * @param {number} y - y-coordinates for popup\n   * @param {Action[]|Action[][]} actions - collection of actions or collection of groups of actions\n   * @param {MenuOptions} [menuOptions] - The options for Menu\n   */\n  showSuperMenu(x, y, actions, menuOptions) {\n    this._createMenuComponent(x, y, actions, menuOptions);\n\n    this.menuComponent.showSuperMenu();\n  }\n\n  /**\n   * Clear the menu component\n   * @private\n   */\n  _clearMenuComponent() {\n    this.menuComponent = undefined;\n    delete this.menuComponent;\n  }\n\n  /**\n   * Create a menu component\n   * @param {number} x - x-coordinates for popup\n   * @param {number} y - y-coordinates for popup\n   * @param {Action[]|Action[][]} actions - collection of actions or collection of groups of actions\n   * @param {MenuOptions} menuOptions - The options for Menu\n   * @private\n   */\n  _createMenuComponent(x, y, actions, menuOptions = {}) {\n    if (this.menuComponent) {\n      this.menuComponent.dismiss();\n    }\n\n    let options = {\n      x,\n      y,\n      actions,\n      ...menuOptions\n    };\n\n    this.menuComponent = new Menu(options);\n    this.menuComponent.once('destroy', this._clearMenuComponent);\n  }\n\n  /**\n   * Show object menu\n   * @param {import('openmct').ObjectPath} objectPath - The object path\n   * @param {number} x - x-coordinates for popup\n   * @param {number} y - y-coordinates for popup\n   * @param {string[]} actionsToBeIncluded - Actions to be included in the menu\n   * @private\n   */\n  _showObjectMenu(objectPath, x, y, actionsToBeIncluded) {\n    let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(\n      objectPath,\n      actionsToBeIncluded\n    );\n\n    this.showMenu(x, y, applicableActions);\n  }\n}\nexport default MenuAPI;\n\n/**\n * @typedef {Object} MenuOptions\n * @property {string} [menuClass] - Class for popup menu\n * @property {MENU_PLACEMENT} [placement] - Placement for menu relative to click\n * @property {() => void} [onDestroy] - callback function: invoked when menu is destroyed\n */\n\n/**\n * @typedef {import('openmct').Action} Action\n */\n"
  },
  {
    "path": "src/api/menu/MenuAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { nextTick } from 'vue';\n\nimport { createMouseEvent, createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport Menu from './menu.js';\nimport MenuAPI from './MenuAPI.js';\n\ndescribe('The Menu API', () => {\n  let openmct;\n  let appHolder;\n  let menuAPI;\n  let actionsArray;\n  let result;\n  let menuElement;\n\n  const x = 8;\n  const y = 16;\n\n  const menuOptions = {\n    onDestroy: () => {\n      console.log('default onDestroy');\n    }\n  };\n\n  beforeEach((done) => {\n    appHolder = document.createElement('div');\n    appHolder.style.display = 'block';\n    appHolder.style.width = '1920px';\n    appHolder.style.height = '1080px';\n\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    menuAPI = new MenuAPI(openmct);\n    actionsArray = [\n      {\n        key: 'test-css-class-1',\n        name: 'Test Action 1',\n        cssClass: 'icon-clock',\n        description: 'This is a test action 1',\n        onItemClicked: () => {\n          result = 'Test Action 1 Invoked';\n        }\n      },\n      {\n        key: 'test-css-class-2',\n        name: 'Test Action 2',\n        cssClass: 'icon-clock',\n        description: 'This is a test action 2',\n        onItemClicked: () => {\n          result = 'Test Action 2 Invoked';\n        }\n      }\n    ];\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('showMenu method', () => {\n    beforeAll(() => {\n      spyOn(menuOptions, 'onDestroy').and.callThrough();\n    });\n\n    it('creates an instance of Menu when invoked', (done) => {\n      menuOptions.onDestroy = done;\n\n      menuAPI.showMenu(x, y, actionsArray, menuOptions);\n\n      expect(menuAPI.menuComponent).toBeInstanceOf(Menu);\n      document.body.click();\n    });\n\n    describe('creates a menu component', () => {\n      it('with all the actions passed in', (done) => {\n        menuOptions.onDestroy = done;\n\n        menuAPI.showMenu(x, y, actionsArray, menuOptions);\n        menuElement = document.querySelector('.c-menu');\n        expect(menuElement).toBeDefined();\n\n        const listItems = menuElement.children[0].children;\n\n        expect(listItems.length).toEqual(actionsArray.length);\n        document.body.click();\n      });\n\n      it('with click-able menu items, that will invoke the correct callBack', (done) => {\n        menuOptions.onDestroy = done;\n\n        menuAPI.showMenu(x, y, actionsArray, menuOptions);\n\n        menuElement = document.querySelector('.c-menu');\n        const listItem1 = menuElement.children[0].children[0];\n\n        listItem1.click();\n\n        expect(result).toEqual('Test Action 1 Invoked');\n      });\n\n      it('dismisses the menu when action is clicked on', (done) => {\n        menuOptions.onDestroy = done;\n\n        menuAPI.showMenu(x, y, actionsArray, menuOptions);\n\n        menuElement = document.querySelector('.c-menu');\n        const listItem1 = menuElement.children[0].children[0];\n        listItem1.click();\n\n        menuElement = document.querySelector('.c-menu');\n\n        expect(menuElement).toBeNull();\n      });\n\n      it('invokes the destroy method when menu is dismissed', (done) => {\n        menuOptions.onDestroy = done;\n\n        spyOn(menuAPI, '_clearMenuComponent').and.callThrough();\n\n        menuAPI.showMenu(x, y, actionsArray, menuOptions);\n\n        document.body.click();\n\n        expect(menuAPI._clearMenuComponent).toHaveBeenCalled();\n      });\n\n      it('invokes the onDestroy callback if passed in', (done) => {\n        let count = 0;\n        menuOptions.onDestroy = () => {\n          count++;\n          expect(count).toEqual(1);\n          done();\n        };\n\n        menuAPI.showMenu(x, y, actionsArray, menuOptions);\n\n        document.body.click();\n      });\n    });\n  });\n\n  describe('superMenu method', () => {\n    it('creates a superMenu', (done) => {\n      menuOptions.onDestroy = done;\n\n      menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);\n      menuElement = document.querySelector('.c-super-menu__menu');\n\n      expect(menuElement).not.toBeNull();\n      document.body.click();\n    });\n\n    it('Mouse over a superMenu shows correct description', (done) => {\n      menuOptions.onDestroy = done;\n\n      menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);\n      menuElement = document.querySelector('.c-super-menu__menu');\n\n      const superMenuItem = menuElement.querySelector('li');\n      const mouseOverEvent = createMouseEvent('mouseover');\n\n      superMenuItem.dispatchEvent(mouseOverEvent);\n      const itemDescription = document.querySelector('.l-item-description__description');\n\n      nextTick(() => {\n        expect(menuElement).not.toBeNull();\n        expect(itemDescription.innerText).toEqual(actionsArray[0].description);\n\n        document.body.click();\n      });\n    });\n  });\n\n  describe('Menu Placements', () => {\n    it('default menu position BOTTOM_RIGHT', (done) => {\n      menuOptions.onDestroy = done;\n\n      menuAPI.showMenu(x, y, actionsArray, menuOptions);\n      menuElement = document.querySelector('.c-menu');\n\n      const boundingClientRect = menuElement.getBoundingClientRect();\n      const left = boundingClientRect.left;\n      const top = boundingClientRect.top;\n\n      expect(left).toEqual(x);\n      expect(top).toEqual(y);\n\n      document.body.click();\n    });\n\n    it('menu position BOTTOM_RIGHT', (done) => {\n      menuOptions.onDestroy = done;\n      menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;\n\n      menuAPI.showMenu(x, y, actionsArray, menuOptions);\n      menuElement = document.querySelector('.c-menu');\n\n      const boundingClientRect = menuElement.getBoundingClientRect();\n      const left = boundingClientRect.left;\n      const top = boundingClientRect.top;\n\n      expect(left).toEqual(x);\n      expect(top).toEqual(y);\n\n      document.body.click();\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/menu/components/MenuComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div :aria-label=\"optionsLabel\" class=\"c-menu\" :class=\"options.menuClass\" :style=\"styleObject\">\n    <ul v-if=\"options.actions.length && options.actions[0].length\" role=\"menu\">\n      <template v-for=\"(actionGroups, index) in options.actions\" :key=\"index\">\n        <div role=\"group\">\n          <li\n            v-for=\"action in actionGroups\"\n            :key=\"action.name\"\n            role=\"menuitem\"\n            :aria-disabled=\"action.isDisabled\"\n            :aria-label=\"action.name\"\n            aria-describedby=\"item-description\"\n            :class=\"action.cssClass\"\n            :title=\"action.description\"\n            @click=\"action.onItemClicked\"\n            @mouseover=\"toggleItem(action)\"\n            @mouseleave=\"toggleItem()\"\n          >\n            {{ action.name }}\n          </li>\n          <div\n            v-if=\"index !== options.actions.length - 1\"\n            :key=\"index\"\n            role=\"separator\"\n            class=\"c-menu__section-separator\"\n          ></div>\n          <li v-if=\"actionGroups.length === 0\" :key=\"index\">No actions defined.</li>\n        </div>\n      </template>\n    </ul>\n\n    <ul v-else role=\"menu\">\n      <li\n        v-for=\"action in options.actions\"\n        :key=\"action.name\"\n        role=\"menuitem\"\n        aria-describedby=\"item-description\"\n        :aria-disabled=\"action.isDisabled\"\n        :class=\"action.cssClass\"\n        :aria-label=\"action.name\"\n        :title=\"action.description\"\n        @click=\"action.onItemClicked\"\n        @mouseover=\"toggleItem(action)\"\n        @mouseleave=\"toggleItem()\"\n      >\n        {{ action.name }}\n      </li>\n      <li v-if=\"options.actions.length === 0\">No actions defined.</li>\n    </ul>\n    <div v-if=\"hoveredItem\" id=\"item-description\" class=\"visually-hidden\" aria-live=\"polite\">\n      <span v-if=\"hoveredItem.name\">{{ hoveredItem.name }}</span>\n      <span v-if=\"hoveredItem.description\">: {{ hoveredItem.description }}</span>\n    </div>\n  </div>\n</template>\n\n<script>\nimport popupMenuMixin from '../mixins/popupMenuMixin.js';\nexport default {\n  mixins: [popupMenuMixin],\n  inject: ['options'],\n  data() {\n    return {\n      hoveredItem: null\n    };\n  },\n  computed: {\n    optionsLabel() {\n      const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';\n      return label;\n    }\n  },\n  methods: {\n    toggleItem(action) {\n      this.hoveredItem = action ?? null;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/menu/components/SuperMenu.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    :aria-label=\"optionsLabel\"\n    class=\"c-menu\"\n    :class=\"[options.menuClass, 'c-super-menu']\"\n    :style=\"styleObject\"\n  >\n    <ul\n      v-if=\"options.actions.length && options.actions[0].length\"\n      role=\"menu\"\n      class=\"c-super-menu__menu\"\n    >\n      <template v-for=\"(actionGroups, index) in options.actions\" :key=\"index\">\n        <div role=\"group\">\n          <li\n            v-for=\"action in actionGroups\"\n            :key=\"action.name\"\n            role=\"menuitem\"\n            :aria-disabled=\"action.isDisabled\"\n            aria-describedby=\"item-description\"\n            :class=\"action.cssClass\"\n            @click=\"action.onItemClicked\"\n            @mouseover=\"toggleItemDescription(action)\"\n            @mouseleave=\"toggleItemDescription()\"\n          >\n            {{ action.name }}\n          </li>\n          <div\n            v-if=\"index !== options.actions.length - 1\"\n            :key=\"index\"\n            role=\"separator\"\n            class=\"c-menu__section-separator\"\n          ></div>\n          <li v-if=\"actionGroups.length === 0\" :key=\"index\">No actions defined.</li>\n        </div></template\n      >\n    </ul>\n\n    <ul v-else class=\"c-super-menu__menu\" role=\"menu\">\n      <li\n        v-for=\"action in options.actions\"\n        :key=\"action.name\"\n        role=\"menuitem\"\n        :class=\"action.cssClass\"\n        :aria-label=\"action.name\"\n        aria-describedby=\"item-description\"\n        @click=\"action.onItemClicked\"\n        @mouseover=\"toggleItemDescription(action)\"\n        @mouseleave=\"toggleItemDescription()\"\n      >\n        {{ action.name }}\n      </li>\n      <li v-if=\"options.actions.length === 0\">No actions defined.</li>\n    </ul>\n\n    <div aria-live=\"polite\" class=\"c-super-menu__item-description\">\n      <div :class=\"itemDescriptionIconClass\"></div>\n      <div class=\"l-item-description__name\">\n        {{ hoveredItemName }}\n      </div>\n      <div id=\"item-description\" class=\"l-item-description__description\">\n        {{ hoveredItemDescription }}\n      </div>\n    </div>\n  </div>\n</template>\n<script>\nimport popupMenuMixin from '../mixins/popupMenuMixin.js';\nexport default {\n  mixins: [popupMenuMixin],\n  inject: ['options'],\n  data() {\n    return {\n      hoveredItem: null\n    };\n  },\n  computed: {\n    optionsLabel() {\n      const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';\n      return label;\n    },\n    itemDescriptionIconClass() {\n      const iconClass = ['l-item-description__icon'];\n      if (this.hoveredItem) {\n        iconClass.push('bg-' + this.hoveredItem.cssClass);\n      }\n      return iconClass;\n    },\n    hoveredItemName() {\n      return this.hoveredItem?.name ?? '';\n    },\n    hoveredItemDescription() {\n      return this.hoveredItem?.description ?? '';\n    }\n  },\n  methods: {\n    toggleItemDescription(action = null) {\n      const hoveredItem = {\n        name: action?.name,\n        description: action?.description,\n        cssClass: action?.cssClass\n      };\n\n      this.hoveredItem = hoveredItem;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/menu/menu.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\nimport { h } from 'vue';\n\nimport MenuComponent from './components/MenuComponent.vue';\nimport SuperMenuComponent from './components/SuperMenu.vue';\n\n/**\n * Enum for menu placement options.\n * @readonly\n * @enum {string}\n */\nexport const MENU_PLACEMENT = {\n  TOP: 'top',\n  TOP_LEFT: 'top-left',\n  TOP_RIGHT: 'top-right',\n  BOTTOM: 'bottom',\n  BOTTOM_LEFT: 'bottom-left',\n  BOTTOM_RIGHT: 'bottom-right',\n  LEFT: 'left',\n  RIGHT: 'right'\n};\n\n/**\n * Class representing a menu.\n * @extends EventEmitter\n */\nclass Menu extends EventEmitter {\n  /**\n   * Create a menu.\n   * @param {MenuOptions} options - The options for the menu.\n   */\n  constructor(options) {\n    super();\n\n    this.options = options;\n    if (options.onDestroy) {\n      this.once('destroy', options.onDestroy);\n    }\n\n    this.dismiss = this.dismiss.bind(this);\n    this.show = this.show.bind(this);\n    this.showMenu = this.showMenu.bind(this);\n    this.showSuperMenu = this.showSuperMenu.bind(this);\n  }\n\n  /**\n   * Dismiss the menu.\n   */\n  dismiss() {\n    if (this.destroy) {\n      this.destroy();\n      this.destroy = null;\n    }\n    document.removeEventListener('click', this.dismiss);\n    this.emit('destroy');\n  }\n\n  /**\n   * Show the menu component.\n   */\n  showMenu() {\n    if (this.destroy) {\n      return;\n    }\n    const { vNode, destroy } = mount({\n      render() {\n        return h(MenuComponent);\n      },\n      provide: {\n        options: this.options\n      }\n    });\n\n    this.el = vNode.el;\n    this.destroy = destroy;\n\n    this.show();\n  }\n\n  /**\n   * Show the super menu component.\n   */\n  showSuperMenu() {\n    const { vNode, destroy } = mount({\n      data() {\n        return {\n          top: '0px',\n          left: '0px'\n        };\n      },\n      render() {\n        return h(SuperMenuComponent);\n      },\n      provide: {\n        options: this.options\n      }\n    });\n\n    this.el = vNode.el;\n    this.destroy = destroy;\n\n    this.show();\n  }\n\n  /**\n   * Show the menu.\n   */\n  show() {\n    document.body.appendChild(this.el);\n    document.addEventListener('click', this.dismiss);\n  }\n}\n\nexport default Menu;\n\n/**\n * @typedef {Object} MenuOptions\n * @property {() => void} [onDestroy] - Callback function to be called when the menu is destroyed.\n */\n"
  },
  {
    "path": "src/api/menu/mixins/popupMenuMixin.js",
    "content": "import { MENU_PLACEMENT } from '../menu.js';\nexport default {\n  methods: {\n    /**\n     * @private\n     */\n    _calculatePopupPosition(menuElement) {\n      let menuDimensions = menuElement.getBoundingClientRect();\n\n      if (!this.options.placement) {\n        this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT;\n      }\n\n      const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions);\n\n      return this._preventMenuOverflow(menuPosition, menuDimensions);\n    },\n    /**\n     * @private\n     */\n    _getMenuPositionBasedOnPlacement(menuDimensions) {\n      let eventPosX = this.options.x;\n      let eventPosY = this.options.y;\n\n      // Adjust popup menu based on placement\n      switch (this.options.placement) {\n        case MENU_PLACEMENT.TOP:\n          eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);\n          eventPosY = this.options.y - menuDimensions.height;\n          break;\n        case MENU_PLACEMENT.BOTTOM:\n          eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);\n          break;\n        case MENU_PLACEMENT.LEFT:\n          eventPosX = this.options.x - menuDimensions.width;\n          eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);\n          break;\n        case MENU_PLACEMENT.RIGHT:\n          eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);\n          break;\n        case MENU_PLACEMENT.TOP_LEFT:\n          eventPosX = this.options.x - menuDimensions.width;\n          eventPosY = this.options.y - menuDimensions.height;\n          break;\n        case MENU_PLACEMENT.TOP_RIGHT:\n          eventPosY = this.options.y - menuDimensions.height;\n          break;\n        case MENU_PLACEMENT.BOTTOM_LEFT:\n          eventPosX = this.options.x - menuDimensions.width;\n          break;\n        case MENU_PLACEMENT.BOTTOM_RIGHT:\n          break;\n      }\n\n      return {\n        x: eventPosX,\n        y: eventPosY\n      };\n    },\n    /**\n     * @private\n     */\n    _preventMenuOverflow(menuPosition, menuDimensions) {\n      let { x: eventPosX, y: eventPosY } = menuPosition;\n      let overflowX = eventPosX + menuDimensions.width - document.body.clientWidth;\n      let overflowY = eventPosY + menuDimensions.height - document.body.clientHeight;\n\n      if (overflowX > 0) {\n        eventPosX = eventPosX - overflowX;\n      }\n\n      if (overflowY > 0) {\n        eventPosY = eventPosY - overflowY;\n      }\n\n      if (eventPosX < 0) {\n        eventPosX = 0;\n      }\n\n      if (eventPosY < 0) {\n        eventPosY = 0;\n      }\n\n      return {\n        x: eventPosX,\n        y: eventPosY\n      };\n    }\n  },\n  mounted() {\n    this.$nextTick(() => {\n      const position = this._calculatePopupPosition(this.$el);\n      this.top = position.y;\n      this.left = position.x;\n    });\n  },\n  data() {\n    return {\n      top: '0px',\n      left: '0px'\n    };\n  },\n  computed: {\n    styleObject() {\n      return {\n        top: `${this.top}px`,\n        left: `${this.left}px`\n      };\n    }\n  }\n};\n"
  },
  {
    "path": "src/api/notifications/NotificationAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This bundle implements the notification service, which can be used to\n * show banner notifications to the user. Banner notifications\n * are used to inform users of events in a non-intrusive way. As\n * much as possible, notifications share a model with blocking\n * dialogs so that the same information can be provided in a dialog\n * and then minimized to a banner notification if needed.\n */\nimport { EventEmitter } from 'eventemitter3';\nimport moment from 'moment';\n\nconst DEFAULT_AUTO_DISMISS_TIMEOUT = 3000;\nconst MINIMIZE_ANIMATION_TIMEOUT = 300;\n\n/**\n * The notification service is responsible for informing the user of\n * events via the use of banner notifications.\n * @extends EventEmitter\n */\nexport default class NotificationAPI extends EventEmitter {\n  /**\n   * @constructor\n   */\n  constructor() {\n    super();\n    /** @type {Notification[]} */\n    this.notifications = [];\n    /** @type {{severity: \"info\" | \"alert\" | \"error\"}} */\n    this.highest = { severity: 'info' };\n\n    /**\n     * A context in which to hold the active notification and a\n     * handle to its timeout.\n     * @type {Notification | undefined}\n     */\n    this.activeNotification = undefined;\n  }\n\n  /**\n   * Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief\n   * period of time.\n   * @param {string} message The message to display to the user\n   * @param {NotificationOptions} [options] The notification options\n   * @returns {Notification}\n   */\n  info(message, options = {}) {\n    /** @type {NotificationModel} */\n    const notificationModel = {\n      message: message,\n      autoDismiss: true,\n      severity: 'info',\n      options\n    };\n\n    return this._notify(notificationModel);\n  }\n\n  /**\n   * Present an alert to the user.\n   * @param {string} message The message to display to the user.\n   * @param {NotificationOptions} [options] The notification options\n   * @returns {Notification}\n   */\n  alert(message, options = {}) {\n    const notificationModel = {\n      message: message,\n      severity: 'alert',\n      options\n    };\n\n    return this._notify(notificationModel);\n  }\n\n  /**\n   * Present an error message to the user\n   * @param {string} message The error message to display\n   * @param {NotificationOptions} [options] The notification options\n   * @returns {Notification}\n   */\n  error(message, options = {}) {\n    let notificationModel = {\n      message: message,\n      severity: 'error',\n      options\n    };\n\n    return this._notify(notificationModel);\n  }\n\n  /**\n   * Create a new progress notification. These notifications will contain a progress bar.\n   * @param {string} message The message to display\n   * @param {number | null} progressPerc A value between 0 and 100, or null.\n   * @param {string} [progressText] Text description of progress (eg. \"10 of 20 objects copied\").\n   * @returns {Notification}\n   */\n  progress(message, progressPerc, progressText) {\n    let notificationModel = {\n      message: message,\n      progressPerc: progressPerc,\n      progressText: progressText,\n      severity: 'info',\n      options: {}\n    };\n\n    return this._notify(notificationModel);\n  }\n\n  /**\n   * Dismiss all active notifications.\n   */\n  dismissAllNotifications() {\n    this.notifications = [];\n    this.emit('dismiss-all');\n  }\n\n  /**\n   * Minimize a notification. The notification will still be available\n   * from the notification list. Typically notifications with a\n   * severity of 'info' should not be minimized, but rather\n   * dismissed.\n   *\n   * @private\n   * @param {Notification | undefined} notification The notification to minimize\n   */\n  _minimize(notification) {\n    if (!notification) {\n      return;\n    }\n\n    //Check this is a known notification\n    let index = this.notifications.indexOf(notification);\n\n    if (this.activeTimeout) {\n      /*\n       * Method can be called manually (clicking dismiss) or\n       * automatically from an auto-timeout. this.activeTimeout\n       * acts as a semaphore to prevent race conditions. Cancel any\n       * timeout in progress (for the case where a manual dismiss\n       * has shortcut an active auto-dismiss), and clear the\n       * semaphore.\n       */\n      clearTimeout(this.activeTimeout);\n      delete this.activeTimeout;\n    }\n\n    if (index >= 0) {\n      notification.model.minimized = true;\n      notification.emit('minimized');\n      //Add a brief timeout before showing the next notification\n      // in order to allow the minimize animation to run through.\n      setTimeout(() => {\n        notification.emit('destroy');\n        this._setActiveNotification(this._selectNextNotification());\n      }, MINIMIZE_ANIMATION_TIMEOUT);\n    }\n  }\n\n  /**\n   * Completely removes a notification. This will dismiss it from the\n   * message banner and remove it from the list of notifications.\n   * Typically only notifications with a severity of info should be\n   * dismissed. If you're not sure whether to dismiss or minimize a\n   * notification, use {@link NotificationAPI#_dismissOrMinimize}.\n   *\n   * @private\n   * @param {Notification | undefined} notification The notification to dismiss\n   */\n  _dismiss(notification) {\n    if (!notification) {\n      return;\n    }\n\n    //Check this is a known notification\n    let index = this.notifications.indexOf(notification);\n\n    if (this.activeTimeout) {\n      /*\n       * Method can be called manually (clicking dismiss) or\n       * automatically from an auto-timeout. this.activeTimeout\n       * acts as a semaphore to prevent race conditions. Cancel any\n       * timeout in progress (for the case where a manual dismiss\n       * has shortcut an active auto-dismiss), and clear the\n       * semaphore.\n       */\n      clearTimeout(this.activeTimeout);\n      delete this.activeTimeout;\n    }\n\n    if (index >= 0) {\n      this.notifications.splice(index, 1);\n    }\n\n    this._setActiveNotification(this._selectNextNotification());\n    this._setHighestSeverity();\n    notification.emit('destroy');\n  }\n\n  /**\n   * Depending on the severity of the notification will selectively\n   * dismiss or minimize where appropriate.\n   *\n   * @private\n   * @param {Notification | undefined} notification The notification to dismiss or minimize\n   */\n  _dismissOrMinimize(notification) {\n    let model = notification?.model;\n    if (model?.severity === 'info') {\n      this._dismiss(notification);\n    } else {\n      this._minimize(notification);\n    }\n  }\n\n  /**\n   * Sets the highest severity notification.\n   * @private\n   */\n  _setHighestSeverity() {\n    let severity = {\n      info: 1,\n      alert: 2,\n      error: 3\n    };\n\n    this.highest.severity = this.notifications.reduce((previous, notification) => {\n      if (severity[notification.model.severity] > severity[previous]) {\n        return notification.model.severity;\n      } else {\n        return previous;\n      }\n    }, 'info');\n  }\n\n  /**\n   * Notifies the user of an event. If there is a banner notification\n   * already active, then it will be dismissed or minimized automatically,\n   * and the provided notification displayed in its place.\n   *\n   * @private\n   * @param {NotificationModel} notificationModel The notification to display\n   * @returns {Notification} the provided notification decorated with\n   * functions to dismiss or minimize\n   */\n  _notify(notificationModel) {\n    let notification;\n    let activeNotification = this.activeNotification;\n\n    notificationModel.severity = notificationModel.severity || 'info';\n    notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');\n\n    notification = this._createNotification(notificationModel);\n\n    this.notifications.push(notification);\n    this._setHighestSeverity();\n\n    /*\n     * Check if there is already an active (ie. visible) notification\n     */\n    if (!this.activeNotification && !notification?.model?.options?.minimized) {\n      this._setActiveNotification(notification);\n    } else if (!this.activeTimeout) {\n      /*\n       * If there is already an active notification, time it out. If it's\n       * already got a timeout in progress (either because it has had\n       * timeout forced because of a queue of messages, or it had an\n       * autodismiss specified), leave it to run. Otherwise force a\n       * timeout.\n       *\n       * This notification has been added to queue and will be\n       * serviced as soon as possible.\n       */\n      this.activeTimeout = setTimeout(() => {\n        this._dismissOrMinimize(activeNotification);\n      }, DEFAULT_AUTO_DISMISS_TIMEOUT);\n    }\n\n    return notification;\n  }\n\n  /**\n   * Creates a new notification object.\n   * @private\n   * @param {NotificationModel} notificationModel The model for the notification\n   * @returns {Notification}\n   */\n  _createNotification(notificationModel) {\n    /** @type {Notification} */\n    let notification = new EventEmitter();\n    notification.model = notificationModel;\n    notification.dismiss = () => {\n      this._dismiss(notification);\n    };\n\n    if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) {\n      notification.progress = (progressPerc, progressText) => {\n        notification.model.progressPerc = progressPerc;\n        notification.model.progressText = progressText;\n        notification.emit('progress', progressPerc, progressText);\n      };\n    }\n\n    return notification;\n  }\n\n  /**\n   * Sets the active notification.\n   * @private\n   * @param {Notification | undefined} notification The notification to set as active\n   */\n  _setActiveNotification(notification) {\n    this.activeNotification = notification;\n\n    if (!notification) {\n      delete this.activeTimeout;\n\n      return;\n    }\n\n    this.emit('notification', notification);\n\n    if (notification.model.autoDismiss || this._selectNextNotification()) {\n      const autoDismissTimeout =\n        notification.model.options.autoDismissTimeout || DEFAULT_AUTO_DISMISS_TIMEOUT;\n      this.activeTimeout = setTimeout(() => {\n        this._dismissOrMinimize(notification);\n      }, autoDismissTimeout);\n    } else {\n      delete this.activeTimeout;\n    }\n  }\n\n  /**\n   * Selects the next notification to be displayed.\n   * @private\n   * @returns {Notification | undefined}\n   */\n  _selectNextNotification() {\n    let notification;\n    let i = 0;\n\n    /*\n     * Loop through the notifications queue and find the first one that\n     * has not already been minimized (manually or otherwise).\n     */\n    for (; i < this.notifications.length; i++) {\n      notification = this.notifications[i];\n\n      const isNotificationMinimized =\n        notification.model.minimized || notification?.model?.options?.minimized;\n\n      if (!isNotificationMinimized && notification !== this.activeNotification) {\n        return notification;\n      }\n    }\n  }\n}\n\n/**\n * @typedef {Object} NotificationProperties\n * @property {() => void} dismiss Dismiss the notification\n * @property {NotificationModel} model The Notification model\n * @property {(progressPerc: number, progressText: string) => void} [progress] Update the progress of the notification\n */\n\n/**\n * @typedef {EventEmitter & NotificationProperties} Notification\n */\n\n/**\n * @typedef {Object} NotificationLink\n * @property {() => void} onClick The function to be called when the link is clicked\n * @property {string} cssClass A CSS class name to style the link\n * @property {string} text The text to be displayed for the link\n */\n\n/**\n * @typedef {Object} NotificationOptions\n * @property {number} [autoDismissTimeout] Milliseconds to wait before automatically dismissing the notification\n * @property {boolean} [minimized] Allows for a notification to be minimized into the indicator by default\n * @property {NotificationLink} [link] A link for the notification\n */\n\n/**\n * A representation of a banner notification.\n * @typedef {Object} NotificationModel\n * @property {string} message The message to be displayed by the notification\n * @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or 'unknown'.\n * @property {string} [progressText] A message conveying progress of some ongoing task.\n * @property {'info' | 'alert' | 'error'} [severity] The severity of the notification.\n * @property {string} [timestamp] The time at which the notification was created. Should be a string in ISO 8601 format.\n * @property {boolean} [minimized] Whether or not the notification has been minimized\n * @property {boolean} [autoDismiss] Whether the notification should be automatically dismissed after a short period of time.\n * @property {NotificationOptions} options The notification options\n */\n"
  },
  {
    "path": "src/api/notifications/NotificationAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport NotificationAPI from './NotificationAPI.js';\n\ndescribe('The Notification API', () => {\n  let notificationAPIInstance;\n  let defaultTimeout = 4000;\n\n  beforeAll(() => {\n    notificationAPIInstance = new NotificationAPI();\n  });\n\n  describe('the info method', () => {\n    let message = 'Example Notification Message';\n    let severity = 'info';\n    let notificationModel;\n\n    beforeAll(() => {\n      notificationModel = notificationAPIInstance.info(message).model;\n    });\n\n    afterAll(() => {\n      notificationAPIInstance.dismissAllNotifications();\n    });\n\n    it('shows a string message with info severity', () => {\n      expect(notificationModel.message).toEqual(message);\n      expect(notificationModel.severity).toEqual(severity);\n    });\n\n    it('auto dismisses the notification after a brief timeout', (done) => {\n      window.setTimeout(() => {\n        expect(notificationAPIInstance.notifications.length).toEqual(0);\n        done();\n      }, defaultTimeout);\n    });\n  });\n\n  describe('the alert method', () => {\n    let message = 'Example alert message';\n    let severity = 'alert';\n    let notificationModel;\n\n    beforeAll(() => {\n      notificationModel = notificationAPIInstance.alert(message).model;\n    });\n\n    afterAll(() => {\n      notificationAPIInstance.dismissAllNotifications();\n    });\n\n    it('shows a string message, with alert severity', () => {\n      expect(notificationModel.message).toEqual(message);\n      expect(notificationModel.severity).toEqual(severity);\n    });\n\n    it('does not auto dismiss the notification', (done) => {\n      window.setTimeout(() => {\n        expect(notificationAPIInstance.notifications.length).toEqual(1);\n        done();\n      }, defaultTimeout);\n    });\n  });\n\n  describe('the error method', () => {\n    let message = 'Example error message';\n    let severity = 'error';\n    let notificationModel;\n\n    beforeAll(() => {\n      notificationModel = notificationAPIInstance.error(message).model;\n    });\n\n    afterAll(() => {\n      notificationAPIInstance.dismissAllNotifications();\n    });\n\n    it('shows a string message, with severity error', () => {\n      expect(notificationModel.message).toEqual(message);\n      expect(notificationModel.severity).toEqual(severity);\n    });\n\n    it('does not auto dismiss the notification', (done) => {\n      window.setTimeout(() => {\n        expect(notificationAPIInstance.notifications.length).toEqual(1);\n        done();\n      }, defaultTimeout);\n    });\n  });\n\n  describe('the error method notification', () => {\n    let message = 'Minimized error message';\n\n    afterAll(() => {\n      notificationAPIInstance.dismissAllNotifications();\n    });\n\n    it('is not shown if configured to show minimized', (done) => {\n      notificationAPIInstance.activeNotification = undefined;\n      notificationAPIInstance.error(message, { minimized: true });\n      window.setTimeout(() => {\n        expect(notificationAPIInstance.notifications.length).toEqual(1);\n        expect(notificationAPIInstance.activeNotification).toEqual(undefined);\n        done();\n      }, defaultTimeout);\n    });\n  });\n\n  describe('the progress method', () => {\n    let title = 'This is a progress notification';\n    let message1 = 'Example progress message 1';\n    let message2 = 'Example progress message 2';\n    let percentage1 = 50;\n    let percentage2 = 99.9;\n    let severity = 'info';\n    let notification;\n    let updatedPercentage;\n    let updatedMessage;\n\n    beforeAll(() => {\n      notification = notificationAPIInstance.progress(title, percentage1, message1);\n      notification.on('progress', (percentage, text) => {\n        updatedPercentage = percentage;\n        updatedMessage = text;\n      });\n    });\n\n    afterAll(() => {\n      notificationAPIInstance.dismissAllNotifications();\n    });\n\n    it('shows a notification with a message, progress message, percentage and info severity', () => {\n      expect(notification.model.message).toEqual(title);\n      expect(notification.model.severity).toEqual(severity);\n      expect(notification.model.progressText).toEqual(message1);\n      expect(notification.model.progressPerc).toEqual(percentage1);\n    });\n\n    it('allows dynamically updating the progress attributes', () => {\n      notification.progress(percentage2, message2);\n\n      expect(updatedPercentage).toEqual(percentage2);\n      expect(updatedMessage).toEqual(message2);\n    });\n\n    it('allows dynamically dismissing of progress notification', () => {\n      notification.dismiss();\n\n      expect(notificationAPIInstance.notifications.length).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/objects/ConflictError.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Represents an error that occurs when there is a conflict (409) while trying to\n * persist an object.\n * This class extends the built-in Error class.\n */\nexport default class ConflictError extends Error {}\n"
  },
  {
    "path": "src/api/objects/InMemorySearchProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { v4 as uuid } from 'uuid';\n\nclass InMemorySearchProvider {\n  /**\n   * A search service which searches through domain objects in\n   * the filetree without using external search implementations.\n   *\n   * @constructor\n   * @param {Object} openmct\n   */\n  constructor(openmct) {\n    /**\n     * Maximum number of concurrent index requests to allow.\n     */\n    this.MAX_CONCURRENT_REQUESTS = 100;\n    /**\n     * If max results is not specified in query, use this as default.\n     */\n    this.DEFAULT_MAX_RESULTS = 100;\n    this.openmct = openmct;\n    this.indexedIds = {};\n    this.indexedCompositions = {};\n    this.idsToIndex = [];\n    this.pendingIndex = {};\n    this.pendingRequests = 0;\n    this.worker = null;\n\n    /**\n     * If we don't have SharedWorkers available (e.g., iOS)\n     */\n    this.localIndexedDomainObjects = {};\n    this.localIndexedAnnotationsByDomainObject = {};\n    this.localIndexedAnnotationsByTag = {};\n\n    this.pendingQueries = {};\n    this.onWorkerMessage = this.onWorkerMessage.bind(this);\n    this.onWorkerMessageError = this.onWorkerMessageError.bind(this);\n    this.localSearchForObjects = this.localSearchForObjects.bind(this);\n    this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);\n    this.localSearchForTags = this.localSearchForTags.bind(this);\n    this.onAnnotationCreation = this.onAnnotationCreation.bind(this);\n    this.onCompositionAdded = this.onCompositionAdded.bind(this);\n    this.onCompositionRemoved = this.onCompositionRemoved.bind(this);\n    this.onerror = this.onWorkerError.bind(this);\n    this.startIndexing = this.startIndexing.bind(this);\n\n    this.openmct.on('start', this.startIndexing);\n    this.openmct.on('destroy', () => {\n      if (this.worker && this.worker.port) {\n        this.worker.onerror = null;\n        this.worker.port.onmessage = null;\n        this.worker.port.onmessageerror = null;\n        this.worker.port.close();\n      }\n\n      Object.keys(this.indexedCompositions).forEach((keyString) => {\n        const composition = this.indexedCompositions[keyString];\n        composition.off('add', this.onCompositionAdded);\n        composition.off('remove', this.onCompositionRemoved);\n      });\n\n      this.destroyObservers(this.indexedIds);\n      this.destroyObservers(this.indexedCompositions);\n    });\n  }\n\n  startIndexing() {\n    const rootObject = this.openmct.objects.rootProvider.rootObject;\n\n    this.searchTypes = this.openmct.objects.SEARCH_TYPES;\n\n    this.supportedSearchTypes = [\n      this.searchTypes.OBJECTS,\n      this.searchTypes.ANNOTATIONS,\n      this.searchTypes.TAGS\n    ];\n\n    this.scheduleForIndexing(rootObject.identifier);\n\n    this.indexAnnotations();\n\n    if (typeof SharedWorker !== 'undefined') {\n      this.worker = this.startSharedWorker();\n    } else {\n      // we must be on iOS\n    }\n\n    this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);\n  }\n\n  indexAnnotations() {\n    const theInMemorySearchProvider = this;\n    this.openmct.objects.providers.forEach((objectProvider) => {\n      if (objectProvider.getAllObjects) {\n        const allObjects = objectProvider.getAllObjects();\n        if (allObjects) {\n          Object.values(allObjects).forEach((domainObject) => {\n            if (domainObject.type === 'annotation') {\n              theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);\n            }\n          });\n        }\n      }\n    });\n  }\n\n  /**\n   * @private\n   */\n  getIntermediateResponse() {\n    let intermediateResponse = {};\n    intermediateResponse.promise = new Promise(function (resolve, reject) {\n      intermediateResponse.resolve = resolve;\n      intermediateResponse.reject = reject;\n    });\n\n    return intermediateResponse;\n  }\n\n  search(query, searchType) {\n    const queryId = uuid();\n    const pendingQuery = this.getIntermediateResponse();\n    this.pendingQueries[queryId] = pendingQuery;\n    const searchOptions = {\n      queryId,\n      searchType,\n      query,\n      maxResults: this.DEFAULT_MAX_RESULTS\n    };\n\n    if (this.worker) {\n      this.#dispatchSearchToWorker(searchOptions);\n    } else {\n      this.#localQueryFallBack(searchOptions);\n    }\n\n    return pendingQuery.promise;\n  }\n\n  /**\n   * @private\n   */\n  #localQueryFallBack({ queryId, searchType, query, maxResults }) {\n    if (searchType === this.searchTypes.OBJECTS) {\n      return this.localSearchForObjects(queryId, query, maxResults);\n    } else if (searchType === this.searchTypes.ANNOTATIONS) {\n      return this.localSearchForAnnotations(queryId, query, maxResults);\n    } else if (searchType === this.searchTypes.TAGS) {\n      return this.localSearchForTags(queryId, query, maxResults);\n    } else {\n      throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);\n    }\n  }\n\n  supportsSearchType(searchType) {\n    return this.supportedSearchTypes.includes(searchType);\n  }\n\n  /**\n   * Handle messages from the worker.\n   * @private\n   */\n  async onWorkerMessage(event) {\n    const pendingQuery = this.pendingQueries[event.data.queryId];\n    const modelResults = {\n      total: event.data.total\n    };\n    modelResults.hits = await Promise.all(\n      event.data.results.map(async (hit) => {\n        if (hit && hit.keyString) {\n          const identifier = this.openmct.objects.parseKeyString(hit.keyString);\n          const domainObject = await this.openmct.objects.get(identifier);\n\n          return domainObject;\n        }\n      })\n    );\n\n    pendingQuery.resolve(modelResults);\n    delete this.pendingQueries[event.data.queryId];\n  }\n\n  /**\n   * Handle error messages from the worker.\n   * @private\n   */\n  onWorkerMessageError(event) {\n    console.error('⚙️ Error message from InMemorySearch worker ⚙️', event);\n  }\n\n  /**\n   * Handle errors from the worker.\n   * @private\n   */\n  onWorkerError(event) {\n    console.error('⚙️ Error with InMemorySearch worker ⚙️', event);\n  }\n\n  /**\n   * @private\n   */\n  startSharedWorker() {\n    // eslint-disable-next-line no-undef\n    const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}inMemorySearchWorker.js`;\n\n    const sharedWorker = new SharedWorker(sharedWorkerURL, 'InMemorySearch Shared Worker');\n    sharedWorker.onerror = this.onWorkerError;\n    sharedWorker.port.onmessage = this.onWorkerMessage;\n    sharedWorker.port.onmessageerror = this.onWorkerMessageError;\n    sharedWorker.port.start();\n\n    return sharedWorker;\n  }\n\n  /**\n   * Schedule an id to be indexed at a later date.  If there are less\n   * pending requests than the maximum allowed, this will kick off an indexing request.\n   * This is done only when indexing first begins and we need to index a lot of objects.\n   *\n   * @private\n   * @param {identifier} id to be indexed.\n   */\n  scheduleForIndexing(identifier) {\n    const keyString = this.openmct.objects.makeKeyString(identifier);\n    const objectProvider = this.openmct.objects.getProvider(identifier);\n\n    if (objectProvider === undefined || objectProvider.search === undefined) {\n      if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) {\n        this.pendingIndex[keyString] = true;\n        this.idsToIndex.push(keyString);\n      }\n    }\n\n    this.keepIndexing();\n  }\n\n  /**\n   * If there are less pending requests than concurrent requests, keep\n   * firing requests.\n   *\n   * @private\n   */\n  keepIndexing() {\n    while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS && this.idsToIndex.length) {\n      this.beginIndexRequest();\n    }\n  }\n\n  onAnnotationCreation(annotationObject) {\n    const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);\n    if (objectProvider === undefined || objectProvider.search === undefined) {\n      const provider = this;\n      provider.index(annotationObject);\n    }\n  }\n\n  onNameMutation(domainObject, name) {\n    const provider = this;\n\n    domainObject.name = name;\n    provider.index(domainObject);\n  }\n\n  onCompositionAdded(newDomainObjectToIndex) {\n    const provider = this;\n    // The object comes in as a mutable domain object, which has functions,\n    // which the index function cannot handle as it will eventually be serialized\n    // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard\n    // those functions.\n    const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));\n\n    const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);\n    if (objectProvider === undefined || objectProvider.search === undefined) {\n      provider.index(nonMutableDomainObject);\n    }\n  }\n\n  onCompositionRemoved(domainObjectToRemoveIdentifier) {\n    const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);\n    if (this.indexedIds[keyString]) {\n      // we store the unobserve function in the indexedId map\n      this.indexedIds[keyString]();\n      delete this.indexedIds[keyString];\n    }\n\n    const composition = this.indexedCompositions[keyString];\n    if (composition) {\n      composition.off('add', this.onCompositionAdded);\n      composition.off('remove', this.onCompositionRemoved);\n      delete this.indexedCompositions[keyString];\n    }\n  }\n\n  /**\n   * Pass a domainObject to the worker to be indexed.\n   * If the object has composition, schedule those ids for later indexing.\n   * Watch for object changes and re-index object and children if so\n   *\n   * @private\n   * @param domainObject a domainObject\n   */\n  async index(domainObject) {\n    const provider = this;\n    const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n    const composition = this.openmct.composition.get(domainObject);\n\n    if (!this.indexedIds[keyString]) {\n      this.indexedIds[keyString] = this.openmct.objects.observe(\n        domainObject,\n        'name',\n        this.onNameMutation.bind(this, domainObject)\n      );\n      if (composition) {\n        composition.on('add', this.onCompositionAdded);\n        composition.on('remove', this.onCompositionRemoved);\n        this.indexedCompositions[keyString] = composition;\n      }\n    }\n\n    if (keyString !== 'ROOT') {\n      if (this.worker) {\n        this.worker.port.postMessage({\n          request: 'index',\n          model: domainObject,\n          keyString\n        });\n      } else {\n        this.localIndexItem(keyString, domainObject);\n      }\n    }\n\n    if (composition !== undefined) {\n      const children = await composition.load();\n\n      children.forEach((child) => provider.scheduleForIndexing(child.identifier));\n    }\n  }\n\n  /**\n   * Pulls an id from the indexing queue, loads it from the model service,\n   * and indexes it.  Upon completion, tells the provider to keep\n   * indexing.\n   *\n   * @private\n   */\n  async beginIndexRequest() {\n    const keyString = this.idsToIndex.shift();\n    const provider = this;\n\n    this.pendingRequests += 1;\n    const domainObject = await this.openmct.objects.get(keyString);\n    delete provider.pendingIndex[keyString];\n\n    try {\n      if (domainObject && domainObject.identifier) {\n        await provider.index(domainObject);\n      }\n    } catch (error) {\n      console.warn('Failed to index domain object ' + keyString, error);\n    }\n\n    setTimeout(function () {\n      provider.pendingRequests -= 1;\n      provider.keepIndexing();\n    }, 0);\n  }\n\n  /**\n   * Dispatch a search query to the worker and return a queryId.\n   *\n   * @private\n   * @returns {string} a unique query Id for the query.\n   */\n  #dispatchSearchToWorker({ queryId, searchType, query, maxResults }) {\n    const message = {\n      request: searchType.toString(),\n      input: query,\n      maxResults,\n      queryId\n    };\n    this.worker.port.postMessage(message);\n  }\n\n  localIndexTags(keyString, objectToIndex, model) {\n    // add new tags\n    model.tags.forEach((tagID) => {\n      if (!this.localIndexedAnnotationsByTag[tagID]) {\n        this.localIndexedAnnotationsByTag[tagID] = [];\n      }\n\n      const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some((indexedObject) => {\n        return indexedObject.keyString === objectToIndex.keyString;\n      });\n\n      if (!existsInIndex) {\n        this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);\n      }\n    });\n    const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(\n      (indexedTag) => {\n        return !model.tags.includes(indexedTag);\n      }\n    );\n    tagsToRemoveFromIndex.forEach((tagToRemoveFromIndex) => {\n      this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[\n        tagToRemoveFromIndex\n      ].filter((indexedAnnotation) => {\n        const shouldKeep = indexedAnnotation.keyString !== keyString;\n\n        return shouldKeep;\n      });\n    });\n  }\n\n  localIndexAnnotation(objectToIndex, model) {\n    model.targets.forEach((target) => {\n      const targetID = target.keyString;\n      if (!this.localIndexedAnnotationsByDomainObject[targetID]) {\n        this.localIndexedAnnotationsByDomainObject[targetID] = [];\n      }\n\n      objectToIndex.targets = model.targets;\n      objectToIndex.tags = model.tags;\n      const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(\n        (indexedObject) => {\n          return indexedObject.keyString === objectToIndex.keyString;\n        }\n      );\n\n      if (!existsInIndex) {\n        this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);\n      }\n    });\n  }\n\n  /**\n   * A local version of the same SharedWorker function\n   * if we don't have SharedWorkers available (e.g., iOS)\n   */\n  localIndexItem(keyString, model) {\n    const objectToIndex = {\n      type: model.type,\n      name: model.name,\n      keyString\n    };\n    if (model && model.type === 'annotation') {\n      if (model.targets) {\n        this.localIndexAnnotation(objectToIndex, model);\n      }\n\n      if (model.tags) {\n        this.localIndexTags(keyString, objectToIndex, model);\n      }\n    } else {\n      this.localIndexedDomainObjects[keyString] = objectToIndex;\n    }\n  }\n\n  /**\n   * A local version of the same SharedWorker function\n   * if we don't have SharedWorkers available (e.g., iOS)\n   *\n   * Gets search results from the indexedItems based on provided search\n   * input. Returns matching results from indexedItems\n   */\n  localSearchForObjects(queryId, searchInput, maxResults) {\n    // This results dictionary will have domain object ID keys which\n    // point to the value the domain object's score.\n    let results = [];\n    const input = searchInput.trim().toLowerCase();\n    const message = {\n      request: 'searchForObjects',\n      results: [],\n      total: 0,\n      queryId\n    };\n\n    results =\n      Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {\n        return indexedItem.name.toLowerCase().includes(input);\n      }) || [];\n\n    message.total = results.length;\n    message.results = results.slice(0, maxResults);\n    const eventToReturn = {\n      data: message\n    };\n    this.onWorkerMessage(eventToReturn);\n  }\n\n  /**\n   * A local version of the same SharedWorker function\n   * if we don't have SharedWorkers available (e.g., iOS)\n   */\n  localSearchForAnnotations(queryId, searchInput, maxResults) {\n    // This results dictionary will have domain object ID keys which\n    // point to the value the domain object's score.\n    let results = [];\n    const message = {\n      request: 'searchForAnnotations',\n      results: [],\n      total: 0,\n      queryId\n    };\n\n    results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];\n\n    message.total = results.length;\n    message.results = results.slice(0, maxResults);\n    const eventToReturn = {\n      data: message\n    };\n    this.onWorkerMessage(eventToReturn);\n  }\n\n  /**\n   * A local version of the same SharedWorker function\n   * if we don't have SharedWorkers available (e.g., iOS)\n   */\n  localSearchForTags(queryId, matchingTagKeys, maxResults) {\n    let results = [];\n    const message = {\n      request: 'searchForTags',\n      results: [],\n      total: 0,\n      queryId\n    };\n\n    if (matchingTagKeys) {\n      matchingTagKeys.forEach((matchingTag) => {\n        const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];\n        if (matchingAnnotations) {\n          matchingAnnotations.forEach((matchingAnnotation) => {\n            const existsInResults = results.some((indexedObject) => {\n              return matchingAnnotation.keyString === indexedObject.keyString;\n            });\n            if (!existsInResults) {\n              results.push(matchingAnnotation);\n            }\n          });\n        }\n      });\n    }\n\n    message.total = results.length;\n    message.results = results.slice(0, maxResults);\n    const eventToReturn = {\n      data: message\n    };\n    this.onWorkerMessage(eventToReturn);\n  }\n\n  destroyObservers(observers) {\n    Object.entries(observers).forEach(([keyString, unobserve]) => {\n      if (typeof unobserve === 'function') {\n        unobserve();\n      }\n\n      delete observers[keyString];\n    });\n  }\n}\n\nexport default InMemorySearchProvider;\n"
  },
  {
    "path": "src/api/objects/InMemorySearchWorker.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019.\n */\n(function () {\n  // An object composed of domain object IDs and models\n  // {id: domainObject's ID, name: domainObject's name}\n  const indexedDomainObjects = {};\n  const indexedAnnotationsByDomainObject = {};\n  const indexedAnnotationsByTag = {};\n\n  self.onconnect = function (e) {\n    const port = e.ports[0];\n\n    port.onmessage = function (event) {\n      const requestType = event.data.request;\n      if (requestType === 'index') {\n        indexItem(event.data.keyString, event.data.model);\n      } else if (requestType === 'OBJECTS') {\n        port.postMessage(searchForObjects(event.data));\n      } else if (requestType === 'ANNOTATIONS') {\n        port.postMessage(searchForAnnotations(event.data));\n      } else if (requestType === 'TAGS') {\n        port.postMessage(searchForTags(event.data));\n      } else {\n        throw new Error(`Unknown request ${event.data.request}`);\n      }\n    };\n\n    port.start();\n  };\n\n  self.onerror = function (error) {\n    //do nothing\n    console.error('Error on feed', error);\n  };\n\n  function indexAnnotation(objectToIndex, model) {\n    model.targets.forEach((target) => {\n      const targetID = target.keyString;\n      if (!indexedAnnotationsByDomainObject[targetID]) {\n        indexedAnnotationsByDomainObject[targetID] = [];\n      }\n\n      objectToIndex.targets = model.targets;\n      objectToIndex.tags = model.tags;\n      const existsInIndex = indexedAnnotationsByDomainObject[targetID].some((indexedObject) => {\n        return indexedObject.keyString === objectToIndex.keyString;\n      });\n\n      if (!existsInIndex) {\n        indexedAnnotationsByDomainObject[targetID].push(objectToIndex);\n      }\n    });\n  }\n\n  function indexTags(keyString, objectToIndex, model) {\n    // add new tags\n    model.tags.forEach((tagID) => {\n      if (!indexedAnnotationsByTag[tagID]) {\n        indexedAnnotationsByTag[tagID] = [];\n      }\n\n      const existsInIndex = indexedAnnotationsByTag[tagID].some((indexedObject) => {\n        return indexedObject.keyString === objectToIndex.keyString;\n      });\n\n      if (!existsInIndex) {\n        indexedAnnotationsByTag[tagID].push(objectToIndex);\n      }\n    });\n    // remove old tags\n    const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter((indexedTag) => {\n      return !model.tags.includes(indexedTag);\n    });\n    tagsToRemoveFromIndex.forEach((tagToRemoveFromIndex) => {\n      indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[\n        tagToRemoveFromIndex\n      ].filter((indexedAnnotation) => {\n        const shouldKeep = indexedAnnotation.keyString !== keyString;\n\n        return shouldKeep;\n      });\n    });\n  }\n\n  function indexItem(keyString, model) {\n    const objectToIndex = {\n      type: model.type,\n      name: model.name,\n      keyString\n    };\n    if (model && model.type === 'annotation') {\n      if (model.targets) {\n        indexAnnotation(objectToIndex, model);\n      }\n\n      if (model.tags) {\n        indexTags(keyString, objectToIndex, model);\n      }\n    } else {\n      indexedDomainObjects[keyString] = objectToIndex;\n    }\n  }\n\n  /**\n   * Gets search results from the indexedItems based on provided search\n   *   input. Returns matching results from indexedItems\n   *\n   * @param data An object which contains:\n   *           * input: The original string which we are searching with\n   *           * maxResults: The maximum number of search results desired\n   *           * queryId: an id identifying this query, will be returned.\n   */\n  function searchForObjects(data) {\n    let results = [];\n    const input = data.input.trim().toLowerCase();\n    const message = {\n      request: 'searchForObjects',\n      results: [],\n      total: 0,\n      queryId: data.queryId\n    };\n\n    results =\n      Object.values(indexedDomainObjects).filter((indexedItem) => {\n        return indexedItem.name.toLowerCase().includes(input);\n      }) || [];\n\n    message.total = results.length;\n    message.results = results.slice(0, data.maxResults);\n\n    return message;\n  }\n\n  function searchForAnnotations(data) {\n    let results = [];\n    const message = {\n      request: 'searchForAnnotations',\n      results: [],\n      total: 0,\n      queryId: data.queryId\n    };\n\n    results = indexedAnnotationsByDomainObject[data.input] || [];\n\n    message.total = results.length;\n    message.results = results.slice(0, data.maxResults);\n\n    return message;\n  }\n\n  function searchForTags(data) {\n    let results = [];\n    const message = {\n      request: 'searchForTags',\n      results: [],\n      total: 0,\n      queryId: data.queryId\n    };\n\n    if (data.input) {\n      data.input.forEach((matchingTag) => {\n        const matchingAnnotations = indexedAnnotationsByTag[matchingTag];\n        if (matchingAnnotations) {\n          matchingAnnotations.forEach((matchingAnnotation) => {\n            const existsInResults = results.some((indexedObject) => {\n              return matchingAnnotation.keyString === indexedObject.keyString;\n            });\n            if (!existsInResults) {\n              results.push(matchingAnnotation);\n            }\n          });\n        }\n      });\n    }\n\n    message.total = results.length;\n    message.results = results.slice(0, data.maxResults);\n\n    return message;\n  }\n})();\n"
  },
  {
    "path": "src/api/objects/InterceptorRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nconst DEFAULT_INTERCEPTOR_PRIORITY = 0;\nexport default class InterceptorRegistry {\n  /**\n   * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.\n   * @interface InterceptorRegistry\n   */\n  constructor() {\n    this.interceptors = [];\n  }\n\n  /**\n   * @interface InterceptorDef\n   * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object\n   * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object\n   * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number\n   */\n\n  /**\n   * Register a new object interceptor.\n   *\n   * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add\n   * @method addInterceptor\n   */\n  addInterceptor(interceptorDef) {\n    this.interceptors.push(interceptorDef);\n  }\n\n  /**\n   * Retrieve all interceptors applicable to a domain object.\n   * @method getInterceptors\n   * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object\n   */\n  getInterceptors(identifier, object) {\n    function byPriority(interceptorA, interceptorB) {\n      let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;\n      let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;\n\n      return priorityB - priorityA;\n    }\n\n    return this.interceptors\n      .filter((interceptor) => {\n        return (\n          typeof interceptor.appliesTo === 'function' && interceptor.appliesTo(identifier, object)\n        );\n      })\n      .sort(byPriority);\n  }\n}\n"
  },
  {
    "path": "src/api/objects/InterceptorRegistrySpec.js",
    "content": ""
  },
  {
    "path": "src/api/objects/MutableDomainObject.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport { makeKeyString, refresh } from './object-utils.js';\n\nconst ANY_OBJECT_EVENT = 'mutation';\n\n/**\n * Wraps a domain object to keep its model synchronized with other instances of the same object.\n *\n * Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers\n * should be careful to destroy MutableDomainObject in order to avoid memory leaks.\n *\n * All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except\n * `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`\n * will result in the creation of a new MutableDomainObject and you will be responsible for destroying it\n * (via openmct.objects.destroy) when you're done with it.\n *\n * @typedef MutableDomainObject\n */\nclass MutableDomainObject {\n  constructor(eventEmitter) {\n    Object.defineProperties(this, {\n      _globalEventEmitter: {\n        value: eventEmitter,\n        // Property should not be serialized\n        enumerable: false\n      },\n      _instanceEventEmitter: {\n        value: new EventEmitter(),\n        // Property should not be serialized\n        enumerable: false\n      },\n      _observers: {\n        value: [],\n        // Property should not be serialized\n        enumerable: false\n      },\n      isMutable: {\n        value: true,\n        // Property should not be serialized\n        enumerable: false\n      }\n    });\n  }\n  $observe(path, callback) {\n    let fullPath = qualifiedEventName(this, path);\n    let eventOff = this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);\n\n    this._globalEventEmitter.on(fullPath, callback);\n    this._observers.push(eventOff);\n\n    return eventOff;\n  }\n  $set(path, value) {\n    const oldModel = JSON.parse(JSON.stringify(this));\n    const oldValue = _.get(oldModel, path);\n    MutableDomainObject.mutateObject(this, path, value);\n\n    //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.\n    this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);\n\n    //Emit a general \"any object\" event\n    this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel);\n    //Emit wildcard event, with path so that callback knows what changed\n    this._globalEventEmitter.emit(\n      qualifiedEventName(this, '*'),\n      this,\n      path,\n      value,\n      oldModel,\n      oldValue\n    );\n\n    //Emit events specific to properties affected\n    let parentPropertiesList = path.split('.');\n    for (let index = parentPropertiesList.length; index > 0; index--) {\n      let pathToThisProperty = parentPropertiesList.slice(0, index);\n      let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');\n      this._globalEventEmitter.emit(\n        qualifiedEventName(this, parentPropertyPath),\n        _.get(this, parentPropertyPath),\n        _.get(oldModel, parentPropertyPath)\n      );\n\n      const lastPathElement = parentPropertiesList[index - 1];\n      // Also emit an event for the array whose element has changed so developers do not need to listen to every element of the array.\n      if (lastPathElement.endsWith(']')) {\n        const arrayPathElement = lastPathElement.substring(0, lastPathElement.lastIndexOf('['));\n        pathToThisProperty[index - 1] = arrayPathElement;\n        const pathToArrayString = pathToThisProperty.join('.');\n        this._globalEventEmitter.emit(\n          qualifiedEventName(this, pathToArrayString),\n          _.get(this, pathToArrayString),\n          _.get(oldModel, pathToArrayString)\n        );\n      }\n    }\n\n    //TODO: Emit events for listeners of child properties when parent changes.\n    // Do it at observer time - also register observers for parent attribute path.\n  }\n\n  $refresh(model) {\n    //TODO: Currently we are updating the entire object.\n    // In the future we could update a specific property of the object using the 'path' parameter.\n    this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);\n\n    //Emit wildcard event, with path so that callback knows what changed\n    this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);\n  }\n\n  $on(event, callback) {\n    this._instanceEventEmitter.on(event, callback);\n\n    return () => this._instanceEventEmitter.off(event, callback);\n  }\n  $destroy() {\n    while (this._observers.length > 0) {\n      const observer = this._observers.pop();\n      observer();\n    }\n\n    this._instanceEventEmitter.emit('$_destroy');\n  }\n\n  static createMutable(object, mutationTopic) {\n    let mutable = Object.create(new MutableDomainObject(mutationTopic));\n    Object.assign(mutable, object);\n\n    mutable.$observe('$_synchronize_model', (updatedObject) => {\n      let clone = JSON.parse(JSON.stringify(updatedObject));\n      refresh(mutable, clone);\n    });\n\n    return mutable;\n  }\n\n  static mutateObject(object, path, value) {\n    if (path !== 'persisted') {\n      _.set(object, 'modified', Date.now());\n    }\n\n    _.set(object, path, value);\n  }\n}\n\nfunction qualifiedEventName(object, eventName) {\n  let keystring = makeKeyString(object.identifier);\n\n  return [keystring, eventName].join(':');\n}\n\nexport default MutableDomainObject;\n"
  },
  {
    "path": "src/api/objects/NamespaceProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Wraps providers that simply match on object identifier namespace\n */\nexport default class NamespaceProvider {\n  #wrappedProvider;\n  #namespace;\n\n  constructor(namespace, provider) {\n    this.#namespace = namespace;\n    this.#wrappedProvider = provider;\n\n    [\n      'create',\n      'update',\n      'delete',\n      'observe',\n      'supportsSearchType',\n      'search',\n      'isReadOnly',\n      'getAllObjects'\n    ].forEach((methodName) => {\n      const method = this.#wrappedProvider[methodName];\n      if (method !== undefined) {\n        this[methodName] = (...methodArguments) => {\n          return this.#delegateIfImplemented(method, methodArguments);\n        };\n      }\n    });\n  }\n\n  appliesTo(identifier) {\n    return Boolean(identifier?.namespace === this.#namespace);\n  }\n\n  getWrappedProvider() {\n    return this.#wrappedProvider;\n  }\n\n  get(identifier) {\n    return this.#wrappedProvider.get(identifier);\n  }\n\n  #delegateIfImplemented(delegateFunction, delegateArguments) {\n    if (delegateFunction !== undefined && typeof delegateFunction === 'function') {\n      return delegateFunction.apply(this.#wrappedProvider, delegateArguments);\n    } else {\n      throw new Error(`Function is not implemented`);\n    }\n  }\n}\n"
  },
  {
    "path": "src/api/objects/ObjectAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';\n\nimport { PLAN_EXECUTION_MONITORING_KEY } from '../../plugins/planExecutionMonitoring/planExecutionMonitoringIdentifier.js';\nimport ConflictError from './ConflictError.js';\nimport InMemorySearchProvider from './InMemorySearchProvider.js';\nimport InterceptorRegistry from './InterceptorRegistry.js';\nimport MutableDomainObject from './MutableDomainObject.js';\nimport NamespaceProvider from './NamespaceProvider.js';\nimport { isIdentifier, isKeyString } from './object-utils.js';\nimport RootObjectCompositionProvider from './RootObjectCompositionProvider.js';\nimport RootObjectProvider from './RootObjectProvider.js';\nimport RootRegistry from './RootRegistry.js';\nimport Transaction from './Transaction.js';\n\n/**\n * Uniquely identifies a domain object.\n * @typedef {Object} Identifier\n * @property {string} namespace the namespace to/from which this domain object should be loaded/stored.\n * @property {string} key a unique identifier for the domain object within that namespace\n */\n\n/**\n * A domain object is an entity of relevance to a user's workflow, that should appear as a distinct and meaningful object within the user interface.\n * @typedef {Object} DomainObject\n * @property {Identifier} identifier a key/namespace pair which uniquely identifies this domain object\n * @property {string} type the type of domain object\n * @property {string} name the human-readable name for this domain object\n * @property {string} [creator] the user name of the creator of this domain object\n * @property {number} [modified] the time, in milliseconds since the UNIX epoch, at which this domain object was last modified\n * @property {Identifier[]} [composition] if present, this will be used by the default composition provider to load domain objects\n * @property {Record<string, any>} [configuration] A key-value map containing configuration settings for this domain object.\n */\n\n/**\n * @readonly\n * @enum {string} SEARCH_TYPES\n * @property {string} OBJECTS Search for objects\n * @property {string} ANNOTATIONS Search for annotations\n * @property {string} TAGS Search for tags\n */\n\n/**\n * Utilities for loading, saving, and manipulating domain objects.\n */\nexport default class ObjectAPI {\n  #makeKeyString;\n  #parseKeyString;\n  #identifierEquals;\n  #refresh;\n  #openmct;\n\n  /**\n   * @param {any} typeRegistry\n   * @param {any} openmct\n   */\n  constructor(typeRegistry, openmct) {\n    this.#makeKeyString = makeKeyString;\n    this.#parseKeyString = parseKeyString;\n    this.#identifierEquals = identifierEquals;\n    this.#refresh = refresh;\n    this.#openmct = openmct;\n\n    this.typeRegistry = typeRegistry;\n    this.SEARCH_TYPES = Object.freeze({\n      OBJECTS: 'OBJECTS',\n      ANNOTATIONS: 'ANNOTATIONS',\n      TAGS: 'TAGS'\n    });\n    this.eventEmitter = new EventEmitter();\n    this.providers = [];\n    this.rootRegistry = new RootRegistry(openmct);\n    this.inMemorySearchProvider = new InMemorySearchProvider(openmct);\n\n    this.rootProvider = new RootObjectProvider(this.rootRegistry);\n    openmct.composition.addProvider(new RootObjectCompositionProvider(openmct, this.rootRegistry));\n\n    this.cache = {};\n    this.interceptorRegistry = new InterceptorRegistry();\n\n    this.SYNCHRONIZED_OBJECT_TYPES = [\n      'notebook',\n      'restricted-notebook',\n      'plan',\n      'annotation',\n      'activity-states',\n      PLAN_EXECUTION_MONITORING_KEY\n    ];\n\n    this.errors = {\n      Conflict: ConflictError\n    };\n  }\n\n  /**\n   * Retrieve the provider for a given identifier.\n   * @param {Identifier} identifier\n   * @returns {ObjectProvider | RootObjectProvider}\n   */\n  getProvider(identifier) {\n    if (identifier.key === 'ROOT') {\n      return this.rootProvider;\n    }\n\n    const provider = this.providers.find((candidateProvider) => {\n      return candidateProvider.appliesTo(identifier);\n    });\n\n    if (provider?.getWrappedProvider && typeof provider.getWrappedProvider === 'function') {\n      return provider.getWrappedProvider();\n    } else {\n      return provider || this.fallbackProvider;\n    }\n  }\n\n  /**\n   * Get the root registry\n   * @returns {RootRegistry} the root registry\n   */\n  getRootRegistry() {\n    return this.rootRegistry;\n  }\n\n  /**\n   * Get an active transaction instance\n   * @returns {Transaction} a transaction object\n   */\n  getActiveTransaction() {\n    return this.transaction;\n  }\n\n  /**\n   * Get the root-level object.\n   * @returns {Promise<DomainObject>} a promise for the root object\n   */\n  getRoot() {\n    return this.rootProvider.get();\n  }\n\n  /**\n   * Register a new object provider for a particular namespace.\n   *\n   * @param {string|ObjectProvider} namespaceOrProvider Either a namespace or a provider. In the case of a namespace, a provider must be provided as the second argument\n   * @param {ObjectProvider} [providerOrNothing] the provider which will handle loading domain objects from the provided namespace (required when first argument is a namespace)\n   */\n  addProvider(namespaceOrProvider, providerOrNothing) {\n    let namespace;\n    let provider;\n\n    // if no namespace is provided, we expect a provider to be provided\n    if (arguments.length === 1) {\n      if (\n        namespaceOrProvider !== undefined &&\n        typeof namespaceOrProvider.appliesTo === 'function'\n      ) {\n        provider = namespaceOrProvider;\n      } else {\n        throw new Error('If no namespace is defined, provider must have an appliesTo function');\n      }\n    }\n\n    // if a namespace is provided, we expect a provider to be provided\n    if (arguments.length === 2) {\n      namespace = namespaceOrProvider;\n      provider = new NamespaceProvider(namespace, providerOrNothing);\n    }\n\n    //Unshift rather than push because we want to last added provider to match first. This is to replicate legacy map behavior\n    this.providers.unshift(provider);\n  }\n\n  /**\n   * Get a domain object.\n   *\n   * @param {Identifier | string} identifier the identifier for the domain object to load\n   * @param {AbortSignal} [abortSignal] (optional) signal to abort fetch requests\n   * @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and\n   *          dirty/in-transaction objects use and the provider.get method\n   * @returns {Promise<DomainObject>} a promise which will resolve when the domain object\n   *          has been saved, or be rejected if it cannot be saved\n   */\n  get(identifier, abortSignal, forceRemote = false) {\n    let keystring = this.#makeKeyString(identifier);\n\n    if (!forceRemote) {\n      if (this.cache[keystring] !== undefined) {\n        return this.cache[keystring];\n      }\n\n      identifier = parseKeyString(identifier);\n\n      if (this.isTransactionActive()) {\n        let dirtyObject = this.transaction.getDirtyObject(identifier);\n\n        if (dirtyObject) {\n          return Promise.resolve(dirtyObject);\n        }\n      }\n    }\n\n    const provider = this.getProvider(identifier);\n\n    if (!provider) {\n      throw new Error(`No Provider Matched for keyString \"${this.#makeKeyString(identifier)}\"`);\n    }\n\n    if (!provider.get) {\n      throw new Error('Provider does not support get!');\n    }\n\n    let objectPromise = provider\n      .get(identifier, abortSignal)\n      .then((domainObject) => {\n        delete this.cache[keystring];\n        if (!domainObject && abortSignal?.aborted) {\n          // we've aborted the request\n          return;\n        }\n        domainObject = this.applyGetInterceptors(identifier, domainObject);\n\n        if (this.supportsMutation(identifier)) {\n          const mutableDomainObject = this.toMutable(domainObject);\n          mutableDomainObject.$refresh(domainObject);\n          this.destroyMutable(mutableDomainObject);\n        }\n\n        return domainObject;\n      })\n      .catch((error) => {\n        delete this.cache[keystring];\n\n        // suppress abort errors\n        if (error.name === 'AbortError') {\n          return;\n        }\n\n        console.warn(`Failed to retrieve ${keystring}:`, error);\n\n        return this.applyGetInterceptors(identifier);\n      });\n\n    this.cache[keystring] = objectPromise;\n\n    return objectPromise;\n  }\n\n  /**\n   * Search for domain objects.\n   *\n   * Object providersSearches and combines results of each object provider search.\n   * Objects without search provided will have been indexed\n   * and will be searched using the fallback in-memory search.\n   * Search results are asynchronous and resolve in parallel.\n   *\n   * @param {string} query the term to search for\n   * @param {AbortController.signal} [abortSignal] (optional) signal to cancel downstream fetch requests\n   * @param {string} [searchType=this.SEARCH_TYPES.OBJECTS] the type of search as defined by SEARCH_TYPES\n   * @returns {Promise<DomainObject>[]} an array of promises returned from each object provider's search function, each resolving to domain objects matching the provided search query and options\n   */\n  search(query, abortSignal, searchType = this.SEARCH_TYPES.OBJECTS) {\n    if (!Object.keys(this.SEARCH_TYPES).includes(searchType.toUpperCase())) {\n      throw new Error(`Unknown search type: ${searchType}`);\n    }\n\n    const searchPromises = this.providers\n      .filter((provider) => {\n        return provider.supportsSearchType !== undefined && provider.supportsSearchType(searchType);\n      })\n      .map((provider) => provider.search(query, abortSignal, searchType));\n    if (!this.inMemorySearchProvider.supportsSearchType(searchType)) {\n      throw new Error(`${searchType} not implemented in inMemorySearchProvider`);\n    }\n\n    searchPromises.push(\n      this.inMemorySearchProvider.search(query, searchType).then((results) =>\n        results.hits.map((hit) => {\n          return hit;\n        })\n      )\n    );\n\n    return searchPromises;\n  }\n\n  /**\n   * Will fetch object for the given identifier, returning a version of the object that will automatically keep\n   * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.\n   * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The\n   * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are\n   * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.\n   *\n   * @param {Identifier} identifier the identifier of the object to fetch\n   * @returns {Promise<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if the object can be mutated\n   */\n  getMutable(identifier) {\n    if (!this.supportsMutation(identifier)) {\n      throw new Error(`Object \"${this.#makeKeyString(identifier)}\" does not support mutation.`);\n    }\n\n    return this.get(identifier).then((object) => {\n      return this.toMutable(object);\n    });\n  }\n\n  /**\n   * This function is for cleaning up a mutable domain object when you're done with it.\n   * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the\n   * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.\n   * @param {MutableDomainObject} domainObject the mutable domain object to destroy\n   */\n  destroyMutable(domainObject) {\n    if (domainObject.isMutable) {\n      return domainObject.$destroy();\n    } else {\n      throw new Error('Attempted to destroy non-mutable domain object');\n    }\n  }\n\n  delete() {\n    throw new Error('Delete not implemented');\n  }\n\n  isPersistable(idOrKeyString) {\n    let identifier = parseKeyString(idOrKeyString);\n    let provider = this.getProvider(identifier);\n    if (provider?.isReadOnly) {\n      return !provider.isReadOnly();\n    }\n\n    return provider !== undefined && provider.create !== undefined && provider.update !== undefined;\n  }\n\n  isMissing(domainObject) {\n    let identifier = makeKeyString(domainObject.identifier);\n    let missingName = 'Missing: ' + identifier;\n\n    return domainObject.name === missingName;\n  }\n\n  /**\n   * Save this domain object in its current state.\n   *\n   * @param {DomainObject} domainObject the domain object to save\n   * @returns {Promise} a promise which will resolve when the domain object has been saved, or be rejected if it cannot be saved\n   */\n  async save(domainObject) {\n    const provider = this.getProvider(domainObject.identifier);\n    let result;\n    let lastPersistedTime;\n\n    if (!this.isPersistable(domainObject.identifier)) {\n      result = Promise.reject('Object provider does not support saving');\n    } else if (this.#hasAlreadyBeenPersisted(domainObject)) {\n      result = Promise.resolve(true);\n    } else {\n      const username = await this.#getCurrentUsername();\n      const isNewObject = domainObject.persisted === undefined;\n      let savedResolve;\n      let savedReject;\n      let savedObjectPromise;\n\n      result = new Promise((resolve, reject) => {\n        savedResolve = resolve;\n        savedReject = reject;\n      });\n\n      this.#mutate(domainObject, 'modifiedBy', username);\n\n      if (isNewObject) {\n        this.#mutate(domainObject, 'createdBy', username);\n\n        const createdTime = Date.now();\n        this.#mutate(domainObject, 'created', createdTime);\n\n        const persistedTime = Date.now();\n        this.#mutate(domainObject, 'persisted', persistedTime);\n\n        savedObjectPromise = provider.create(domainObject);\n      } else {\n        lastPersistedTime = domainObject.persisted;\n        const persistedTime = Date.now();\n        this.#mutate(domainObject, 'persisted', persistedTime);\n        savedObjectPromise = provider.update(domainObject);\n      }\n\n      if (savedObjectPromise) {\n        savedObjectPromise\n          .then((response) => {\n            savedResolve(response);\n          })\n          .catch((error) => {\n            if (!isNewObject) {\n              this.#mutate(domainObject, 'persisted', lastPersistedTime);\n            }\n\n            savedReject(error);\n          });\n      } else {\n        result = Promise.reject(\n          `[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${\n            isNewObject ? 'creating new' : 'updating'\n          } object.`\n        );\n      }\n    }\n\n    return result.catch(async (error) => {\n      if (error instanceof this.errors.Conflict) {\n        // Synchronized objects will resolve their own conflicts\n        if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {\n          this.#openmct.notifications.info(\n            `Conflict detected while saving \"${this.#makeKeyString(\n              domainObject.name\n            )}\", attempting to resolve`\n          );\n        } else {\n          this.#openmct.notifications.error(\n            `Conflict detected while saving ${this.#makeKeyString(domainObject.identifier)}`\n          );\n\n          if (this.isTransactionActive()) {\n            this.endTransaction();\n          }\n\n          await this.#refresh(domainObject);\n        }\n      }\n\n      throw error;\n    });\n  }\n\n  async #getCurrentUsername() {\n    const user = await this.#openmct.user.getCurrentUser();\n    let username;\n\n    if (user !== undefined) {\n      username = user.getName();\n    }\n\n    return username;\n  }\n\n  /**\n   * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects\n   *\n   * @returns {Transaction} a new Transaction that was just created\n   */\n  startTransaction() {\n    if (this.isTransactionActive()) {\n      throw new Error('Unable to start new Transaction: Previous Transaction is active');\n    }\n\n    this.transaction = new Transaction(this);\n\n    return this.transaction;\n  }\n\n  /**\n   * Clear instance of Transaction\n   */\n  endTransaction() {\n    this.transaction = null;\n  }\n\n  /**\n   * Add a root-level object.\n   * @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or\n   *        an array of identifiers for root level objects, or a function that returns a\n   *        promise for an identifier or an array of root level objects.\n   * @param {module:openmct.PriorityAPI~priority|Number} priority a number representing\n   *        this item(s) position in the root object's composition (example: order in object tree).\n   *        For arrays, they are treated as blocks.\n   * @method addRoot\n   */\n  addRoot(identifier, priority) {\n    this.rootRegistry.addRoot(identifier, priority);\n  }\n\n  removeRoot(identifier) {\n    this.rootRegistry.removeRoot(identifier);\n  }\n\n  /**\n   * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get\n   * The domain object will be transformed after it is retrieved from the persistence store\n   * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef\n   *\n   * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add\n   * @method addGetInterceptor\n   */\n  addGetInterceptor(interceptorDef) {\n    this.interceptorRegistry.addInterceptor(interceptorDef);\n  }\n\n  /**\n   * Retrieve the interceptors for a given domain object.\n   */\n  #listGetInterceptors(identifier, object) {\n    return this.interceptorRegistry.getInterceptors(identifier, object);\n  }\n\n  /**\n   * Invoke interceptors if applicable for a given domain object.\n   * @private\n   */\n  applyGetInterceptors(identifier, domainObject) {\n    const interceptors = this.#listGetInterceptors(identifier, domainObject);\n    interceptors.forEach((interceptor) => {\n      domainObject = interceptor.invoke(identifier, domainObject);\n    });\n\n    return domainObject;\n  }\n\n  /**\n   * Return relative url path from a given object path\n   * eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....\n   * @param {Array<DomainObject>} objectPath\n   * @returns {string} relative url for object\n   */\n  getRelativePath(objectPath) {\n    return objectPath\n      .map((p) => this.#makeKeyString(p.identifier))\n      .reverse()\n      .join('/');\n  }\n\n  /**\n   * Return path of telemetry objects in the object composition\n   * @param {Object} identifier the identifier for the domain object to query for\n   * @param {Object} [telemetryIdentifier] the specific identifier for the telemetry\n   *  to look for in the composition, uses first object in composition otherwise\n   * @returns {Array} path of telemetry object in object composition\n   */\n  async getTelemetryPath(identifier, telemetryIdentifier) {\n    const objectDetails = await this.get(identifier);\n    let telemetryPath = [];\n    if (objectDetails?.type === 'folder') {\n      return telemetryPath;\n    }\n\n    let sourceTelemetry = null;\n    if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {\n      sourceTelemetry = identifier;\n    } else if (objectDetails.composition) {\n      sourceTelemetry = objectDetails.composition[0];\n      if (telemetryIdentifier) {\n        sourceTelemetry = objectDetails.composition.find((telemetrySource) =>\n          this.#identifierEquals(telemetrySource, telemetryIdentifier)\n        );\n      }\n    }\n\n    const compositionElement = await this.get(sourceTelemetry);\n    if (!['yamcs.telemetry', 'generator', 'yamcs.aggregate'].includes(compositionElement.type)) {\n      return telemetryPath;\n    }\n\n    const telemetryPathObjects = await this.getOriginalPath(compositionElement.identifier);\n    telemetryPath = telemetryPathObjects\n      .reverse()\n      .filter((pathObject) => pathObject.type !== 'root')\n      .map((pathObject) => pathObject.name);\n\n    return telemetryPath;\n  }\n\n  /**\n   * Modify a domain object. Internal to ObjectAPI, won't call save after.\n   *\n   * @param {DomainObject} domainObject the object to mutate\n   * @param {string} path the property to modify\n   * @param {*} value the new value for this property\n   */\n  #mutate(domainObject, path, value) {\n    if (!this.supportsMutation(domainObject.identifier)) {\n      throw `Error: Attempted to mutate immutable object ${domainObject.name}`;\n    }\n\n    if (domainObject.isMutable) {\n      domainObject.$set(path, value);\n    } else {\n      // Creating a temporary mutable domain object allows other mutable instances of the\n      // object to be kept in sync.\n      let mutableDomainObject = this.toMutable(domainObject);\n\n      // Mutate original object\n      MutableDomainObject.mutateObject(domainObject, path, value);\n\n      // Mutate temporary mutable object, in the process informing any other mutable instances\n      mutableDomainObject.$set(path, value);\n\n      // Destroy temporary mutable object\n      this.destroyMutable(mutableDomainObject);\n    }\n  }\n\n  /**\n   * Modify a domain object and save.\n   * @param {DomainObject} domainObject the object to mutate\n   * @param {string} path the property to modify\n   * @param {*} value the new value for this property\n   */\n  mutate(domainObject, path, value) {\n    this.#mutate(domainObject, path, value);\n\n    if (this.isTransactionActive()) {\n      this.transaction.add(domainObject);\n    } else {\n      this.save(domainObject);\n    }\n  }\n\n  /**\n   * Create a mutable domain object from an existing domain object.\n   * @param {DomainObject} domainObject the object to make mutable\n   * @returns {MutableDomainObject} a mutable domain object that will automatically sync\n   */\n  toMutable(domainObject) {\n    let mutableObject;\n\n    if (domainObject.isMutable) {\n      mutableObject = domainObject;\n    } else {\n      mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);\n\n      // Check if provider supports realtime updates\n      let identifier = parseKeyString(mutableObject.identifier);\n      let provider = this.getProvider(identifier);\n\n      if (\n        provider !== undefined &&\n        provider.observe !== undefined &&\n        this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)\n      ) {\n        let unobserve = provider.observe(identifier, (updatedModel) => {\n          // modified can sometimes be undefined, so make it 0 in this case\n          const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;\n          if (updatedModel.persisted > mutableObjectModification) {\n            // Don't replace with a stale model. This can happen on slow connections when multiple mutations happen\n            // in rapid succession and intermediate persistence states are returned by the observe function.\n            updatedModel = this.applyGetInterceptors(identifier, updatedModel);\n            mutableObject.$refresh(updatedModel);\n          }\n        });\n        mutableObject.$on('$_destroy', () => {\n          unobserve();\n        });\n      }\n    }\n\n    return mutableObject;\n  }\n\n  /**\n   * Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.\n   * @param {DomainObject} domainObject an object to refresh from its persistence store\n   * @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and\n   *          dirty/in-transaction objects use and the provider.get method\n   * @returns {Promise<DomainObject>} the provided object, updated to reflect the latest persisted state of the object.\n   */\n  async refresh(domainObject, forceRemote = false) {\n    const refreshedObject = await this.get(domainObject.identifier, null, forceRemote);\n\n    if (domainObject.isMutable) {\n      domainObject.$refresh(refreshedObject);\n    } else {\n      refresh(domainObject, refreshedObject);\n    }\n\n    return domainObject;\n  }\n\n  /**\n   * Determine if the object can be mutated.\n   * @param {Identifier} identifier An object identifier\n   * @returns {boolean} true if the object can be mutated, otherwise returns false\n   */\n  supportsMutation(identifier) {\n    return this.isPersistable(identifier);\n  }\n\n  /**\n   * Observe changes to a domain object.\n   * @param {DomainObject} domainObject the object to observe\n   * @param {string} path the property to observe\n   * @param {Function} callback a callback to invoke when new values for this property are observed.\n   * @returns {() => void} a function to unsubscribe from the updates\n   */\n  observe(domainObject, path, callback) {\n    if (domainObject.isMutable) {\n      return domainObject.$observe(path, callback);\n    } else {\n      let mutable = this.toMutable(domainObject);\n      mutable.$observe(path, callback);\n\n      return () => mutable.$destroy();\n    }\n  }\n\n  /**\n   * @param {module:openmct.ObjectAPI~Identifier} identifier\n   * @returns {string} A string representation of the given identifier, including namespace and key\n   */\n  makeKeyString(identifier) {\n    return makeKeyString(identifier);\n  }\n\n  /**\n   * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.\n   * @returns {module:openmct.ObjectAPI~Identifier} An identifier object\n   */\n  parseKeyString(keyString) {\n    return parseKeyString(keyString);\n  }\n\n  /**\n   * Given any number of identifiers, will return true if they are all equal, otherwise false.\n   * @param {module:openmct.ObjectAPI~Identifier[]} identifiers\n   */\n  areIdsEqual(...identifiers) {\n    const firstIdentifier = this.#parseKeyString(identifiers[0]);\n\n    return identifiers.map(this.#parseKeyString).every((identifier) => {\n      return (\n        identifier === firstIdentifier ||\n        (identifier.namespace === firstIdentifier.namespace &&\n          identifier.key === firstIdentifier.key)\n      );\n    });\n  }\n\n  /**\n   * Given an original path check if the path is reachable via root\n   * @param {Array<DomainObject>} originalPath an array of path objects to check\n   * @returns {boolean} whether the domain object is reachable\n   */\n  isReachable(originalPath) {\n    if (originalPath && originalPath.length) {\n      return originalPath[originalPath.length - 1].type === 'root';\n    }\n\n    return false;\n  }\n\n  /**\n   * Check if a path contains a domain object with a given key string\n   * @param {string} keyStringToCheck the keystring to check for\n   * @param {Array<DomainObject>} path the path to check\n   * @returns {boolean} true if the path contains a DomainObject with the given keystring, otherwise false\n   */\n  #pathContainsDomainObject(keyStringToCheck, path) {\n    if (!keyStringToCheck) {\n      return false;\n    }\n\n    return path.some((pathElement) => {\n      const identifierToCheck = this.#parseKeyString(keyStringToCheck);\n\n      return this.areIdsEqual(identifierToCheck, pathElement.identifier);\n    });\n  }\n\n  /**\n   * Given an identifier, constructs the original path by walking up its parents\n   * @param {Identifier} identifier\n   * @param {Array<DomainObject>} path an array of path objects\n   * @param {AbortSignal} abortSignal (optional) signal to abort fetch requests\n   * @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects\n   */\n  async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {\n    let domainObject;\n\n    if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {\n      domainObject = await this.get(identifierOrObject, abortSignal);\n    } else {\n      domainObject = identifierOrObject;\n    }\n\n    if (!domainObject) {\n      return [];\n    }\n\n    path.push(domainObject);\n    const { location } = domainObject;\n    if (location && !this.#pathContainsDomainObject(location, path)) {\n      // if we have a location, and we don't already have this in our constructed path,\n      // then keep walking up the path\n      return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);\n    } else {\n      return path;\n    }\n  }\n\n  /**\n   * Parse and construct an `objectPath` from a `navigationPath`.\n   *\n   * A `navigationPath` is a string of the form `\"/browse/<keyString>/<keyString>/...\"` that is used\n   * by the Open MCT router to navigate to a specific object.\n   *\n   * Throws an error if the `navigationPath` is malformed.\n   *\n   * @param {string} navigationPath\n   * @returns {DomainObject[]} objectPath\n   */\n  async getRelativeObjectPath(navigationPath) {\n    if (!navigationPath.startsWith('/browse/')) {\n      throw new Error(`Malformed navigation path: \"${navigationPath}\"`);\n    }\n\n    navigationPath = navigationPath.replace('/browse/', '');\n\n    if (!navigationPath || navigationPath === '/') {\n      return [];\n    }\n\n    // Remove any query params and split on '/'\n    const keyStrings = navigationPath.split('?')?.[0].split('/');\n\n    if (keyStrings[0] !== 'ROOT') {\n      keyStrings.unshift('ROOT');\n    }\n\n    const objectPath = (\n      await Promise.all(\n        keyStrings.map((keyString) =>\n          this.supportsMutation(keyString)\n            ? this.getMutable(this.#parseKeyString(keyString))\n            : this.get(this.#parseKeyString(keyString))\n        )\n      )\n    ).reverse();\n\n    return objectPath;\n  }\n\n  /**\n   * Check if the object is a link based on its path\n   * @param {DomainObject} domainObject the DomainObject to check\n   * @param {Array<DomainObject>} objectPath the object path to check\n   * @returns {boolean} true if the object path is a link, otherwise false\n   */\n  isObjectPathToALink(domainObject, objectPath) {\n    return (\n      objectPath !== undefined &&\n      objectPath.length > 1 &&\n      domainObject.location !== this.#makeKeyString(objectPath[1].identifier)\n    );\n  }\n\n  /**\n   * Check if a transaction is active\n   * @returns {boolean} true if a transaction is active, otherwise false\n   */\n  isTransactionActive() {\n    return this.transaction !== undefined && this.transaction !== null;\n  }\n\n  /**\n   * Check if a domain object has already been persisted\n   * @param {DomainObject} domainObject the domain object to check\n   * @returns {boolean} true if the domain object has already been persisted, otherwise false\n   */\n  #hasAlreadyBeenPersisted(domainObject) {\n    // modified can sometimes be undefined, so make it 0 in this case\n    const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER;\n    const result = domainObject.persisted !== undefined && domainObject.persisted >= modified;\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/api/objects/ObjectAPISearchSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from '../../utils/testing.js';\n\ndescribe('The Object API Search Function', () => {\n  describe('The infrastructure', () => {\n    const MOCK_PROVIDER_KEY = 'mockProvider';\n    const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';\n    const MOCK_PROVIDER_SEARCH_DELAY = 15000;\n    const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;\n    const TOTAL_TIME_ELAPSED = 21000;\n    const BASE_TIME = new Date(2021, 0, 1);\n\n    let mockObjectProvider;\n    let anotherMockObjectProvider;\n    let openmct;\n\n    beforeEach((done) => {\n      openmct = createOpenMct();\n\n      mockObjectProvider = jasmine.createSpyObj('mock object provider', [\n        'search',\n        'supportsSearchType'\n      ]);\n      anotherMockObjectProvider = jasmine.createSpyObj('another mock object provider', [\n        'search',\n        'supportsSearchType'\n      ]);\n      openmct.objects.addProvider('objects', mockObjectProvider);\n      openmct.objects.addProvider('other-objects', anotherMockObjectProvider);\n      mockObjectProvider.supportsSearchType.and.callFake(() => {\n        return true;\n      });\n      mockObjectProvider.search.and.callFake(() => {\n        return new Promise((resolve) => {\n          const mockProviderSearch = {\n            name: MOCK_PROVIDER_KEY,\n            start: new Date()\n          };\n\n          setTimeout(() => {\n            mockProviderSearch.end = new Date();\n\n            return resolve(mockProviderSearch);\n          }, MOCK_PROVIDER_SEARCH_DELAY);\n        });\n      });\n      anotherMockObjectProvider.supportsSearchType.and.callFake(() => {\n        return true;\n      });\n      anotherMockObjectProvider.search.and.callFake(() => {\n        return new Promise((resolve) => {\n          const anotherMockProviderSearch = {\n            name: ANOTHER_MOCK_PROVIDER_KEY,\n            start: new Date()\n          };\n\n          setTimeout(() => {\n            anotherMockProviderSearch.end = new Date();\n\n            return resolve(anotherMockProviderSearch);\n          }, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);\n        });\n      });\n      openmct.on('start', () => {\n        done();\n      });\n      openmct.startHeadless();\n    });\n    afterEach(async () => {\n      await resetApplicationState(openmct);\n    });\n    it(\"uses each objects given provider's search function\", () => {\n      openmct.objects.search('foo');\n      expect(mockObjectProvider.search).toHaveBeenCalled();\n    });\n    it('provides each providers results as promises that resolve in parallel', async () => {\n      jasmine.clock().install();\n      jasmine.clock().mockDate(BASE_TIME);\n      const resultsPromises = openmct.objects.search('foo');\n      jasmine.clock().tick(TOTAL_TIME_ELAPSED);\n      const results = await Promise.all(resultsPromises);\n      const mockProviderResults = results.find((result) => result.name === MOCK_PROVIDER_KEY);\n      const anotherMockProviderResults = results.find(\n        (result) => result.name === ANOTHER_MOCK_PROVIDER_KEY\n      );\n      const mockProviderStart = mockProviderResults.start.getTime();\n      const mockProviderEnd = mockProviderResults.end.getTime();\n      const anotherMockProviderStart = anotherMockProviderResults.start.getTime();\n      const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();\n      const searchElapsedTime =\n        Math.max(mockProviderEnd, anotherMockProviderEnd) -\n        Math.min(mockProviderEnd, anotherMockProviderEnd);\n\n      expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);\n      expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);\n      expect(searchElapsedTime).toBeLessThan(\n        MOCK_PROVIDER_SEARCH_DELAY + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY\n      );\n\n      jasmine.clock().uninstall();\n    });\n  });\n\n  describe('The in-memory search indexer', () => {\n    let openmct;\n    let mockDomainObject1;\n    let mockIdentifier1;\n    let mockDomainObject2;\n    let mockIdentifier2;\n    let mockDomainObject3;\n    let mockIdentifier3;\n\n    beforeEach((done) => {\n      openmct = createOpenMct();\n      const defaultObjectProvider = openmct.objects.getProvider({\n        key: '',\n        namespace: ''\n      });\n      openmct.objects.addProvider('foo', defaultObjectProvider);\n      spyOn(openmct.objects.inMemorySearchProvider, 'search').and.callThrough();\n      spyOn(openmct.objects.inMemorySearchProvider, 'localSearchForObjects').and.callThrough();\n\n      openmct.on('start', async () => {\n        mockIdentifier1 = {\n          key: 'some-object',\n          namespace: 'foo'\n        };\n        mockDomainObject1 = {\n          type: 'clock',\n          name: 'fooRabbit',\n          identifier: mockIdentifier1\n        };\n        mockIdentifier2 = {\n          key: 'some-other-object',\n          namespace: 'foo'\n        };\n        mockDomainObject2 = {\n          type: 'clock',\n          name: 'fooBear',\n          identifier: mockIdentifier2\n        };\n        mockIdentifier3 = {\n          key: 'yet-another-object',\n          namespace: 'foo'\n        };\n        mockDomainObject3 = {\n          type: 'clock',\n          name: 'redBear',\n          identifier: mockIdentifier3\n        };\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);\n        done();\n      });\n      openmct.startHeadless();\n    });\n\n    afterEach(async () => {\n      await resetApplicationState(openmct);\n    });\n\n    it('can provide indexing without a provider', () => {\n      openmct.objects.search('foo');\n      expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();\n    });\n\n    it('can do partial search', async () => {\n      const searchPromises = openmct.objects.search('foo');\n      const searchResults = await Promise.all(searchPromises);\n      expect(searchResults[0].length).toBe(2);\n    });\n\n    it('returns nothing when appropriate', async () => {\n      const searchPromises = openmct.objects.search('laser');\n      const searchResults = await Promise.all(searchPromises);\n      expect(searchResults[0].length).toBe(0);\n    });\n\n    it('returns exact matches', async () => {\n      const searchPromises = openmct.objects.search('redBear');\n      const searchResults = await Promise.all(searchPromises);\n      expect(searchResults[0].length).toBe(1);\n    });\n\n    describe('Without Shared Workers', () => {\n      let sharedWorkerToRestore;\n      beforeEach(async () => {\n        // use local worker\n        sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;\n        openmct.objects.inMemorySearchProvider.worker = null;\n        // reindex locally\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);\n        await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);\n      });\n      afterEach(() => {\n        openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;\n      });\n      it('calls local search', () => {\n        openmct.objects.search('foo');\n        expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();\n      });\n\n      it('can do partial search', async () => {\n        const searchPromises = openmct.objects.search('foo');\n        const searchResults = await Promise.all(searchPromises);\n        expect(searchResults[0].length).toBe(2);\n      });\n\n      it('returns nothing when appropriate', async () => {\n        const searchPromises = openmct.objects.search('laser');\n        const searchResults = await Promise.all(searchPromises);\n        expect(searchResults[0].length).toBe(0);\n      });\n\n      it('returns exact matches', async () => {\n        const searchPromises = openmct.objects.search('redBear');\n        const searchResults = await Promise.all(searchPromises);\n        expect(searchResults[0].length).toBe(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/objects/ObjectAPISpec.js",
    "content": "import { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport ObjectAPI from './ObjectAPI.js';\n\ndescribe('The Object API', () => {\n  let objectAPI;\n  let typeRegistry;\n  let openmct = {};\n  let mockDomainObject;\n  const TEST_NAMESPACE = 'test-namespace';\n  const TEST_KEY = 'test-key';\n  const USERNAME = 'Joan Q Public';\n  const FIFTEEN_MINUTES = 15 * 60 * 1000;\n\n  beforeEach((done) => {\n    typeRegistry = jasmine.createSpyObj('typeRegistry', ['get']);\n    const userProvider = {\n      isLoggedIn() {\n        return true;\n      },\n      getCurrentUser() {\n        return Promise.resolve({\n          getName() {\n            return USERNAME;\n          }\n        });\n      },\n      getPossibleRoles() {\n        return Promise.resolve([]);\n      }\n    };\n    openmct = createOpenMct();\n    openmct.user.setProvider(userProvider);\n    objectAPI = openmct.objects;\n\n    openmct.editor = {};\n    openmct.editor.isEditing = () => false;\n\n    mockDomainObject = {\n      identifier: {\n        namespace: TEST_NAMESPACE,\n        key: TEST_KEY\n      },\n      name: 'test object',\n      type: 'test-type'\n    };\n    openmct.on('start', () => {\n      done();\n    });\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    await resetApplicationState(openmct);\n  });\n\n  describe('The save function', () => {\n    it('Rejects if no provider available', async () => {\n      let rejected = false;\n      objectAPI.providers = {};\n      objectAPI.fallbackProvider = null;\n\n      try {\n        await objectAPI.save(mockDomainObject);\n      } catch (error) {\n        rejected = true;\n      }\n\n      expect(rejected).toBe(true);\n    });\n    describe('when a provider is available', () => {\n      let mockProvider;\n      beforeEach(() => {\n        mockProvider = jasmine.createSpyObj('mock provider', ['create', 'update']);\n        mockProvider.create.and.returnValue(Promise.resolve(true));\n        mockProvider.update.and.returnValue(Promise.resolve(true));\n        objectAPI.addProvider(TEST_NAMESPACE, mockProvider);\n      });\n      it(\"Adds a 'created' timestamp to new objects\", async () => {\n        await objectAPI.save(mockDomainObject);\n        expect(mockDomainObject.created).not.toBeUndefined();\n      });\n      it(\"Calls 'create' on provider if object is new\", async () => {\n        await objectAPI.save(mockDomainObject);\n        expect(mockProvider.create).toHaveBeenCalled();\n        expect(mockProvider.update).not.toHaveBeenCalled();\n      });\n      it(\"Calls 'update' on provider if object is not new\", async () => {\n        mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;\n        mockDomainObject.modified = Date.now();\n\n        await objectAPI.save(mockDomainObject);\n        expect(mockProvider.create).not.toHaveBeenCalled();\n        expect(mockProvider.update).toHaveBeenCalled();\n      });\n      describe('the persisted timestamp for existing objects', () => {\n        let persistedTimestamp;\n        beforeEach(() => {\n          persistedTimestamp = Date.now() - FIFTEEN_MINUTES;\n          mockDomainObject.persisted = persistedTimestamp;\n          mockDomainObject.modified = Date.now();\n        });\n\n        it('is updated', async () => {\n          await objectAPI.save(mockDomainObject);\n          expect(mockDomainObject.persisted).toBeDefined();\n          expect(mockDomainObject.persisted > persistedTimestamp).toBe(true);\n        });\n        it('is >= modified timestamp', async () => {\n          await objectAPI.save(mockDomainObject);\n          expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);\n        });\n      });\n      describe('the persisted timestamp for new objects', () => {\n        it('is updated', async () => {\n          await objectAPI.save(mockDomainObject);\n          expect(mockDomainObject.persisted).toBeDefined();\n        });\n        it('is >= modified timestamp', async () => {\n          await objectAPI.save(mockDomainObject);\n          expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);\n        });\n      });\n\n      it(\"Sets the current user for 'createdBy' on new objects\", async () => {\n        await objectAPI.save(mockDomainObject);\n        expect(mockDomainObject.createdBy).toBe(USERNAME);\n      });\n      it(\"Sets the current user for 'modifiedBy' on existing objects\", async () => {\n        mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;\n        mockDomainObject.modified = Date.now();\n\n        await objectAPI.save(mockDomainObject);\n        expect(mockDomainObject.modifiedBy).toBe(USERNAME);\n      });\n\n      it('Does not persist if the object is unchanged', () => {\n        mockDomainObject.persisted = mockDomainObject.modified = Date.now();\n\n        objectAPI.save(mockDomainObject);\n        expect(mockProvider.create).not.toHaveBeenCalled();\n        expect(mockProvider.update).not.toHaveBeenCalled();\n      });\n\n      describe('Shows a notification on persistence conflict', () => {\n        beforeEach(() => {\n          openmct.notifications.error = jasmine.createSpy('error');\n        });\n\n        it('on create', () => {\n          mockProvider.create.and.returnValue(\n            Promise.reject(new openmct.objects.errors.Conflict('Test Conflict error'))\n          );\n\n          return objectAPI.save(mockDomainObject).catch(() => {\n            expect(openmct.notifications.error).toHaveBeenCalledWith(\n              `Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`\n            );\n          });\n        });\n\n        it('on update', () => {\n          mockProvider.update.and.returnValue(\n            Promise.reject(new openmct.objects.errors.Conflict('Test Conflict error'))\n          );\n          mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;\n          mockDomainObject.modified = Date.now();\n\n          return objectAPI.save(mockDomainObject).catch(() => {\n            expect(openmct.notifications.error).toHaveBeenCalledWith(\n              `Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`\n            );\n          });\n        });\n      });\n    });\n  });\n\n  describe('The get function', () => {\n    describe('when a provider is available', () => {\n      let mockProvider;\n      let mockInterceptor;\n      let anotherMockInterceptor;\n      let notApplicableMockInterceptor;\n      beforeEach(() => {\n        mockProvider = jasmine.createSpyObj('mock provider', ['get']);\n        mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));\n\n        mockInterceptor = jasmine.createSpyObj('mock interceptor', ['appliesTo', 'invoke']);\n        mockInterceptor.appliesTo.and.returnValue(true);\n        mockInterceptor.invoke.and.callFake((identifier, object) => {\n          return Object.assign(\n            {\n              changed: true\n            },\n            object\n          );\n        });\n\n        anotherMockInterceptor = jasmine.createSpyObj('another mock interceptor', [\n          'appliesTo',\n          'invoke'\n        ]);\n        anotherMockInterceptor.appliesTo.and.returnValue(true);\n        anotherMockInterceptor.invoke.and.callFake((identifier, object) => {\n          return Object.assign(\n            {\n              alsoChanged: true\n            },\n            object\n          );\n        });\n\n        notApplicableMockInterceptor = jasmine.createSpyObj('not applicable mock interceptor', [\n          'appliesTo',\n          'invoke'\n        ]);\n        notApplicableMockInterceptor.appliesTo.and.returnValue(false);\n        notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {\n          return Object.assign(\n            {\n              shouldNotBeChanged: true\n            },\n            object\n          );\n        });\n        objectAPI.addProvider(TEST_NAMESPACE, mockProvider);\n        objectAPI.addGetInterceptor(mockInterceptor);\n        objectAPI.addGetInterceptor(anotherMockInterceptor);\n        objectAPI.addGetInterceptor(notApplicableMockInterceptor);\n      });\n\n      it('Caches multiple requests for the same object', () => {\n        const promises = [];\n        expect(mockProvider.get.calls.count()).toBe(0);\n        promises.push(objectAPI.get(mockDomainObject.identifier));\n        expect(mockProvider.get.calls.count()).toBe(1);\n        promises.push(objectAPI.get(mockDomainObject.identifier));\n        expect(mockProvider.get.calls.count()).toBe(1);\n\n        return Promise.all(promises);\n      });\n\n      it('applies any applicable interceptors', () => {\n        expect(mockDomainObject.changed).toBeUndefined();\n\n        return objectAPI.get(mockDomainObject.identifier).then((object) => {\n          expect(object.changed).toBeTrue();\n          expect(object.alsoChanged).toBeTrue();\n          expect(object.shouldNotBeChanged).toBeUndefined();\n        });\n      });\n\n      it('displays a notification in the event of an error', () => {\n        openmct.notifications.warn = jasmine.createSpy('warn');\n        mockProvider.get.and.returnValue(\n          Promise.reject({\n            name: 'Error',\n            status: 404,\n            statusText: 'Not Found'\n          })\n        );\n\n        return objectAPI.get(mockDomainObject.identifier).catch(() => {\n          expect(openmct.notifications.warn).toHaveBeenCalledWith(\n            `Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`\n          );\n        });\n      });\n    });\n  });\n\n  describe('the mutation API', () => {\n    let testObject;\n    let updatedTestObject;\n    let mutable;\n    let mockProvider;\n    let callbacks = [];\n\n    beforeEach(function () {\n      objectAPI = new ObjectAPI(typeRegistry, openmct);\n      testObject = {\n        identifier: {\n          namespace: TEST_NAMESPACE,\n          key: TEST_KEY\n        },\n        name: 'test object',\n        type: 'notebook',\n        otherAttribute: 'other-attribute-value',\n        modified: 0,\n        persisted: 0,\n        objectAttribute: {\n          embeddedObject: {\n            embeddedKey: 'embedded-value'\n          }\n        }\n      };\n      updatedTestObject = Object.assign(\n        {\n          otherAttribute: 'changed-attribute-value'\n        },\n        testObject\n      );\n      updatedTestObject.modified = 1;\n      updatedTestObject.persisted = 1;\n\n      mockProvider = jasmine.createSpyObj('mock provider', [\n        'get',\n        'create',\n        'update',\n        'observe',\n        'observeObjectChanges'\n      ]);\n      mockProvider.get.and.returnValue(Promise.resolve(testObject));\n      mockProvider.create.and.returnValue(Promise.resolve(true));\n      mockProvider.update.and.returnValue(Promise.resolve(true));\n      mockProvider.observeObjectChanges.and.callFake(() => {\n        callbacks[0](updatedTestObject);\n        callbacks.splice(0, 1);\n\n        return () => {};\n      });\n      mockProvider.observe.and.callFake((id, callback) => {\n        if (callbacks.length === 0) {\n          callbacks.push(callback);\n        } else {\n          callbacks[0] = callback;\n        }\n\n        return () => {};\n      });\n\n      objectAPI.addProvider(TEST_NAMESPACE, mockProvider);\n\n      return objectAPI.getMutable(testObject.identifier).then((object) => {\n        mutable = object;\n\n        return mutable;\n      });\n    });\n\n    afterEach(() => {\n      mutable.$destroy();\n    });\n\n    it('mutates the original object', () => {\n      const MUTATED_NAME = 'mutated name';\n      objectAPI.mutate(testObject, 'name', MUTATED_NAME);\n      expect(testObject.name).toBe(MUTATED_NAME);\n    });\n\n    it('Provides a way of refreshing an object from the persistence store', () => {\n      const modifiedTestObject = JSON.parse(JSON.stringify(testObject));\n      const OTHER_ATTRIBUTE_VALUE = 'Modified value';\n      const NEW_ATTRIBUTE_VALUE = 'A new attribute';\n      modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;\n      modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;\n      delete modifiedTestObject.objectAttribute;\n\n      spyOn(objectAPI, 'get');\n      objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));\n\n      expect(objectAPI.get).not.toHaveBeenCalled();\n\n      return objectAPI.refresh(testObject).then(() => {\n        expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier, null, false);\n\n        expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);\n        expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);\n        expect(testObject.objectAttribute).not.toBeDefined();\n      });\n    });\n\n    describe('uses a MutableDomainObject', () => {\n      it('and retains properties of original object ', function () {\n        expect(hasOwnProperty(mutable, 'identifier')).toBe(true);\n        expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);\n        expect(mutable.identifier).toEqual(testObject.identifier);\n        expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);\n      });\n\n      it('that is identical to original object when serialized', function () {\n        expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));\n      });\n\n      it('that observes for object changes', function () {\n        let mockListener = jasmine.createSpy('mockListener');\n        objectAPI.observe(testObject, '*', mockListener);\n        mockProvider.observeObjectChanges();\n        expect(mockListener).toHaveBeenCalled();\n      });\n    });\n\n    describe('uses events', function () {\n      let testObjectDuplicate;\n      let mutableSecondInstance;\n\n      beforeEach(function () {\n        // Duplicate object to guarantee we are not sharing object instance, which would invalidate test\n        testObjectDuplicate = JSON.parse(JSON.stringify(testObject));\n        mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate);\n      });\n\n      afterEach(() => {\n        mutableSecondInstance.$destroy();\n      });\n\n      it('to stay synchronized when mutated', function () {\n        objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');\n        expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');\n      });\n\n      it('to indicate when a property changes', function () {\n        let mutationCallback = jasmine.createSpy('mutation-callback');\n        let unlisten;\n\n        return new Promise(function (resolve) {\n          mutationCallback.and.callFake(resolve);\n          unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);\n          objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');\n        }).then(function () {\n          expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value');\n          unlisten();\n        });\n      });\n\n      it('to indicate when a child property has changed', function () {\n        let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');\n        let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');\n        let objectAttributeCallback = jasmine.createSpy('objectAttribute');\n        let listeners = [];\n\n        return new Promise(function (resolve) {\n          objectAttributeCallback.and.callFake(resolve);\n\n          listeners.push(\n            objectAPI.observe(\n              mutableSecondInstance,\n              'objectAttribute.embeddedObject.embeddedKey',\n              embeddedKeyCallback\n            )\n          );\n          listeners.push(\n            objectAPI.observe(\n              mutableSecondInstance,\n              'objectAttribute.embeddedObject',\n              embeddedObjectCallback\n            )\n          );\n          listeners.push(\n            objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)\n          );\n\n          objectAPI.mutate(\n            mutable,\n            'objectAttribute.embeddedObject.embeddedKey',\n            'updated-embedded-value'\n          );\n        }).then(function () {\n          expect(embeddedKeyCallback).toHaveBeenCalledWith(\n            'updated-embedded-value',\n            'embedded-value'\n          );\n          expect(embeddedObjectCallback).toHaveBeenCalledWith(\n            {\n              embeddedKey: 'updated-embedded-value'\n            },\n            {\n              embeddedKey: 'embedded-value'\n            }\n          );\n          expect(objectAttributeCallback).toHaveBeenCalledWith(\n            {\n              embeddedObject: {\n                embeddedKey: 'updated-embedded-value'\n              }\n            },\n            {\n              embeddedObject: {\n                embeddedKey: 'embedded-value'\n              }\n            }\n          );\n\n          listeners.forEach((listener) => listener());\n        });\n      });\n    });\n  });\n\n  describe('getOriginalPath', () => {\n    let mockGrandParentObject;\n    let mockParentObject;\n    let mockChildObject;\n\n    beforeEach(() => {\n      const mockObjectProvider = jasmine.createSpyObj('mock object provider', [\n        'create',\n        'update',\n        'get'\n      ]);\n\n      mockGrandParentObject = {\n        type: 'folder',\n        name: 'Grand Parent Folder',\n        location: 'fooNameSpace:child',\n        identifier: {\n          key: 'grandParent',\n          namespace: 'fooNameSpace'\n        }\n      };\n      mockParentObject = {\n        type: 'folder',\n        name: 'Parent Folder',\n        location: 'fooNameSpace:grandParent',\n        identifier: {\n          key: 'parent',\n          namespace: 'fooNameSpace'\n        }\n      };\n      mockChildObject = {\n        type: 'folder',\n        name: 'Child Folder',\n        location: 'fooNameSpace:parent',\n        identifier: {\n          key: 'child',\n          namespace: 'fooNameSpace'\n        }\n      };\n\n      // eslint-disable-next-line require-await\n      mockObjectProvider.get = async (identifier) => {\n        if (identifier.key === mockGrandParentObject.identifier.key) {\n          return mockGrandParentObject;\n        } else if (identifier.key === mockParentObject.identifier.key) {\n          return mockParentObject;\n        } else if (identifier.key === mockChildObject.identifier.key) {\n          return mockChildObject;\n        } else {\n          return null;\n        }\n      };\n\n      openmct.objects.addProvider('fooNameSpace', mockObjectProvider);\n\n      mockObjectProvider.create.and.returnValue(Promise.resolve(true));\n      mockObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      openmct.objects.addProvider('fooNameSpace', mockObjectProvider);\n    });\n\n    it('can construct paths even with cycles', async () => {\n      const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);\n      expect(objectPath.length).toEqual(3);\n    });\n  });\n\n  describe('transactions', () => {\n    beforeEach(() => {\n      spyOn(openmct.editor, 'isEditing').and.returnValue(true);\n    });\n\n    it('there is no active transaction', () => {\n      expect(objectAPI.isTransactionActive()).toBe(false);\n    });\n\n    it('start a transaction', () => {\n      objectAPI.startTransaction();\n      expect(objectAPI.isTransactionActive()).toBe(true);\n    });\n\n    it('has active transaction', () => {\n      objectAPI.startTransaction();\n      const activeTransaction = objectAPI.getActiveTransaction();\n      expect(activeTransaction).not.toBe(null);\n    });\n\n    it('end a transaction', () => {\n      objectAPI.endTransaction();\n      expect(objectAPI.isTransactionActive()).toBe(false);\n    });\n\n    it('returns dirty object on get', (done) => {\n      spyOn(objectAPI, 'supportsMutation').and.returnValue(true);\n\n      objectAPI.startTransaction();\n      objectAPI.mutate(mockDomainObject, 'name', 'dirty object');\n\n      const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier);\n\n      objectAPI\n        .get(mockDomainObject.identifier)\n        .then((object) => {\n          const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject);\n          expect(areEqual).toBe(true);\n        })\n        .finally(done);\n    });\n  });\n});\n\nfunction hasOwnProperty(object, property) {\n  return Object.prototype.hasOwnProperty.call(object, property);\n}\n"
  },
  {
    "path": "src/api/objects/RootObjectCompositionProvider.js",
    "content": "import CompositionProvider from '../composition/CompositionProvider.js';\n\nexport default class RootObjectCompositionProvider extends CompositionProvider {\n  #openmct;\n  #boundCallbacks = new Set();\n  #rootRegistry;\n\n  constructor(openmct, rootRegistry) {\n    super(openmct, openmct.composition);\n    this.#openmct = openmct;\n    this.#rootRegistry = rootRegistry;\n  }\n\n  appliesTo(domainObject) {\n    return Boolean(domainObject?.identifier?.key === 'ROOT');\n  }\n\n  load(domainObject) {\n    return this.#rootRegistry.getRoots();\n  }\n\n  on(domainObject, event, callback, context) {\n    let boundCallbackObject = this.#addListener(event, callback, context);\n\n    this.#rootRegistry.addEventListener(event, boundCallbackObject.callbackWrapper, context);\n  }\n\n  off(domainObject, event, callback, context) {\n    const boundCallbackObject = this.#removeListener(event, callback, context);\n\n    if (boundCallbackObject !== undefined) {\n      this.#rootRegistry.removeEventListener(event, boundCallbackObject.callbackWrapper, context);\n    }\n  }\n\n  add(domainObject, childId) {\n    this.#rootRegistry.addRoot(childId);\n  }\n\n  remove(domainObject, childId) {\n    this.#rootRegistry.removeRoot(childId);\n  }\n\n  includes(domainObject, childId) {\n    return this.#rootRegistry.isRootObject(childId);\n  }\n\n  #addListener(event, callback, context) {\n    let boundCallback = callback;\n\n    if (context !== undefined) {\n      boundCallback = callback.bind(context);\n    }\n\n    const boundCallbackObject = {\n      event,\n      callback,\n      context,\n      boundCallback,\n      callbackWrapper: (e) => {\n        boundCallback(e.detail);\n      }\n    };\n    this.#boundCallbacks.add(boundCallbackObject);\n\n    return boundCallbackObject;\n  }\n\n  #removeListener(event, callback, context) {\n    const boundCallbackObject = this.#boundCallbacks.entries().find((o) => {\n      return o.event === event && o.callback === callback && o.context === context;\n    });\n\n    if (boundCallbackObject !== undefined) {\n      this.#boundCallbacks.delete(boundCallbackObject);\n    }\n\n    return boundCallbackObject;\n  }\n}\n"
  },
  {
    "path": "src/api/objects/RootObjectProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Provides the root object for the Open MCT application.\n */\nclass RootObjectProvider {\n  /**\n   * @param {RootRegistry} rootRegistry - The registry containing root objects.\n   */\n  constructor(rootRegistry) {\n    if (!RootObjectProvider.instance) {\n      this.rootRegistry = rootRegistry;\n      this.rootObject = {\n        identifier: {\n          key: 'ROOT',\n          namespace: ''\n        },\n        name: 'Open MCT',\n        type: 'root'\n      };\n      RootObjectProvider.instance = this;\n    } else if (rootRegistry) {\n      // if called twice, update instance rootRegistry\n      RootObjectProvider.instance.rootRegistry = rootRegistry;\n    }\n\n    return RootObjectProvider.instance; // eslint-disable-line no-constructor-return\n  }\n\n  /**\n   * Updates the name of the root object.\n   * @param {string} name - The new name for the root object.\n   */\n  updateName(name) {\n    this.rootObject.name = name;\n  }\n\n  /**\n   * Retrieves the root object\n   * @returns {Promise<RootObject>} A promise that resolves to the root object.\n   */\n  get() {\n    return Promise.resolve(this.rootObject);\n  }\n}\n\n/**\n * Creates or returns an instance of RootObjectProvider.\n * @param {RootRegistry} rootRegistry - The registry containing root objects.\n * @returns {RootObjectProvider} An instance of RootObjectProvider.\n */\nfunction instance(rootRegistry) {\n  return new RootObjectProvider(rootRegistry);\n}\n\nexport default instance;\n\n/**\n * @typedef {import('openmct').Identifier} Identifier\n */\n\n/**\n * @typedef {Object} RootObject\n * @property {Identifier} identifier - The identifier of the root object.\n * @property {string} name - The name of the root object.\n * @property {string} type - The type of the root object.\n * @property {Identifier[]} composition - The composition of the root object.\n */\n\n/**\n * @typedef {Object} RootRegistry\n * @property {() => Promise<Identifier[]>} getRoots - A method that returns a promise resolving to an array of root identifiers.\n */\n"
  },
  {
    "path": "src/api/objects/RootRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { isIdentifier } from './object-utils.js';\n\n/**\n * Registry for managing root items in Open MCT.\n */\nexport default class RootRegistry extends EventTarget {\n  /**\n   * @param {OpenMCT} openmct - The Open MCT instance.\n   */\n  constructor(openmct) {\n    super();\n    /** @type {Array<RootItemEntry>} */\n    this._rootItems = [];\n    /** @type {OpenMCT} */\n    this._openmct = openmct;\n  }\n\n  /**\n   * Get all registered root items.\n   * @returns {Promise<Array<Identifier>>} A promise that resolves to an array of root item identifiers.\n   */\n  getRoots() {\n    const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);\n    const promises = sortedItems.map((rootItem) => rootItem.provider());\n\n    return Promise.all(promises).then((rootItems) => rootItems.flat());\n  }\n\n  isRootObject(identifier) {\n    return this._rootItems.some((rootItem) =>\n      this._openmct.objects.areIdsEqual(rootItem, identifier)\n    );\n  }\n\n  /**\n   * Add a root item to the registry.\n   * @param {RootItemInput} rootItem - The root item to add.\n   * @param {number} [priority] - The priority of the root item.\n   */\n  addRoot(rootItem, priority) {\n    if (!this._isValid(rootItem)) {\n      return;\n    }\n\n    this._rootItems.push({\n      priority: priority || this._openmct.priority.DEFAULT,\n      provider: typeof rootItem === 'function' ? rootItem : () => rootItem\n    });\n    this.dispatchEvent(new CustomEvent('add', { detail: rootItem }));\n  }\n\n  removeRoot(identifier) {\n    const rootItems = this._rootItems.filter((rootObjectContainer) => {\n      const rootObject = rootObjectContainer.provider();\n\n      return !this._openmct.objects.areIdsEqual(identifier, rootObject);\n    });\n\n    if (rootItems.length !== this._rootItems.length) {\n      this.dispatchEvent(new CustomEvent('remove', { detail: identifier }));\n      this._rootItems = rootItems;\n    }\n  }\n\n  /**\n   * Validate a root item.\n   * @param {RootItemInput} rootItem - The root item to validate.\n   * @returns {boolean} True if the root item is valid, false otherwise.\n   * @private\n   */\n  _isValid(rootItem) {\n    if (isIdentifier(rootItem) || typeof rootItem === 'function') {\n      return true;\n    }\n\n    if (Array.isArray(rootItem)) {\n      return rootItem.every(isIdentifier);\n    }\n\n    return false;\n  }\n}\n\n/**\n * @typedef {Object} RootItemEntry\n * @property {number} priority - The priority of the root item.\n * @property {() => Promise<Identifier | Identifier[]>} provider - A function that returns a promise resolving to a root item or an array of root items.\n */\n\n/**\n * @typedef {import('openmct').Identifier} Identifier\n * @typedef {Identifier | Identifier[] | (() => Promise<Identifier | Identifier[]>)} RootItemInput\n * @typedef {import('openmct').OpenMCT} OpenMCT\n */\n"
  },
  {
    "path": "src/api/objects/Transaction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Represents a transaction for managing changes to domain objects.\n */\nexport default class Transaction {\n  /**\n   * @param {import('./ObjectAPI').default} objectAPI - The object API instance.\n   */\n  constructor(objectAPI) {\n    /** @type {Record<string, DomainObject>} */\n    this.dirtyObjects = {};\n    /** @type {import('./ObjectAPI').default} */\n    this.objectAPI = objectAPI;\n  }\n\n  /**\n   * Adds an object to the transaction.\n   * @param {DomainObject} object - The object to add.\n   */\n  add(object) {\n    const key = this.objectAPI.makeKeyString(object.identifier);\n\n    this.dirtyObjects[key] = object;\n  }\n\n  /**\n   * Cancels the transaction and reverts changes.\n   * @returns {Promise<void[]>}\n   */\n  cancel() {\n    return this._clear();\n  }\n\n  /**\n   * Commits the transaction and saves changes.\n   * @returns {Promise<void[]>}\n   */\n  commit() {\n    const promiseArray = [];\n    const save = this.objectAPI.save.bind(this.objectAPI);\n\n    Object.values(this.dirtyObjects).forEach((object) => {\n      promiseArray.push(this.createDirtyObjectPromise(object, save));\n    });\n\n    return Promise.all(promiseArray);\n  }\n\n  /**\n   * Creates a promise for handling a dirty object.\n   * @template T\n   * @param {DomainObject} object - The dirty object.\n   * @param {(object: DomainObject, ...args: any[]) => Promise<T>} action - The action to perform.\n   * @param {...any} args - Additional arguments for the action.\n   * @returns {Promise<T>}\n   */\n  createDirtyObjectPromise(object, action, ...args) {\n    return new Promise((resolve, reject) => {\n      action(object, ...args)\n        .then((success) => {\n          const key = this.objectAPI.makeKeyString(object.identifier);\n\n          delete this.dirtyObjects[key];\n          resolve(success);\n        })\n        .catch(reject);\n    });\n  }\n\n  /**\n   * Retrieves a dirty object by its identifier.\n   * @param {Identifier} identifier - The object identifier.\n   * @returns {DomainObject | undefined}\n   */\n  getDirtyObject(identifier) {\n    let dirtyObject;\n\n    Object.values(this.dirtyObjects).forEach((object) => {\n      const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);\n      if (areIdsEqual) {\n        dirtyObject = object;\n      }\n    });\n\n    return dirtyObject;\n  }\n\n  /**\n   * Clears the transaction and refreshes objects.\n   * @returns {Promise<void[]>}\n   * @private\n   */\n  _clear() {\n    const promiseArray = [];\n    const action = (obj) => this.objectAPI.refresh(obj, true);\n\n    Object.values(this.dirtyObjects).forEach((object) => {\n      promiseArray.push(this.createDirtyObjectPromise(object, action));\n    });\n\n    return Promise.all(promiseArray);\n  }\n}\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n * @typedef {import('openmct').Identifier} Identifier\n */\n"
  },
  {
    "path": "src/api/objects/TransactionSpec.js",
    "content": "import { makeKeyString, parseKeyString } from 'objectUtils';\n\nimport Transaction from './Transaction.js';\n\nlet openmct = {};\nlet objectAPI;\nlet transaction;\n\ndescribe('Transaction Class', () => {\n  beforeEach(() => {\n    objectAPI = {\n      makeKeyString: (identifier) => makeKeyString(identifier),\n      save: () => Promise.resolve(true),\n      mutate: (object, prop, value) => {\n        object[prop] = value;\n\n        return object;\n      },\n      refresh: (object) => Promise.resolve(object),\n      areIdsEqual: (...identifiers) => {\n        return identifiers.map(parseKeyString).every((identifier) => {\n          return (\n            identifier === identifiers[0] ||\n            (identifier.namespace === identifiers[0].namespace &&\n              identifier.key === identifiers[0].key)\n          );\n        });\n      }\n    };\n\n    transaction = new Transaction(objectAPI);\n\n    openmct.editor = {\n      isEditing: () => true\n    };\n  });\n\n  it('has no dirty objects', () => {\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);\n  });\n\n  it('add(), adds object to dirtyObjects', () => {\n    const mockDomainObjects = createMockDomainObjects();\n    transaction.add(mockDomainObjects[0]);\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);\n  });\n\n  it('cancel(), clears all dirtyObjects', (done) => {\n    const mockDomainObjects = createMockDomainObjects(3);\n    mockDomainObjects.forEach(transaction.add.bind(transaction));\n\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);\n\n    transaction\n      .cancel()\n      .then((success) => {\n        expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);\n      })\n      .finally(done);\n  });\n\n  it('commit(), saves all dirtyObjects', (done) => {\n    const mockDomainObjects = createMockDomainObjects(3);\n    mockDomainObjects.forEach(transaction.add.bind(transaction));\n\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);\n    spyOn(objectAPI, 'save').and.callThrough();\n\n    transaction\n      .commit()\n      .then((success) => {\n        expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);\n        expect(objectAPI.save.calls.count()).toEqual(3);\n      })\n      .finally(done);\n  });\n\n  it('getDirtyObject(), returns correct dirtyObject', () => {\n    const mockDomainObjects = createMockDomainObjects();\n    transaction.add(mockDomainObjects[0]);\n\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);\n    const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);\n\n    expect(dirtyObject).toEqual(mockDomainObjects[0]);\n  });\n\n  it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {\n    const mockDomainObjects = createMockDomainObjects();\n\n    expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);\n    const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);\n\n    expect(dirtyObject).toEqual(undefined);\n  });\n});\n\nfunction createMockDomainObjects(size = 1) {\n  const objects = [];\n\n  while (size > 0) {\n    const mockDomainObject = {\n      identifier: {\n        namespace: 'test-namespace',\n        key: `test-key-${size}`\n      },\n      name: `test object ${size}`,\n      type: 'test-type'\n    };\n\n    objects.push(mockDomainObject);\n\n    size--;\n  }\n\n  return objects;\n}\n"
  },
  {
    "path": "src/api/objects/object-utils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Utility for checking if a thing is an Open MCT Identifier.\n * @private\n */\nexport function isIdentifier(thing) {\n  return (\n    typeof thing === 'object' &&\n    Object.prototype.hasOwnProperty.call(thing, 'key') &&\n    Object.prototype.hasOwnProperty.call(thing, 'namespace')\n  );\n}\n\n/**\n * Utility for checking if a thing is a key string.  Not perfect.\n * @private\n */\nexport function isKeyString(thing) {\n  return typeof thing === 'string';\n}\n\n/**\n * Convert a keyString into an Open MCT Identifier, ex:\n * 'scratch:root' ==> {namespace: 'scratch', key: 'root'}\n *\n * Idempotent.\n *\n * @param keyString\n * @returns identifier\n */\nexport function parseKeyString(keyString) {\n  if (isIdentifier(keyString)) {\n    return keyString;\n  }\n\n  let namespace = '';\n  let key = keyString;\n  for (let i = 0; i < key.length; i++) {\n    if (key[i] === '\\\\' && key[i + 1] === ':') {\n      i++; // skip escape character.\n    } else if (key[i] === ':') {\n      key = key.slice(i + 1);\n      break;\n    }\n\n    namespace += key[i];\n  }\n\n  if (keyString === namespace) {\n    namespace = '';\n  }\n\n  return {\n    namespace: namespace,\n    key: key\n  };\n}\n\n/**\n * Convert an Open MCT Identifier into a keyString, ex:\n * {namespace: 'scratch', key: 'root'} ==> 'scratch:root'\n *\n * Idempotent\n *\n * @param identifier\n * @returns keyString\n */\nexport function makeKeyString(identifier) {\n  if (!identifier) {\n    throw new Error('Cannot make key string from null identifier');\n  }\n\n  if (isKeyString(identifier)) {\n    return identifier;\n  }\n\n  if (!identifier.namespace) {\n    return identifier.key;\n  }\n\n  return [identifier.namespace.replace(/\\\\/g, '\\\\\\\\').replace(/:/g, '\\\\:'), identifier.key].join(\n    ':'\n  );\n}\n\n/**\n * Convert a new domain object into an old format model, removing the\n * identifier and converting the composition array from Open MCT Identifiers\n * to old format keyStrings.\n *\n * @param domainObject\n * @returns oldFormatModel\n */\nexport function toOldFormat(model) {\n  model = JSON.parse(JSON.stringify(model));\n  delete model.identifier;\n  if (model.composition) {\n    model.composition = model.composition.map(makeKeyString);\n  }\n\n  return model;\n}\n\n/**\n * Convert an old format domain object model into a new format domain\n * object.  Adds an identifier using the provided keyString, and converts\n * the composition array to utilize Open MCT Identifiers.\n *\n * @param model\n * @param keyString\n * @returns domainObject\n */\nexport function toNewFormat(model, keyString) {\n  model = JSON.parse(JSON.stringify(model));\n  model.identifier = parseKeyString(keyString);\n  if (model.composition) {\n    model.composition = model.composition.map(parseKeyString);\n  }\n\n  return model;\n}\n\n/**\n * Compare two Open MCT Identifiers, returning true if they are equal.\n *\n * @param identifier\n * @param otherIdentifier\n * @returns Boolean true if identifiers are equal.\n */\nexport function identifierEquals(a, b) {\n  return a.key === b.key && a.namespace === b.namespace;\n}\n\n/**\n * Compare two domain objects, return true if they're the same object.\n * Equality is determined by identifier.\n *\n * @param domainObject\n * @param otherDomainOBject\n * @returns Boolean true if objects are equal.\n */\nexport function objectEquals(a, b) {\n  return identifierEquals(a.identifier, b.identifier);\n}\n\nexport function refresh(oldObject, newObject) {\n  let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));\n  deleted.forEach((propertyName) => delete oldObject[propertyName]);\n  Object.assign(oldObject, newObject);\n}\n"
  },
  {
    "path": "src/api/objects/test/object-utilsSpec.js",
    "content": "import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } from 'objectUtils';\n\ndescribe('objectUtils', function () {\n  describe('keyString util', function () {\n    const EXPECTATIONS = {\n      ROOT: {\n        namespace: '',\n        key: 'ROOT'\n      },\n      mine: {\n        namespace: '',\n        key: 'mine'\n      },\n      'extended:something:with:colons': {\n        key: 'something:with:colons',\n        namespace: 'extended'\n      },\n      'https\\\\://some/url:resourceId': {\n        key: 'resourceId',\n        namespace: 'https://some/url'\n      },\n      'scratch:root': {\n        namespace: 'scratch',\n        key: 'root'\n      },\n      'thingy\\\\:thing:abc123': {\n        namespace: 'thingy:thing',\n        key: 'abc123'\n      }\n    };\n\n    Object.keys(EXPECTATIONS).forEach(function (keyString) {\n      it('parses \"' + keyString + '\".', function () {\n        expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);\n      });\n\n      it('parses and re-encodes \"' + keyString + '\"', function () {\n        const identifier = parseKeyString(keyString);\n        expect(makeKeyString(identifier)).toEqual(keyString);\n      });\n\n      it('is idempotent for \"' + keyString + '\".', function () {\n        const identifier = parseKeyString(keyString);\n        let again = parseKeyString(identifier);\n        expect(identifier).toEqual(again);\n        again = parseKeyString(again);\n        again = parseKeyString(again);\n        expect(identifier).toEqual(again);\n\n        let againKeyString = makeKeyString(again);\n        expect(againKeyString).toEqual(keyString);\n        againKeyString = makeKeyString(againKeyString);\n        againKeyString = makeKeyString(againKeyString);\n        againKeyString = makeKeyString(againKeyString);\n        expect(againKeyString).toEqual(keyString);\n      });\n    });\n  });\n\n  describe('old object conversions', function () {\n    it('translate ids', function () {\n      expect(\n        toNewFormat(\n          {\n            prop: 'someValue'\n          },\n          'objId'\n        )\n      ).toEqual({\n        prop: 'someValue',\n        identifier: {\n          namespace: '',\n          key: 'objId'\n        }\n      });\n    });\n\n    it('translates composition', function () {\n      expect(\n        toNewFormat(\n          {\n            prop: 'someValue',\n            composition: ['anotherObjectId', 'scratch:anotherObjectId']\n          },\n          'objId'\n        )\n      ).toEqual({\n        prop: 'someValue',\n        composition: [\n          {\n            namespace: '',\n            key: 'anotherObjectId'\n          },\n          {\n            namespace: 'scratch',\n            key: 'anotherObjectId'\n          }\n        ],\n        identifier: {\n          namespace: '',\n          key: 'objId'\n        }\n      });\n    });\n  });\n\n  describe('new object conversions', function () {\n    it('removes ids', function () {\n      expect(\n        toOldFormat({\n          prop: 'someValue',\n          identifier: {\n            namespace: '',\n            key: 'objId'\n          }\n        })\n      ).toEqual({\n        prop: 'someValue'\n      });\n    });\n\n    it('translates composition', function () {\n      expect(\n        toOldFormat({\n          prop: 'someValue',\n          composition: [\n            {\n              namespace: '',\n              key: 'anotherObjectId'\n            },\n            {\n              namespace: 'scratch',\n              key: 'anotherObjectId'\n            }\n          ],\n          identifier: {\n            namespace: '',\n            key: 'objId'\n          }\n        })\n      ).toEqual({\n        prop: 'someValue',\n        composition: ['anotherObjectId', 'scratch:anotherObjectId']\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/overlays/Dialog.js",
    "content": "import mount from 'utils/mount';\n\nimport DialogComponent from './components/DialogComponent.vue';\nimport Overlay from './Overlay.js';\n\nclass Dialog extends Overlay {\n  constructor({ iconClass, message, title, hint, timestamp, ...options }) {\n    const { vNode, destroy } = mount({\n      components: {\n        DialogComponent: DialogComponent\n      },\n      provide: {\n        iconClass,\n        message,\n        title,\n        hint,\n        timestamp\n      },\n      template: '<dialog-component></dialog-component>'\n    });\n\n    super({\n      element: vNode.el,\n      size: 'fit',\n      dismissible: false,\n      ...options\n    });\n\n    this.once('destroy', () => {\n      destroy();\n    });\n  }\n}\n\nexport default Dialog;\n"
  },
  {
    "path": "src/api/overlays/Overlay.js",
    "content": "import { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\n\nimport OverlayComponent from './components/OverlayComponent.vue';\n\nconst cssClasses = {\n  large: 'l-overlay-large',\n  small: 'l-overlay-small',\n  fit: 'l-overlay-fit',\n  fullscreen: 'l-overlay-fullscreen',\n  dialog: 'l-overlay-dialog'\n};\n\nclass Overlay extends EventEmitter {\n  constructor({\n    buttons,\n    autoHide = true,\n    dismissible = true,\n    element,\n    onDestroy,\n    onDismiss,\n    size\n  } = {}) {\n    super();\n\n    this.container = document.createElement('div');\n    this.container.classList.add('l-overlay-wrapper', cssClasses[size]);\n\n    this.autoHide = autoHide;\n    this.dismissible = dismissible !== false;\n\n    const { destroy } = mount(\n      {\n        components: {\n          OverlayComponent\n        },\n        provide: {\n          dismiss: this.notifyAndDismiss.bind(this),\n          element,\n          buttons,\n          dismissible: this.dismissible\n        },\n        template: '<overlay-component></overlay-component>'\n      },\n      {\n        element: this.container\n      }\n    );\n\n    this.destroy = destroy;\n\n    if (onDestroy) {\n      this.once('destroy', onDestroy);\n    }\n\n    if (onDismiss) {\n      this.once('dismiss', onDismiss);\n    }\n  }\n\n  dismiss() {\n    this.emit('destroy');\n    this.destroy();\n    this.container.remove();\n  }\n\n  //Ensures that any callers are notified that the overlay is dismissed\n  notifyAndDismiss() {\n    this.emit('dismiss');\n    this.dismiss();\n  }\n\n  /**\n   * @private\n   **/\n  show() {\n    document.body.appendChild(this.container);\n  }\n}\n\nexport default Overlay;\n"
  },
  {
    "path": "src/api/overlays/OverlayAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Dialog from './Dialog.js';\nimport Overlay from './Overlay.js';\nimport ProgressDialog from './ProgressDialog.js';\nimport Selection from './Selection.js';\n\n/**\n * The OverlayAPI is responsible for pre-pending templates to\n * the body of the document, which is useful for displaying templates\n * which need to block the full screen.\n */\nclass OverlayAPI {\n  constructor() {\n    this.activeOverlays = [];\n\n    this.dismissLastOverlay = this.dismissLastOverlay.bind(this);\n\n    document.addEventListener('keyup', (event) => {\n      if (event.key === 'Escape') {\n        this.dismissLastOverlay();\n      }\n    });\n  }\n\n  /**\n   * Shows an overlay\n   * @private\n   * @param {Overlay} overlay - The overlay to show\n   */\n  showOverlay(overlay) {\n    if (this.activeOverlays.length) {\n      const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];\n      if (previousOverlay.autoHide) {\n        previousOverlay.container.classList.add('invisible');\n      }\n    }\n\n    this.activeOverlays.push(overlay);\n\n    overlay.once('destroy', () => {\n      this.activeOverlays.splice(this.activeOverlays.indexOf(overlay), 1);\n\n      if (this.activeOverlays.length) {\n        this.activeOverlays[this.activeOverlays.length - 1].container.classList.remove('invisible');\n      }\n    });\n\n    overlay.show();\n  }\n\n  /**\n   * Dismisses the last overlay\n   * @private\n   */\n  dismissLastOverlay() {\n    let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];\n    if (lastOverlay && lastOverlay.dismissible) {\n      lastOverlay.notifyAndDismiss();\n    }\n  }\n\n  /**\n   * Creates and displays an overlay with the specified options.\n   * @param {OverlayOptions} options - The configuration options for the overlay.\n   * @returns {Overlay} An instance of the Overlay class.\n   */\n  overlay(options) {\n    let overlay = new Overlay(options);\n\n    this.showOverlay(overlay);\n\n    return overlay;\n  }\n\n  /**\n   * Displays a blocking (modal) dialog. This dialog can be used for\n   * displaying messages that require the user's immediate attention.\n   * @param {DialogOptions} options - Defines options for the dialog\n   * @returns {Dialog} An object with a dismiss function that can be called from the calling code to dismiss/destroy the dialog\n   */\n  dialog(options) {\n    let dialog = new Dialog(options);\n\n    this.showOverlay(dialog);\n\n    return dialog;\n  }\n\n  /**\n   * Displays a blocking (modal) progress dialog. This dialog can be used for\n   * displaying messages that require the user's attention, and show progress\n   * @param {ProgressDialogOptions} options - Defines options for the dialog\n   * @returns {ProgressDialog} An object with a dismiss function that can be called from the calling code\n   * to dismiss/destroy the dialog and an updateProgress function that takes progressPercentage(Number 0-100)\n   * and progressText (string)\n   */\n  progressDialog(options) {\n    let progressDialog = new ProgressDialog(options);\n\n    this.showOverlay(progressDialog);\n\n    return progressDialog;\n  }\n\n  /**\n   * Creates and displays a selection overlay\n   * @param {SelectionOptions} options - The options for the selection overlay\n   * @returns {Selection} The created Selection instance\n   */\n  selection(options) {\n    let selection = new Selection(options);\n    this.showOverlay(selection);\n\n    return selection;\n  }\n}\n\nexport default OverlayAPI;\n\n/**\n * @typedef {Object} OverlayOptions\n * @property {HTMLElement} element - The DOM Element to be inserted or shown in the overlay.\n * @property {'large'|'small'|'fit'} size - The preferred size of the overlay.\n * @property {Array<{label: string, callback: Function}>} [buttons] - Optional array of button objects, each with 'label' and 'callback' properties.\n * @property {Function} onDestroy - Callback to be called when the overlay is destroyed.\n * @property {boolean} [dismissible=true] - Whether the overlay can be dismissed by pressing 'esc' or clicking outside of it. Defaults to true.\n */\n\n/**\n * @typedef {Object} DialogOptions\n * @property {string} title - The title to use for the dialog\n * @property {string} iconClass - Class to apply to icon that is shown on dialog\n * @property {string} message - Text that indicates a current message\n * @property {Array<{label: string, callback: Function}>} buttons - A list of buttons with label and callback properties that will be added to the dialog.\n */\n\n/**\n * @typedef {Object} ProgressDialogOptions\n * @property {number | null} progressPerc - The initial progress value (0-100) or null for anonymous progress\n * @property {string} progressText - The initial text to be shown under the progress bar\n * @property {Array<{label: string, callback: Function}>} buttons - A list of buttons with label and callback properties that will be added to the dialog.\n */\n\n/**\n * @typedef {Object} SelectionOptions\n * @property {any} options - The options for the selection overlay\n */\n"
  },
  {
    "path": "src/api/overlays/ProgressDialog.js",
    "content": "import mount from 'utils/mount';\n\nimport ProgressDialogComponent from './components/ProgressDialogComponent.vue';\nimport Overlay from './Overlay.js';\n\nlet component;\nclass ProgressDialog extends Overlay {\n  constructor({\n    progressPerc,\n    progressText,\n    iconClass,\n    message,\n    title,\n    hint,\n    timestamp,\n    ...options\n  }) {\n    const { vNode, destroy } = mount({\n      components: {\n        ProgressDialogComponent\n      },\n      provide: {\n        iconClass,\n        message,\n        title,\n        hint,\n        timestamp\n      },\n      data() {\n        return {\n          progressPerc,\n          progressText\n        };\n      },\n      template:\n        '<progress-dialog-component :progress-perc=\"progressPerc\" :progress-text=\"progressText\"></progress-dialog-component>'\n    });\n\n    component = vNode.componentInstance;\n    super({\n      element: vNode.el,\n      size: 'fit',\n      dismissible: false,\n      ...options\n    });\n\n    this.once('destroy', () => {\n      destroy();\n    });\n  }\n\n  updateProgress(progressPerc, progressText) {\n    component.$data.progressPerc = progressPerc;\n    component.$data.progressText = progressText;\n  }\n}\n\nexport default ProgressDialog;\n"
  },
  {
    "path": "src/api/overlays/Selection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport SelectionComponent from './components/SelectionComponent.vue';\nimport Overlay from './Overlay.js';\n\nclass Selection extends Overlay {\n  constructor({\n    iconClass,\n    title,\n    message,\n    selectionOptions,\n    onChange,\n    currentSelection,\n    ...options\n  }) {\n    const { vNode, destroy } = mount({\n      components: {\n        SelectionComponent: SelectionComponent\n      },\n      provide: {\n        iconClass,\n        title,\n        message,\n        selectionOptions,\n        onChange,\n        currentSelection\n      },\n      template: '<selection-component></selection-component>'\n    });\n\n    const component = vNode.componentInstance;\n\n    super({\n      element: component.$el,\n      size: 'fit',\n      dismissible: false,\n      onChange,\n      currentSelection,\n      ...options\n    });\n\n    this.once('destroy', () => {\n      destroy();\n    });\n  }\n}\n\nexport default Selection;\n"
  },
  {
    "path": "src/api/overlays/components/DialogComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-message\">\n    <!--Uses flex-row -->\n    <div class=\"c-message__icon\" :class=\"['u-icon-bg-color-' + iconClass]\"></div>\n    <div class=\"c-message__text\">\n      <!-- Uses flex-column -->\n      <div v-if=\"title\" class=\"c-message__title\">\n        {{ title }}\n      </div>\n\n      <div v-if=\"hint\" class=\"c-message__hint\">\n        {{ hint }}\n        <span v-if=\"timestamp\">[{{ timestamp }}]</span>\n      </div>\n\n      <div v-if=\"message\" class=\"c-message__action-text\">\n        {{ message }}\n      </div>\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['iconClass', 'title', 'hint', 'timestamp', 'message']\n};\n</script>\n"
  },
  {
    "path": "src/api/overlays/components/OverlayComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-overlay js-overlay\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Modal Overlay\">\n    <div class=\"c-overlay__blocker\" @click=\"destroy\"></div>\n    <div class=\"c-overlay__outer\">\n      <button\n        v-if=\"dismissible\"\n        aria-label=\"Close\"\n        class=\"c-click-icon c-overlay__close-button icon-x\"\n        @click.stop=\"destroy\"\n      ></button>\n      <div class=\"c-overlay__content-wrapper\">\n        <div\n          ref=\"element\"\n          class=\"c-overlay__contents js-notebook-snapshot-item-wrapper\"\n          tabindex=\"0\"\n        ></div>\n        <div v-if=\"buttons\" class=\"c-overlay__button-bar\">\n          <button\n            v-for=\"(button, index) in buttons\"\n            ref=\"buttons\"\n            :key=\"index\"\n            class=\"c-button js-overlay__button\"\n            tabindex=\"0\"\n            :class=\"{ 'c-button--major': focusIndex === index }\"\n            @focus=\"focusIndex = index\"\n            @click=\"buttonClickHandler(button.callback)\"\n          >\n            {{ button.label }}\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['dismiss', 'element', 'buttons', 'dismissible'],\n  emits: ['destroy'],\n  data() {\n    return {\n      focusIndex: -1\n    };\n  },\n  mounted() {\n    const element = this.$refs.element;\n    element.appendChild(this.element);\n    const elementForFocus = this.getElementForFocus() || element;\n    this.$nextTick(() => {\n      elementForFocus.focus();\n    });\n  },\n  methods: {\n    destroy() {\n      if (this.dismissible) {\n        this.dismiss();\n      }\n    },\n    buttonClickHandler(method) {\n      method();\n      this.$emit('destroy');\n    },\n    getElementForFocus() {\n      const defaultElement = this.$refs.element;\n      if (!this.$refs.buttons) {\n        return defaultElement;\n      }\n\n      const focusButton = this.$refs.buttons.filter((button, index) => {\n        if (this.buttons[index].emphasis) {\n          this.focusIndex = index;\n        }\n\n        return this.buttons[index].emphasis;\n      });\n\n      if (!focusButton.length) {\n        return defaultElement;\n      }\n\n      return focusButton[0];\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/overlays/components/ProgressDialogComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <DialogComponent>\n    <ProgressComponent :progress-perc=\"progressPerc\" :progress-text=\"progressText\" />\n  </DialogComponent>\n</template>\n\n<script>\nimport ProgressComponent from '../../../ui/components/ProgressBar.vue';\nimport DialogComponent from './DialogComponent.vue';\n\nexport default {\n  components: {\n    DialogComponent,\n    ProgressComponent\n  },\n  inject: ['iconClass', 'title', 'hint', 'timestamp', 'message'],\n  props: {\n    progressPerc: {\n      type: Number,\n      default: 0\n    },\n    progressText: {\n      type: String,\n      default: ''\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/overlays/components/SelectionComponent.vue",
    "content": "<template>\n  <div class=\"c-message\">\n    <!--Uses flex-row -->\n    <div class=\"c-message__icon\" :class=\"['u-icon-bg-color-' + iconClass]\"></div>\n    <div class=\"c-message__text\">\n      <!-- Uses flex-column -->\n      <div v-if=\"title\" class=\"c-message__title\">\n        {{ title }}\n      </div>\n\n      <div v-if=\"message\" class=\"c-message__action-text\">\n        {{ message }}\n      </div>\n      <select @change=\"onChange\">\n        <option\n          v-for=\"option in selectionOptions\"\n          :key=\"option.key\"\n          :value=\"option.key\"\n          :selected=\"option.key === currentSelection\"\n        >\n          {{ option.name }}\n        </option>\n      </select>\n\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']\n};\n</script>\n"
  },
  {
    "path": "src/api/overlays/components/dialog-component.scss",
    "content": "@mixin legacyMessage() {\n  flex: 0 1 auto;\n  font-family: symbolsfont;\n  font-size: $messageIconD; // Singleton message in a dialog\n  margin-right: $interiorMarginLg;\n}\n\n.c-message {\n  display: flex;\n  align-items: center;\n\n  > * + * {\n    margin-left: $interiorMarginLg;\n  }\n\n  &__icon {\n    // Holds a background SVG graphic\n    $s: 50px;\n    flex: 0 0 auto;\n    min-width: $s;\n    min-height: $s;\n  }\n\n  &__text {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n  }\n\n  // __text elements\n  &__action-text {\n    font-size: 1.2em;\n  }\n\n  &__title {\n    font-size: 1.5em;\n    font-weight: bold;\n  }\n\n  &--simple {\n    // Icon and text elements only\n    &:before {\n      font-size: 30px !important;\n    }\n\n    [class*='__text'] {\n      font-size: 1.25em;\n    }\n  }\n\n  /************************** LEGACY */\n  &.message-severity-info:before {\n    @include legacyMessage();\n    content: $glyph-icon-info;\n    color: $colorInfo;\n  }\n\n  &.message-severity-alert:before {\n    @include legacyMessage();\n    content: $glyph-icon-alert-rect;\n    color: $colorWarningLo;\n  }\n\n  &.message-severity-error:before {\n    @include legacyMessage();\n    content: $glyph-icon-alert-triangle;\n    color: $colorWarningHi;\n  }\n\n  // Messages in a list\n  .c-overlay__messages & {\n    padding: $interiorMarginLg;\n    &:before {\n      font-size: $messageListIconD;\n    }\n  }\n}\n"
  },
  {
    "path": "src/api/overlays/components/overlay-component.scss",
    "content": "@mixin overlaySizing($marginTB: auto, $marginLR: auto, $width: auto, $height: auto) {\n  position: absolute;\n  top: $marginTB;\n  right: $marginLR;\n  bottom: $marginTB;\n  left: $marginLR;\n  width: $width;\n  height: $height;\n}\n\n.l-overlay-wrapper {\n  // Created by overlayService.js, contains this template.\n  // Acts as an anchor for one or more overlays.\n  display: contents;\n}\n\n.c-overlay {\n  @include abs();\n  z-index: 70;\n\n  &__blocker {\n    // Mobile-first: use the blocker to create a full look to dialogs\n    @include abs();\n    background: $colorBodyBg;\n  }\n\n  &__outer {\n    @include abs();\n    background: $colorBodyBg;\n    display: flex;\n    flex-direction: column;\n\n    body.mobile .l-overlay-fit & {\n      // Vertically center small dialogs in mobile\n      top: 50%;\n      bottom: auto;\n      transform: translateY(-50%);\n    }\n  }\n\n  &__close-button {\n    $p: $interiorMargin + 2px;\n    font-size: 1.5em;\n    position: absolute;\n    top: $p;\n    right: $p;\n    z-index: 99;\n  }\n\n  &__content-wrapper {\n    display: flex;\n    height: 100%;\n    overflow: auto;\n    flex-direction: column;\n    gap: $interiorMargin;\n\n    body.desktop & {\n      overflow: hidden;\n    }\n\n    .l-overlay-fit &,\n    .l-overlay-dialog & {\n      margin: $overlayInnerMargin;\n    }\n  }\n\n  &__contents {\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n    outline: none;\n    overflow: auto;\n    body.mobile & {\n      flex: none;\n    }\n  }\n\n  &__top-bar {\n    flex: 0 0 auto;\n    flex-direction: column;\n    display: flex;\n\n    > * {\n      flex: 0 0 auto;\n      margin-bottom: $interiorMargin;\n    }\n  }\n\n  &__dialog-title {\n    @include ellipsize();\n    font-size: 1.5em;\n    line-height: 120%;\n  }\n\n  &__contents-main {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    height: 0; // Chrome 73 overflow bug fix\n    overflow: auto;\n    padding-right: $interiorMargin; // fend off scroll bar\n  }\n\n  &__button-bar {\n    flex: 0 0 auto;\n    display: flex;\n    justify-content: flex-end;\n    margin-top: $interiorMargin;\n    body.mobile & {\n      justify-content: flex-end;\n      padding-right: $interiorMargin;\n    }\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  .c-object-label__name {\n    color: $objectLabelNameColorFg;\n  }\n}\n\nbody.desktop {\n  .c-overlay {\n    &__blocker {\n      @include abs();\n      background: $colorOvrBlocker;\n      cursor: pointer;\n      display: block;\n    }\n  }\n\n  // Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.\n  .l-overlay-large,\n  .l-overlay-small,\n  .l-overlay-dialog,\n  .l-overlay-fit {\n    .c-overlay__outer {\n      border-radius: $overlayCr;\n      box-shadow: rgba(black, 0.5) 0 2px 25px;\n    }\n  }\n\n  .l-overlay-fullscreen {\n    // Used by About > Licenses display\n    .c-overlay__outer {\n      @include overlaySizing(\n        nth($overlayOuterMarginFullscreen, 1),\n        nth($overlayOuterMarginFullscreen, 2)\n      );\n    }\n  }\n\n  .l-overlay-large {\n    // Default\n    $pad: $interiorMarginLg;\n    $tbPad: floor($pad * 0.8);\n    $lrPad: $pad;\n    .c-overlay {\n      &__outer {\n        @include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2));\n        padding: $tbPad $lrPad;\n      }\n\n      &__close-button {\n        //top: $interiorMargin;\n        //right: $interiorMargin;\n      }\n    }\n\n    .l-browse-bar {\n      margin-right: 50px; // Don't cover close button\n      margin-bottom: $interiorMargin;\n    }\n  }\n\n  .l-overlay-small {\n    .c-overlay__outer {\n      @include overlaySizing(nth($overlayOuterMarginSmall, 1), nth($overlayOuterMarginSmall, 2));\n    }\n  }\n\n  .l-overlay-dialog {\n    .c-overlay__outer {\n      @include overlaySizing(nth($overlayOuterMarginDialog, 1), nth($overlayOuterMarginDialog, 2));\n    }\n  }\n\n  .t-dialog-sm .l-overlay-small, // Legacy dialog support\n    .l-overlay-fit {\n    .c-overlay__outer {\n      @include overlaySizing(auto, auto);\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%, -50%);\n      min-width: 20%;\n    }\n  }\n}\n"
  },
  {
    "path": "src/api/priority/PriorityAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst PRIORITIES = Object.freeze({\n  HIGHEST: Infinity,\n  HIGH: 1000,\n  DEFAULT: 0,\n  LOW: -1000,\n  LOWEST: -Infinity\n});\n\nexport default PRIORITIES;\n"
  },
  {
    "path": "src/api/status/StatusAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @typedef {import('openmct').OpenMCT} OpenMCT\n * @typedef {import('openmct').Identifier} Identifier\n * @typedef {string} Status\n */\n\nimport { EventEmitter } from 'eventemitter3';\n/**\n * Get, set, and observe statuses for Open MCT objects. A status is a string\n * that represents the current state of an object.\n *\n * @extends EventEmitter\n */\nexport default class StatusAPI extends EventEmitter {\n  /**\n   * Constructs a new instance of the StatusAPI class.\n   * @param {OpenMCT} openmct - The Open MCT application instance.\n   */\n  constructor(openmct) {\n    super();\n\n    this._openmct = openmct;\n    /** @type {Record<string, Status>} */\n    this._statusCache = {};\n\n    this.get = this.get.bind(this);\n    this.set = this.set.bind(this);\n    this.observe = this.observe.bind(this);\n  }\n\n  /**\n   * Retrieves the status of the object with the given identifier.\n   * @param {Identifier} identifier - The identifier of the object.\n   * @returns {Status | undefined} The status of the object, or undefined if the object's status is not cached.\n   */\n  get(identifier) {\n    let keyString = this._openmct.objects.makeKeyString(identifier);\n\n    return this._statusCache[keyString];\n  }\n\n  /**\n   * Sets the status of the object with the given identifier.\n   * @param {Identifier} identifier - The identifier of the object.\n   * @param {Status} status - The new status value for the object.\n   */\n  set(identifier, status) {\n    let keyString = this._openmct.objects.makeKeyString(identifier);\n\n    this._statusCache[keyString] = status;\n    this.emit(keyString, status);\n  }\n\n  /**\n   * Deletes the status of the object with the given identifier.\n   * @param {Identifier} identifier - The identifier of the object.\n   */\n  delete(identifier) {\n    let keyString = this._openmct.objects.makeKeyString(identifier);\n\n    this._statusCache[keyString] = undefined;\n    this.emit(keyString, undefined);\n    delete this._statusCache[keyString];\n  }\n\n  /**\n   * Observes the status of the object with the given identifier, and calls the provided callback\n   * function whenever the status changes.\n   * @param {Identifier} identifier - The identifier of the object.\n   * @param {(value: any) => void} callback - The function to be called whenever the status changes.\n   * @returns {() => void} A function that can be called to stop observing the status.\n   */\n  observe(identifier, callback) {\n    let key = this._openmct.objects.makeKeyString(identifier);\n\n    this.on(key, callback);\n\n    return () => {\n      this.off(key, callback);\n    };\n  }\n}\n"
  },
  {
    "path": "src/api/status/StatusAPISpec.js",
    "content": "import { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport StatusAPI from './StatusAPI.js';\n\ndescribe('The Status API', () => {\n  let statusAPI;\n  let openmct;\n  let identifier;\n  let status;\n  let status2;\n  let callback;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n    statusAPI = new StatusAPI(openmct);\n    identifier = {\n      namespace: 'test-namespace',\n      key: 'test-key'\n    };\n    status = 'test-status';\n    status2 = 'test-status-deux';\n    callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('set function', () => {\n    it('sets status for identifier', () => {\n      statusAPI.set(identifier, status);\n\n      let resultingStatus = statusAPI.get(identifier);\n\n      expect(resultingStatus).toEqual(status);\n    });\n  });\n\n  describe('get function', () => {\n    it('returns status for identifier', () => {\n      statusAPI.set(identifier, status2);\n\n      let resultingStatus = statusAPI.get(identifier);\n\n      expect(resultingStatus).toEqual(status2);\n    });\n  });\n\n  describe('delete function', () => {\n    it('deletes status for identifier', () => {\n      statusAPI.set(identifier, status);\n\n      let resultingStatus = statusAPI.get(identifier);\n      expect(resultingStatus).toEqual(status);\n\n      statusAPI.delete(identifier);\n      resultingStatus = statusAPI.get(identifier);\n\n      expect(resultingStatus).toBeUndefined();\n    });\n  });\n\n  describe('observe function', () => {\n    it('allows callbacks to be attached to status set and delete events', () => {\n      let unsubscribe = statusAPI.observe(identifier, callback);\n      statusAPI.set(identifier, status);\n\n      expect(callback).toHaveBeenCalledWith(status);\n\n      statusAPI.delete(identifier);\n\n      expect(callback).toHaveBeenCalledWith(undefined);\n      unsubscribe();\n    });\n\n    it('returns a unsubscribe function', () => {\n      let unsubscribe = statusAPI.observe(identifier, callback);\n      unsubscribe();\n\n      statusAPI.set(identifier, status);\n\n      expect(callback).toHaveBeenCalledTimes(0);\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/telemetry/BatchingWebSocket.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport installWorker from './WebSocketWorker.js';\n\n/**\n * @typedef RequestIdleCallbackOptions\n * @prop {Number} timeout If the number of milliseconds represented by this\n * parameter has elapsed and the callback has not already been called, invoke\n *  the callback.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback\n */\n\n/**\n * Mocks requestIdleCallback for Safari using setTimeout. Functionality will be\n * identical to setTimeout in Safari, which is to fire the callback function\n * after the provided timeout period.\n *\n * In browsers that support requestIdleCallback, this const is just a\n * pointer to the native function.\n *\n * @param {Function} callback a callback to be invoked during the next idle period, or\n * after the specified timeout\n * @param {RequestIdleCallbackOptions} options\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback\n *\n */\nfunction requestIdleCallbackPolyfill(callback, options) {\n  return (\n    // eslint-disable-next-line compat/compat\n    window.requestIdleCallback ??\n    ((fn, { timeout }) =>\n      setTimeout(() => {\n        fn({ didTimeout: false });\n      }, timeout))\n  );\n}\nconst requestIdleCallback = requestIdleCallbackPolyfill();\n\nconst ONE_SECOND = 1000;\n\n/**\n * Provides a WebSocket abstraction layer that handles a lot of boilerplate common\n * to managing WebSocket connections such as:\n * - Establishing a WebSocket connection to a server\n * - Reconnecting on error, with a fallback strategy\n * - Queuing messages so that clients can send messages without concern for the current\n * connection state of the WebSocket.\n *\n * The WebSocket that it manages is based in a dedicated worker so that network\n * concerns are not handled on the main event loop. This allows for performant receipt\n * and batching of messages without blocking either the UI or server.\n *\n */\nclass BatchingWebSocket extends EventTarget {\n  #worker;\n  #openmct;\n  #showingRateLimitNotification;\n  #maxBufferSize;\n  #throttleRate;\n  #firstBatchReceived;\n  #lastBatchReceived;\n  #peakBufferSize = Number.NEGATIVE_INFINITY;\n\n  /**\n   * @param {import('openmct.js').OpenMCT} openmct\n   */\n  constructor(openmct) {\n    super();\n    // Install worker, register listeners etc.\n    const workerFunction = `(${installWorker.toString()})()`;\n    const workerBlob = new Blob([workerFunction]);\n    const workerUrl = URL.createObjectURL(workerBlob, { type: 'application/javascript' });\n    this.#worker = new Worker(workerUrl);\n    this.#openmct = openmct;\n    this.#showingRateLimitNotification = false;\n    this.#maxBufferSize = Number.POSITIVE_INFINITY;\n    this.#throttleRate = ONE_SECOND;\n    this.#firstBatchReceived = false;\n\n    const routeMessageToHandler = this.#routeMessageToHandler.bind(this);\n    this.#worker.addEventListener('message', routeMessageToHandler);\n    openmct.on(\n      'destroy',\n      () => {\n        this.disconnect();\n        URL.revokeObjectURL(workerUrl);\n      },\n      { once: true }\n    );\n  }\n\n  /**\n   * Will establish a WebSocket connection to the provided url\n   * @param {string} url The URL to connect to\n   */\n  connect(url) {\n    this.#worker.postMessage({\n      type: 'connect',\n      url\n    });\n\n    this.#readyForNextBatch();\n  }\n\n  #readyForNextBatch() {\n    this.#worker.postMessage({\n      type: 'readyForNextBatch'\n    });\n  }\n\n  /**\n   * Send a message to the WebSocket.\n   * @param {any} message The message to send. Can be any type supported by WebSockets.\n   * See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#data\n   */\n  sendMessage(message) {\n    this.#worker.postMessage({\n      type: 'message',\n      message\n    });\n  }\n\n  /**\n   * @param {number} maxBufferSize the maximum length of the receive buffer in characters.\n   * Note that this is a fail-safe that is only invoked if performance drops to the\n   * point where Open MCT cannot keep up with the amount of telemetry it is receiving.\n   * In this event it will sacrifice the oldest telemetry in the batch in favor of the\n   * most recent telemetry. The user will be informed that telemetry has been dropped.\n   *\n   * This should be set appropriately for the expected data rate. eg. If typical usage\n   * sees 2000 messages arriving at a client per second, with an average message size\n   * of 500 bytes, then 2000 * 500 = 1000000 characters will be right on the limit.\n   * In this scenario, a buffer size of 1500000 character might be more appropriate\n   * to allow some overhead for bursty telemetry, and temporary UI load during page\n   * load.\n   *\n   * The PerformanceIndicator plugin (openmct.plugins.PerformanceIndicator) gives\n   * statistics on buffer utilization. It can be used to scale the buffer appropriately.\n   */\n  setMaxBufferSize(maxBatchSize) {\n    this.#maxBufferSize = maxBatchSize;\n    this.#sendMaxBufferSizeToWorker(this.#maxBufferSize);\n  }\n  setThrottleRate(throttleRate) {\n    this.#throttleRate = throttleRate;\n    this.#sendThrottleRateToWorker(this.#throttleRate);\n  }\n  setThrottleMessagePattern(throttleMessagePattern) {\n    this.#worker.postMessage({\n      type: 'setThrottleMessagePattern',\n      throttleMessagePattern\n    });\n  }\n\n  #sendMaxBufferSizeToWorker(maxBufferSize) {\n    this.#worker.postMessage({\n      type: 'setMaxBufferSize',\n      maxBufferSize\n    });\n  }\n\n  #sendThrottleRateToWorker(throttleRate) {\n    this.#worker.postMessage({\n      type: 'setThrottleRate',\n      throttleRate\n    });\n  }\n\n  /**\n   * Disconnect the associated WebSocket. Generally speaking there is no need to call\n   * this manually.\n   */\n  disconnect() {\n    this.#worker.postMessage({\n      type: 'disconnect'\n    });\n  }\n\n  #routeMessageToHandler(message) {\n    if (message.data.type === 'batch') {\n      const batch = message.data.batch;\n      const now = performance.now();\n\n      let currentBufferLength = message.data.currentBufferLength;\n      let maxBufferSize = message.data.maxBufferSize;\n      let parameterCount = batch.length;\n      if (this.#peakBufferSize < currentBufferLength) {\n        this.#peakBufferSize = currentBufferLength;\n      }\n\n      if (this.#openmct.performance !== undefined) {\n        if (!isNaN(this.#lastBatchReceived)) {\n          const elapsed = (now - this.#lastBatchReceived) / 1000;\n          this.#lastBatchReceived = now;\n          this.#openmct.performance.measurements.set(\n            'Parameters/s',\n            Math.floor(parameterCount / elapsed)\n          );\n        }\n        this.#openmct.performance.measurements.set(\n          'Buff. Util. (bytes)',\n          `${currentBufferLength} / ${maxBufferSize}`\n        );\n        this.#openmct.performance.measurements.set(\n          'Peak Buff. Util. (bytes)',\n          `${this.#peakBufferSize} / ${maxBufferSize}`\n        );\n      }\n\n      this.start = Date.now();\n      const dropped = message.data.dropped;\n      if (dropped === true && !this.#showingRateLimitNotification) {\n        const notification = this.#openmct.notifications.alert(\n          'Telemetry dropped due to client rate limiting.',\n          { hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }\n        );\n        this.#showingRateLimitNotification = true;\n        notification.once('minimized', () => {\n          this.#showingRateLimitNotification = false;\n        });\n      }\n\n      this.dispatchEvent(new CustomEvent('batch', { detail: batch }));\n      this.#waitUntilIdleAndRequestNextBatch(batch);\n    } else if (message.data.type === 'message') {\n      this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));\n    } else if (message.data.type === 'reconnected') {\n      this.dispatchEvent(new CustomEvent('reconnected'));\n    } else {\n      throw new Error(`Unknown message type: ${message.data.type}`);\n    }\n  }\n\n  #waitUntilIdleAndRequestNextBatch(batch) {\n    requestIdleCallback(\n      (state) => {\n        if (this.#firstBatchReceived === false) {\n          this.#firstBatchReceived = true;\n        }\n        const now = Date.now();\n        const waitedFor = now - this.start;\n        if (state.didTimeout === true) {\n          if (document.visibilityState === 'visible') {\n            console.warn(`Event loop is too busy to process batch.`);\n            this.#waitUntilIdleAndRequestNextBatch(batch);\n          } else {\n            this.#readyForNextBatch();\n          }\n        } else {\n          if (waitedFor > this.#throttleRate) {\n            console.warn(`Warning, batch processing took ${waitedFor}ms`);\n          }\n          this.#readyForNextBatch();\n        }\n      },\n      { timeout: this.#throttleRate }\n    );\n  }\n}\n\nexport default BatchingWebSocket;\n"
  },
  {
    "path": "src/api/telemetry/DefaultMetadataProvider.js",
    "content": "/*****************************************************************************\n * Open openmct, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open openmct is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open openmct includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\n\n/**\n * This is the default metadata provider; for any object with a \"telemetry\"\n * property, this provider will return the value of that property as the\n * telemetry metadata.\n *\n * This provider also implements legacy support for telemetry metadata\n * defined on the type.  Telemetry metadata definitions on type will be\n * depreciated in the future.\n */\nexport default class DefaultMetadataProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  /**\n   * Applies to any domain object with a telemetry property, or whose type\n   * definition has a telemetry property.\n   */\n  supportsMetadata(domainObject) {\n    return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject));\n  }\n\n  /**\n   * Returns telemetry metadata for a given domain object.\n   */\n  getMetadata(domainObject) {\n    const metadata = domainObject.telemetry || {};\n    if (this.typeHasTelemetry(domainObject)) {\n      const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;\n\n      Object.assign(metadata, typeMetadata);\n\n      if (!metadata.values) {\n        metadata.values = valueMetadatasFromOldFormat(metadata);\n      }\n    }\n\n    return metadata;\n  }\n\n  /**\n   * @private\n   */\n  typeHasTelemetry(domainObject) {\n    const type = this.openmct.types.get(domainObject.type);\n\n    return Boolean(type.definition.telemetry);\n  }\n}\n\n/**\n * Retrieves valueMetadata from legacy metadata.\n * @private\n */\nfunction valueMetadatasFromOldFormat(metadata) {\n  const valueMetadatas = [];\n\n  valueMetadatas.push({\n    key: 'name',\n    name: 'Name'\n  });\n\n  metadata.domains.forEach(function (domain, index) {\n    const valueMetadata = _.clone(domain);\n    valueMetadata.hints = {\n      domain: index + 1\n    };\n    valueMetadatas.push(valueMetadata);\n  });\n\n  metadata.ranges.forEach(function (range, index) {\n    const valueMetadata = _.clone(range);\n    valueMetadata.hints = {\n      range: index,\n      priority: index + metadata.domains.length + 1\n    };\n\n    if (valueMetadata.type === 'enum') {\n      valueMetadata.key = 'enum';\n      valueMetadata.hints.y -= 10;\n      valueMetadata.hints.range -= 10;\n      valueMetadata.enumerations = _.sortBy(\n        valueMetadata.enumerations.map(function (e) {\n          return {\n            string: e.string,\n            value: Number(e.value)\n          };\n        }),\n        'e.value'\n      );\n      valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);\n      valueMetadata.max = Math.max(valueMetadata.values);\n      valueMetadata.min = Math.min(valueMetadata.values);\n    }\n\n    valueMetadatas.push(valueMetadata);\n  });\n\n  return valueMetadatas;\n}\n"
  },
  {
    "path": "src/api/telemetry/TelemetryAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open openmct is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open openmct includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { makeKeyString } from 'objectUtils';\n\nimport CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';\nimport BatchingWebSocket from './BatchingWebSocket.js';\nimport DefaultMetadataProvider from './DefaultMetadataProvider.js';\nimport TelemetryCollection from './TelemetryCollection.js';\nimport TelemetryMetadataManager from './TelemetryMetadataManager.js';\nimport TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor.js';\nimport TelemetryValueFormatter from './TelemetryValueFormatter.js';\n\n/**\n * @typedef {import('../time/TimeContext').TimeContext} TimeContext\n */\n\n/**\n * Describes and bounds requests for telemetry data.\n *\n * @typedef TelemetryRequestOptions\n * @property {string} [sort] the key of the property to sort by. This may\n *           be prefixed with a \"+\" or a \"-\" sign to sort in ascending\n *           or descending order respectively. If no prefix is present,\n *           ascending order will be used.\n * @property {number} [start] the lower bound for values of the sorting property\n * @property {number} [end] the upper bound for values of the sorting property\n * @property {string} [strategy] symbolic identifier for strategies\n *           (such as `latest` or `minmax`) which may be recognized by providers;\n *           these will be tried in order until an appropriate provider\n *           is found\n * @property {AbortController} [signal] an AbortController which can be used\n *           to cancel a telemetry request\n * @property {string} [domain] the domain key of the request\n * @property {TimeContext} [timeContext] the time context to use for this request\n */\n\n/**\n * Describes and bounds requests for telemetry data.\n *\n * @typedef TelemetrySubscriptionOptions\n * @property {string} [strategy] symbolic identifier directing providers on how\n * to handle telemetry subscriptions. The default behavior is 'latest' which will\n * always return a single telemetry value with each callback, and in the event\n * of throttling will always prioritize the latest data, meaning intermediate\n * data will be skipped. Alternatively, the `batch` strategy can be used, which\n * will return all telemetry values since the last callback. This strategy is\n * useful for cases where intermediate data is important, such as when\n * rendering a telemetry plot or table. If `batch` is specified, the subscription\n * callback will be invoked with an Array.\n *\n */\n\nconst SUBSCRIBE_STRATEGY = {\n  LATEST: 'latest',\n  BATCH: 'batch'\n};\n\n/**\n * Utilities for telemetry\n * @interface TelemetryAPI\n */\nexport default class TelemetryAPI {\n  #isGreedyLAD;\n  #subscribeCache;\n  #hasReturnedFirstData;\n\n  get SUBSCRIBE_STRATEGY() {\n    return SUBSCRIBE_STRATEGY;\n  }\n\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.formatMapCache = new WeakMap();\n    this.formatters = new Map();\n    this.limitProviders = [];\n    this.stalenessProviders = [];\n    this.metadataCache = new WeakMap();\n    this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];\n    this.noRequestProviderForAllObjects = false;\n    this.requestAbortControllers = new Set();\n    this.requestProviders = [];\n    this.subscriptionProviders = [];\n    this.valueFormatterCache = new WeakMap();\n    this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();\n    this.#isGreedyLAD = true;\n    this.BatchingWebSocket = BatchingWebSocket;\n    this.#subscribeCache = {};\n    this.#hasReturnedFirstData = false;\n  }\n\n  abortAllRequests() {\n    this.requestAbortControllers.forEach((controller) => controller.abort());\n    this.requestAbortControllers.clear();\n  }\n\n  /**\n   * Return Custom String Formatter\n   *\n   * @param {Object} valueMetadata valueMetadata for given telemetry object\n   * @param {string} format custom formatter string (eg: %.4f, &lts etc.)\n   * @returns {CustomStringFormatter}\n   */\n  customStringFormatter(valueMetadata, format) {\n    return new CustomStringFormatter(this.openmct, valueMetadata, format);\n  }\n\n  /**\n   * Return true if the given domainObject is a telemetry object.  A telemetry\n   * object is any object which has telemetry metadata-- regardless of whether\n   * the telemetry object has an available telemetry provider.\n   *\n   * @param {import('openmct').DomainObject} domainObject\n   * @returns {boolean} true if the object is a telemetry object.\n   */\n  isTelemetryObject(domainObject) {\n    return Boolean(this.#findMetadataProvider(domainObject));\n  }\n\n  /**\n   * Check if this provider can supply telemetry data associated with\n   * this domain object.\n   *\n   * @method canProvideTelemetry\n   * @param {import('openmct').DomainObject} domainObject the object for\n   *        which telemetry would be provided\n   * @returns {boolean} true if telemetry can be provided\n   */\n  canProvideTelemetry(domainObject) {\n    return (\n      Boolean(this.findSubscriptionProvider(domainObject)) ||\n      Boolean(this.findRequestProvider(domainObject))\n    );\n  }\n\n  /**\n   * Register a telemetry provider with the telemetry service. This\n   * allows you to connect alternative telemetry sources.\n   * @method addProvider\n   * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new\n   *        telemetry provider\n   */\n  addProvider(provider) {\n    if (provider.supportsRequest) {\n      this.requestProviders.unshift(provider);\n    }\n\n    if (provider.supportsSubscribe) {\n      this.subscriptionProviders.unshift(provider);\n    }\n\n    if (provider.supportsMetadata) {\n      this.metadataProviders.unshift(provider);\n    }\n\n    if (provider.supportsLimits) {\n      this.limitProviders.unshift(provider);\n    }\n\n    if (provider.supportsStaleness) {\n      this.stalenessProviders.unshift(provider);\n    }\n  }\n\n  /**\n   * Returns a telemetry subscription provider that supports\n   * a given domain object and options.\n   */\n  findSubscriptionProvider() {\n    const args = Array.prototype.slice.apply(arguments);\n    function supportsDomainObject(provider) {\n      return provider.supportsSubscribe.apply(provider, args);\n    }\n\n    return this.subscriptionProviders.find(supportsDomainObject);\n  }\n\n  /**\n   * Returns a telemetry request provider that supports\n   * a given domain object and options.\n   */\n  findRequestProvider() {\n    const args = Array.prototype.slice.apply(arguments);\n    function supportsDomainObject(provider) {\n      return provider.supportsRequest.apply(provider, args);\n    }\n\n    return this.requestProviders.find(supportsDomainObject);\n  }\n\n  /**\n   * @private\n   */\n  #findMetadataProvider(domainObject) {\n    return this.metadataProviders.find((provider) => {\n      return provider.supportsMetadata(domainObject);\n    });\n  }\n\n  /**\n   * @private\n   */\n  #findLimitEvaluator(domainObject) {\n    return this.limitProviders.find((provider) => {\n      return provider.supportsLimits(domainObject);\n    });\n  }\n\n  /**\n   * @param {TelemetryRequestOptions} options options for the telemetry request\n   * @returns {TelemetryRequestOptions} the options, with defaults filled in\n   */\n  standardizeRequestOptions(options = {}) {\n    if (!Object.hasOwn(options, 'timeContext')) {\n      options.timeContext = this.openmct.time;\n    }\n\n    if (!Object.hasOwn(options, 'domain')) {\n      options.domain = options.timeContext.getTimeSystem().key;\n    }\n\n    // if no specific start/end bounds are passed, use the timeContext bounds\n    if (!Object.hasOwn(options, 'start')) {\n      options.start = options.timeContext.getBounds().start;\n    }\n\n    if (!Object.hasOwn(options, 'end')) {\n      options.end = options.timeContext.getBounds().end;\n    }\n\n    return options;\n  }\n\n  /**\n   * Sanitizes objects for consistent serialization by:\n   * 1. Removing non-plain objects (class instances) and functions\n   * 2. Sorting object keys alphabetically to ensure consistent ordering\n   */\n  sanitizeForSerialization(key, value) {\n    // Handle null and primitives directly\n    if (value === null || typeof value !== 'object') {\n      return value;\n    }\n\n    // Remove functions and non-plain objects (except arrays)\n    if (\n      typeof value === 'function' ||\n      (Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))\n    ) {\n      return undefined;\n    }\n\n    // For plain objects, just sort the keys\n    if (!Array.isArray(value)) {\n      const sortedObject = {};\n      const sortedKeys = Object.keys(value).sort();\n\n      sortedKeys.forEach((objectKey) => {\n        sortedObject[objectKey] = value[objectKey];\n      });\n\n      return sortedObject;\n    }\n\n    return value;\n  }\n\n  /**\n   * Determines whether a domain object has numeric telemetry data.\n   * A domain object has numeric telemetry if it:\n   * 1. Has a telemetry property\n   * 2. Has telemetry metadata with domain values (like timestamps)\n   * 3. Has range values (measurements) where at least one is numeric\n   *\n   * @method hasNumericTelemetry\n   * @param {import('openmct').DomainObject} domainObject The domain object to check\n   * @returns {boolean} True if the object has numeric telemetry, false otherwise\n   */\n  hasNumericTelemetry(domainObject) {\n    const hasTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);\n\n    if (!hasTelemetry) {\n      return false;\n    }\n\n    const metadata = this.openmct.telemetry.getMetadata(domainObject);\n\n    if (!metadata) {\n      return false;\n    }\n\n    const rangeValues = metadata.valuesForHints(['range']);\n    const domains = metadata.valuesForHints(['domain']);\n\n    return (\n      domains.length > 0 &&\n      rangeValues.length > 0 &&\n      !rangeValues.every((value) => value.format === 'string')\n    );\n  }\n\n  /**\n   * Generates a numeric hash value for an options object. The hash is consistent\n   * for equivalent option objects regardless of property order.\n   *\n   * This is used to create compact, unique cache keys for telemetry subscriptions with\n   * different options configurations. The hash function ensures that identical options\n   * objects will always generate the same hash value, while different options objects\n   * (even with small differences) will generate different hash values.\n   *\n   * @private\n   * @param {Object} options The options object to hash\n   * @returns {number} A positive integer hash of the options object\n   */\n  #hashOptions(options) {\n    const sanitizedOptionsString = JSON.stringify(\n      options,\n      this.sanitizeForSerialization.bind(this)\n    );\n\n    let hash = 0;\n    const prime = 31;\n    const modulus = 1e9 + 9; // Large prime number\n\n    for (let i = 0; i < sanitizedOptionsString.length; i++) {\n      const char = sanitizedOptionsString.charCodeAt(i);\n      // Calculate new hash value while keeping numbers manageable\n      hash = Math.floor((hash * prime + char) % modulus);\n    }\n\n    return Math.abs(hash);\n  }\n\n  /**\n   * Generates a unique cache key for a telemetry subscription based on the\n   * domain object identifier and options (which includes strategy).\n   *\n   * Uses a hash of the options object to create compact cache keys while still\n   * ensuring unique keys for different subscription configurations.\n   *\n   * @private\n   * @param {import('openmct').DomainObject} domainObject The domain object being subscribed to\n   * @param {Object} options The subscription options object (including strategy)\n   * @returns {string} A unique key string for caching the subscription\n   */\n  #getSubscriptionCacheKey(domainObject, options) {\n    const keyString = makeKeyString(domainObject.identifier);\n\n    return `${keyString}:${this.#hashOptions(options)}`;\n  }\n\n  /**\n   * Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request\n   * The request will be modified when it is received and will be returned in it's modified state\n   * The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef\n   *\n   * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add\n   * @method addRequestInterceptor\n   */\n  addRequestInterceptor(requestInterceptorDef) {\n    this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);\n  }\n\n  /**\n   * Retrieve the request interceptors for a given domain object.\n   * @private\n   */\n  #getInterceptorsForRequest(identifier, request) {\n    return this.requestInterceptorRegistry.getInterceptors(identifier, request);\n  }\n\n  /**\n   * Invoke interceptors if applicable for a given domain object.\n   */\n  async applyRequestInterceptors(domainObject, request) {\n    const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);\n\n    if (interceptors.length === 0) {\n      return request;\n    }\n\n    let modifiedRequest = { ...request };\n\n    for (let interceptor of interceptors) {\n      modifiedRequest = await interceptor.invoke(modifiedRequest);\n    }\n\n    return modifiedRequest;\n  }\n\n  /**\n   * Get or set greedy LAD. For strategy \"latest\" telemetry in\n   * realtime mode the start bound will be ignored if true and\n   * there is no new data to replace the existing data.\n   * defaults to true\n   *\n   * To turn off greedy LAD:\n   * openmct.telemetry.greedyLAD(false);\n   *\n   * @method greedyLAD\n   * @returns {boolean} if greedyLAD is active or not\n   */\n  greedyLAD(isGreedy) {\n    if (arguments.length > 0) {\n      if (isGreedy !== true && isGreedy !== false) {\n        throw new Error('Error setting greedyLAD. Greedy LAD only accepts true or false values');\n      }\n\n      this.#isGreedyLAD = isGreedy;\n    }\n\n    return this.#isGreedyLAD;\n  }\n\n  /**\n   * Request telemetry collection for a domain object.\n   * The `options` argument allows you to specify filters\n   * (start, end, etc.), sort order, and strategies for retrieving\n   * telemetry (aggregation, latest available, etc.).\n   *\n   * @method requestCollection\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated telemetry\n   * @param {TelemetryRequestOptions} options\n   *        options for this telemetry collection request\n   * @returns {TelemetryCollection} a TelemetryCollection instance\n   */\n  requestCollection(domainObject, options = {}) {\n    return new TelemetryCollection(this.openmct, domainObject, options);\n  }\n\n  /**\n   * Request historical telemetry for a domain object.\n   * The `options` argument allows you to specify filters\n   * (start, end, etc.), sort order, time context, and strategies for retrieving\n   * telemetry (aggregation, latest available, etc.).\n   *\n   * @method request\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated telemetry\n   * @param {TelemetryRequestOptions} options\n   *        options for this historical request\n   * @returns {Promise.<object[]>} a promise for an array of\n   *          telemetry data\n   */\n  async request(domainObject) {\n    if (this.noRequestProviderForAllObjects || domainObject.type === 'unknown') {\n      return [];\n    }\n\n    if (arguments.length === 1) {\n      arguments.length = 2;\n      arguments[1] = {};\n    }\n\n    const abortController = new AbortController();\n    arguments[1].signal = abortController.signal;\n    this.requestAbortControllers.add(abortController);\n\n    this.standardizeRequestOptions(arguments[1]);\n\n    const provider = this.findRequestProvider.apply(this, arguments);\n    if (!provider) {\n      this.requestAbortControllers.delete(abortController);\n\n      return this.#handleMissingRequestProvider(domainObject);\n    }\n\n    arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);\n    try {\n      const telemetry = await provider.request(...arguments);\n      if (!this.#hasReturnedFirstData) {\n        this.#hasReturnedFirstData = true;\n        performance.mark('firstHistoricalDataReturned');\n      }\n      return telemetry;\n    } catch (error) {\n      if (error.name !== 'AbortError') {\n        this.openmct.notifications.error(\n          'Error requesting telemetry data, see console for details'\n        );\n        console.error(error);\n      }\n\n      throw new Error(error);\n    } finally {\n      this.requestAbortControllers.delete(abortController);\n    }\n  }\n\n  /**\n   * Subscribe to realtime telemetry for a specific domain object.\n   * The callback will be called whenever data is received from a\n   * realtime provider.\n   *\n   * @method subscribe\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated telemetry\n   * @param {TelemetrySubscriptionOptions} options configuration items for subscription\n   * @param {Function} callback the callback to invoke with new data, as\n   *        it becomes available\n   * @returns {Function} a function which may be called to terminate\n   *          the subscription\n   */\n  subscribe(domainObject, callback, options = { strategy: SUBSCRIBE_STRATEGY.LATEST }) {\n    const requestedStrategy = options.strategy || SUBSCRIBE_STRATEGY.LATEST;\n\n    if (domainObject.type === 'unknown') {\n      return () => {};\n    }\n\n    const provider = this.findSubscriptionProvider(domainObject, options);\n    const supportsBatching =\n      Boolean(provider?.supportsBatching) && provider?.supportsBatching(domainObject, options);\n\n    if (!this.#subscribeCache) {\n      this.#subscribeCache = {};\n    }\n\n    const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;\n    // Override the requested strategy with the strategy supported by the provider\n    const optionsWithSupportedStrategy = {\n      ...options,\n      strategy: supportedStrategy\n    };\n\n    const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);\n    let subscriber = this.#subscribeCache[cacheKey];\n\n    if (!subscriber) {\n      subscriber = this.#subscribeCache[cacheKey] = {\n        latestCallbacks: [],\n        batchCallbacks: []\n      };\n      if (provider) {\n        subscriber.unsubscribe = provider.subscribe(\n          domainObject,\n          invokeCallbackWithRequestedStrategy,\n          optionsWithSupportedStrategy\n        );\n      } else {\n        subscriber.unsubscribe = function () {};\n      }\n    }\n\n    if (requestedStrategy === SUBSCRIBE_STRATEGY.BATCH) {\n      subscriber.batchCallbacks.push(callback);\n    } else {\n      subscriber.latestCallbacks.push(callback);\n    }\n\n    // Guarantees that view receive telemetry in the expected form\n    function invokeCallbackWithRequestedStrategy(data) {\n      invokeCallbacksWithArray(data, subscriber.batchCallbacks);\n      invokeCallbacksWithSingleValue(data, subscriber.latestCallbacks);\n    }\n\n    function invokeCallbacksWithArray(data, batchCallbacks) {\n      //\n      if (data === undefined || data === null || data.length === 0) {\n        throw new Error(\n          'Attempt to invoke telemetry subscription callback with no telemetry datum'\n        );\n      }\n\n      if (!Array.isArray(data)) {\n        data = [data];\n      }\n\n      batchCallbacks.forEach((cb) => {\n        cb(data);\n      });\n    }\n\n    function invokeCallbacksWithSingleValue(data, latestCallbacks) {\n      if (Array.isArray(data)) {\n        data = data[data.length - 1];\n      }\n\n      if (data === undefined || data === null) {\n        throw new Error(\n          'Attempt to invoke telemetry subscription callback with no telemetry datum'\n        );\n      }\n\n      latestCallbacks.forEach((cb) => {\n        cb(data);\n      });\n    }\n\n    return function unsubscribe() {\n      subscriber.latestCallbacks = subscriber.latestCallbacks.filter(function (cb) {\n        return cb !== callback;\n      });\n      subscriber.batchCallbacks = subscriber.batchCallbacks.filter(function (cb) {\n        return cb !== callback;\n      });\n\n      if (subscriber.latestCallbacks.length === 0 && subscriber.batchCallbacks.length === 0) {\n        subscriber.unsubscribe();\n        delete this.#subscribeCache[cacheKey];\n      }\n    }.bind(this);\n  }\n\n  /**\n   * Subscribe to staleness updates for a specific domain object.\n   * The callback will be called whenever staleness changes.\n   *\n   * @method subscribeToStaleness\n   * @param {import('openmct').DomainObject} domainObject the object\n   *          to watch for staleness updates\n   * @param {Function} callback the callback to invoke with staleness data,\n   *  as it is received: ex.\n   *  {\n   *      isStale: <Boolean>,\n   *      timestamp: <timestamp>\n   *  }\n   * @returns {Function} a function which may be called to terminate\n   *          the subscription to staleness updates\n   */\n  subscribeToStaleness(domainObject, callback) {\n    const provider = this.#findStalenessProvider(domainObject);\n\n    if (!this.stalenessSubscriberCache) {\n      this.stalenessSubscriberCache = {};\n    }\n\n    const keyString = makeKeyString(domainObject.identifier);\n    let stalenessSubscriber = this.stalenessSubscriberCache[keyString];\n\n    if (!stalenessSubscriber) {\n      stalenessSubscriber = this.stalenessSubscriberCache[keyString] = {\n        callbacks: [callback]\n      };\n      if (provider) {\n        stalenessSubscriber.unsubscribe = provider.subscribeToStaleness(\n          domainObject,\n          (stalenessResponse) => {\n            stalenessSubscriber.callbacks.forEach((cb) => {\n              cb(stalenessResponse);\n            });\n          }\n        );\n      } else {\n        stalenessSubscriber.unsubscribe = () => {};\n      }\n    } else {\n      stalenessSubscriber.callbacks.push(callback);\n    }\n\n    return function unsubscribe() {\n      stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => {\n        return cb !== callback;\n      });\n      if (stalenessSubscriber.callbacks.length === 0) {\n        stalenessSubscriber.unsubscribe();\n        delete this.stalenessSubscriberCache[keyString];\n      }\n    }.bind(this);\n  }\n\n  /**\n   * Subscribe to run-time changes in configured telemetry limits for a specific domain object.\n   * The callback will be called whenever data is received from a\n   * limit provider.\n   *\n   * @method subscribeToLimits\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated limits\n   * @param {Function} callback the callback to invoke with new data, as\n   *        it becomes available\n   * @returns {Function} a function which may be called to terminate\n   *          the subscription\n   */\n  subscribeToLimits(domainObject, callback) {\n    if (domainObject.type === 'unknown') {\n      return () => {};\n    }\n\n    const provider = this.#findLimitEvaluator(domainObject);\n\n    if (!this.limitsSubscribeCache) {\n      this.limitsSubscribeCache = {};\n    }\n\n    const keyString = makeKeyString(domainObject.identifier);\n    let subscriber = this.limitsSubscribeCache[keyString];\n\n    if (!subscriber) {\n      subscriber = this.limitsSubscribeCache[keyString] = {\n        callbacks: [callback]\n      };\n      if (provider && provider.subscribeToLimits) {\n        subscriber.unsubscribe = provider.subscribeToLimits(domainObject, function (value) {\n          subscriber.callbacks.forEach(function (cb) {\n            cb(value);\n          });\n        });\n      } else {\n        subscriber.unsubscribe = function () {};\n      }\n    } else {\n      subscriber.callbacks.push(callback);\n    }\n\n    return function unsubscribe() {\n      subscriber.callbacks = subscriber.callbacks.filter(function (cb) {\n        return cb !== callback;\n      });\n      if (subscriber.callbacks.length === 0) {\n        subscriber.unsubscribe();\n        delete this.limitsSubscribeCache[keyString];\n      }\n    }.bind(this);\n  }\n\n  /**\n   * Request telemetry staleness for a domain object.\n   *\n   * @method isStale\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated telemetry staleness\n   * @returns {Promise.<StalenessResponseObject>} a promise for a StalenessResponseObject\n   *        or undefined if no provider exists\n   */\n  async isStale(domainObject) {\n    const provider = this.#findStalenessProvider(domainObject);\n\n    if (!provider) {\n      return;\n    }\n\n    const abortController = new AbortController();\n    const options = { signal: abortController.signal };\n    this.requestAbortControllers.add(abortController);\n\n    try {\n      const staleness = await provider.isStale(domainObject, options);\n\n      return staleness;\n    } finally {\n      this.requestAbortControllers.delete(abortController);\n    }\n  }\n\n  /**\n   * @private\n   * @param {import('openmct').DomainObject} domainObject the object\n   *        which has associated telemetry staleness\n   * @returns {StalenessProvider | undefined}\n   */\n  #findStalenessProvider(domainObject) {\n    return this.stalenessProviders.find((provider) => {\n      return provider.supportsStaleness(domainObject);\n    });\n  }\n\n  /**\n   * Get telemetry metadata for a given domain object.  Returns a telemetry\n   * metadata manager which provides methods for interrogating telemetry\n   * metadata.\n   *\n   * @returns {TelemetryMetadataManager}\n   */\n  getMetadata(domainObject) {\n    if (!this.metadataCache.has(domainObject)) {\n      const metadataProvider = this.#findMetadataProvider(domainObject);\n      if (!metadataProvider) {\n        return;\n      }\n\n      const metadata = metadataProvider.getMetadata(domainObject);\n\n      this.metadataCache.set(domainObject, new TelemetryMetadataManager(metadata));\n    }\n\n    return this.metadataCache.get(domainObject);\n  }\n\n  /**\n   * Remove a domain object from the telemetry metadata cache.\n   * @param {import('openmct').DomainObject} domainObject\n   */\n\n  removeMetadataFromCache(domainObject) {\n    this.metadataCache.delete(domainObject);\n  }\n\n  /**\n   * Get a value formatter for a given valueMetadata.\n   *\n   * @returns {TelemetryValueFormatter}\n   */\n  getValueFormatter(valueMetadata) {\n    if (!this.valueFormatterCache.has(valueMetadata)) {\n      this.valueFormatterCache.set(\n        valueMetadata,\n        new TelemetryValueFormatter(valueMetadata, this.formatters)\n      );\n    }\n\n    return this.valueFormatterCache.get(valueMetadata);\n  }\n\n  /**\n   * Get a value formatter for a given key.\n   * @param {string} key\n   *\n   * @returns {Format}\n   */\n  getFormatter(key) {\n    return this.formatters.get(key);\n  }\n\n  /**\n   * Get a format map of all value formatters for a given piece of telemetry\n   * metadata.\n   *\n   * @returns {Record<string, TelemetryValueFormatter>}\n   */\n  getFormatMap(metadata) {\n    if (!metadata) {\n      return {};\n    }\n\n    if (!this.formatMapCache.has(metadata)) {\n      const formatMap = metadata.values().reduce(\n        function (map, valueMetadata) {\n          map[valueMetadata.key] = this.getValueFormatter(valueMetadata);\n\n          return map;\n        }.bind(this),\n        {}\n      );\n      this.formatMapCache.set(metadata, formatMap);\n    }\n\n    return this.formatMapCache.get(metadata);\n  }\n\n  /**\n   * Error Handling: Missing Request provider\n   *\n   * @returns Promise\n   */\n  #handleMissingRequestProvider(domainObject) {\n    this.noRequestProviderForAllObjects = this.requestProviders.every((requestProvider) => {\n      const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);\n      const hasRequestProvider =\n        Object.prototype.hasOwnProperty.call(requestProvider, 'request') &&\n        typeof requestProvider.request === 'function';\n\n      return supportsRequest && hasRequestProvider;\n    });\n\n    let message = '';\n    let detailMessage = '';\n    if (this.noRequestProviderForAllObjects) {\n      message = 'Missing request providers, see console for details';\n      detailMessage = 'Missing request provider for all request providers';\n    } else {\n      message = 'Missing request provider, see console for details';\n      const { name, identifier } = domainObject;\n      detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify(\n        identifier\n      )}`;\n    }\n\n    this.openmct.notifications.error(message);\n    console.warn(detailMessage);\n\n    return Promise.resolve([]);\n  }\n\n  /**\n   * Register a new telemetry data formatter.\n   * @param {Format} format the\n   */\n  addFormat(format) {\n    this.formatters.set(format.key, format);\n  }\n\n  /**\n   * Get a limit evaluator for this domain object.\n   * Limit Evaluators help you evaluate limit and alarm status of individual\n   * telemetry datums for display purposes without having to interact directly\n   * with the Limit API.\n   *\n   * This method is optional.\n   * If a provider does not implement this method, it is presumed\n   * that no limits are defined for this domain object's telemetry.\n   *\n   * @param {import('openmct').DomainObject} domainObject the domain\n   *        object for which to evaluate limits\n   * @returns {module:openmct.TelemetryAPI~LimitEvaluator}\n   * @method limitEvaluator\n   */\n  limitEvaluator(domainObject) {\n    return this.getLimitEvaluator(domainObject);\n  }\n\n  /**\n   * Get a limits for this domain object.\n   * Limits help you display limits and alarms of\n   * telemetry for display purposes without having to interact directly\n   * with the Limit API.\n   *\n   * This method is optional.\n   * If a provider does not implement this method, it is presumed\n   * that no limits are defined for this domain object's telemetry.\n   *\n   * @param {import('openmct').DomainObject} domainObject the domain\n   *        object for which to get limits\n   * @returns {LimitsResponseObject}\n   * @method limits\n   */\n  limitDefinition(domainObject) {\n    return this.getLimits(domainObject);\n  }\n\n  /**\n   * Get a limit evaluator for this domain object.\n   * Limit Evaluators help you evaluate limit and alarm status of individual\n   * telemetry datums for display purposes without having to interact directly\n   * with the Limit API.\n   *\n   * This method is optional.\n   * If a provider does not implement this method, it is presumed\n   * that no limits are defined for this domain object's telemetry.\n   *\n   * @param {import('openmct').DomainObject} domainObject the domain\n   *        object for which to evaluate limits\n   * @returns {module:openmct.TelemetryAPI~LimitEvaluator}\n   * @method limitEvaluator\n   */\n  getLimitEvaluator(domainObject) {\n    const provider = this.#findLimitEvaluator(domainObject);\n    if (!provider) {\n      return {\n        evaluate: function () {}\n      };\n    }\n\n    return provider.getLimitEvaluator(domainObject);\n  }\n\n  /**\n   * Get a limit definitions for this domain object.\n   * Limit Definitions help you indicate limits and alarms of\n   * telemetry for display purposes without having to interact directly\n   * with the Limit API.\n   *\n   * This method is optional.\n   * If a provider does not implement this method, it is presumed\n   * that no limits are defined for this domain object's telemetry.\n   *\n   * @param {import('openmct').DomainObject} domainObject the domain\n   *        object for which to display limits\n   * @returns {LimitsResponseObject}\n   * @method limits returns a limits object of type {LimitsResponseObject}\n   *  supported colors are purple, red, orange, yellow and cyan\n   */\n  getLimits(domainObject) {\n    const provider = this.#findLimitEvaluator(domainObject);\n\n    if (!provider || !provider.getLimits) {\n      return {\n        limits: function () {\n          return Promise.resolve(undefined);\n        }\n      };\n    }\n\n    const abortController = new AbortController();\n    const options = { signal: abortController.signal };\n    this.requestAbortControllers.add(abortController);\n\n    try {\n      return provider.getLimits(domainObject, options);\n    } catch (error) {\n      if (error.name !== 'AbortError') {\n        this.openmct.notifications.error(\n          'Error requesting telemetry data, see console for details'\n        );\n      }\n\n      throw new Error(error);\n    } finally {\n      this.requestAbortControllers.delete(abortController);\n    }\n  }\n}\n\n/**\n * A LimitEvaluator may be used to detect when telemetry values\n * have exceeded nominal conditions.\n *\n * @interface LimitEvaluator\n */\n\n/**\n * Check for any limit violations associated with a telemetry datum.\n * @method evaluate\n * @param {*} datum the telemetry datum to evaluate\n * @param {TelemetryProperty} the property to check for limit violations\n * @returns {LimitViolation} metadata about\n *          the limit violation, or undefined if a value is within limits\n */\n\n/**\n * A violation of limits defined for a telemetry property.\n * @typedef LimitViolation\n * @property {string} cssClass the class (or space-separated classes) to\n *           apply to display elements for values which violate this limit\n * @property {string} name the human-readable name for the limit violation\n * @property {number} low a lower limit for violation\n * @property {number} high a higher limit violation\n */\n\n/**\n * @typedef {Object} LimitsResponseObject\n * @property {LimitDefinition} limitLevel the level name and it's limit definition\n * @example {\n *  [limitLevel]: {\n *    low: {\n *      color: lowColor,\n *      value: lowValue\n *    },\n *    high: {\n *      color: highColor,\n *      value: highValue\n *    }\n *  }\n * }\n */\n\n/**\n * Limit defined for a telemetry property.\n * @typedef LimitDefinition\n * @property {LimitDefinitionValue} low a lower limit\n * @property {LimitDefinitionValue} high a higher limit\n */\n\n/**\n * Limit definition for a Limit of a telemetry property.\n * @typedef LimitDefinitionValue\n * @property {string} color color to represent this limit\n * @property {number} value the limit value\n */\n\n/**\n * A TelemetryFormatter converts telemetry values for purposes of\n * display as text.\n *\n * @interface TelemetryFormatter\n */\n\n/**\n * Retrieve the 'key' from the datum and format it accordingly to\n * telemetry metadata in domain object.\n *\n * @method format\n */\n\n/**\n * Describes a property which would be found in a datum of telemetry\n * associated with a particular domain object.\n *\n * @typedef TelemetryProperty\n * @property {string} key the name of the property in the datum which\n *           contains this telemetry value\n * @property {string} name the human-readable name for this property\n * @property {string} [units] the units associated with this property\n * @property {boolean} [temporal] true if this property is a timestamp, or\n *           may be otherwise used to order telemetry in a time-like\n *           fashion; default is false\n * @property {boolean} [numeric] true if the values for this property\n *           can be interpreted plainly as numbers; default is true\n * @property {boolean} [enumerated] true if this property may have only\n *           certain specific values; default is false\n * @property {string} [values] for enumerated states, an ordered list\n *           of possible values\n */\n\n/**\n * Describes and bounds requests for telemetry data.\n *\n * @typedef TelemetryRequest\n * @property {string} sort the key of the property to sort by. This may\n *           be prefixed with a \"+\" or a \"-\" sign to sort in ascending\n *           or descending order respectively. If no prefix is present,\n *           ascending order will be used.\n * @property {*} start the lower bound for values of the sorting property\n * @property {*} end the upper bound for values of the sorting property\n * @property {string[]} strategies symbolic identifiers for strategies\n *           (such as `minmax`) which may be recognized by providers;\n *           these will be tried in order until an appropriate provider\n *           is found\n */\n\n/**\n * Provides telemetry data. To connect to new data sources, new\n * TelemetryProvider implementations should be\n * [registered]{@link module:openmct.TelemetryAPI#addProvider}.\n *\n * @interface TelemetryProvider\n */\n\n/**\n * Provides telemetry staleness data. To subscribe to telemetry staleness,\n * new StalenessProvider implementations should be\n * [registered]{@link module:openmct.TelemetryAPI#addProvider}.\n *\n * @interface StalenessProvider\n * @property {function} supportsStaleness receives a domainObject and\n *           returns a boolean to indicate it will provide staleness\n * @property {function} subscribeToStaleness receives a domainObject to\n *           be subscribed to and a callback to invoke with a StalenessResponseObject\n * @property {function} isStale an asynchronous method called with a domainObject\n *           and an options object which currently has an abort signal, ex.\n *           { signal: <AbortController.signal> }\n *           this method should return a current StalenessResponseObject\n */\n\n/**\n * @typedef {Object} StalenessResponseObject\n * @property {boolean} isStale boolean representing the staleness state\n * @property {number} timestamp Unix timestamp in milliseconds\n */\n\n/**\n * An interface for retrieving telemetry data associated with a domain\n * object.\n *\n * @interface TelemetryAPI\n * @augments module:openmct.TelemetryAPI~TelemetryProvider\n */\n"
  },
  {
    "path": "src/api/telemetry/TelemetryAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport TelemetryAPI from './TelemetryAPI.js';\nimport TelemetryCollection from './TelemetryCollection.js';\n\ndescribe('Telemetry API', () => {\n  let openmct;\n  let telemetryAPI;\n\n  beforeEach(() => {\n    openmct = {\n      time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'getTimeSystem', 'bounds', 'getBounds']),\n      types: jasmine.createSpyObj('typeRegistry', ['get'])\n    };\n\n    openmct.time.timeSystem.and.returnValue({ key: 'system' });\n    openmct.time.getTimeSystem.and.returnValue({ key: 'system' });\n    openmct.time.bounds.and.returnValue({\n      start: 0,\n      end: 1\n    });\n    openmct.time.getBounds.and.returnValue({\n      start: 0,\n      end: 1\n    });\n    telemetryAPI = new TelemetryAPI(openmct);\n  });\n\n  describe('Telemetry providers', () => {\n    let telemetryProvider;\n    let domainObject;\n\n    beforeEach(() => {\n      telemetryProvider = jasmine.createSpyObj('telemetryProvider', [\n        'supportsSubscribe',\n        'subscribe',\n        'supportsRequest',\n        'request'\n      ]);\n      domainObject = {\n        identifier: {\n          key: 'a',\n          namespace: 'b'\n        },\n        type: 'sample-type'\n      };\n\n      openmct.notifications = {\n        error: () => {\n          console.log('sample error notification');\n        }\n      };\n    });\n\n    it('provides consistent results without providers', async () => {\n      const unsubscribe = telemetryAPI.subscribe(domainObject);\n\n      expect(unsubscribe).toEqual(jasmine.any(Function));\n\n      const data = await telemetryAPI.request(domainObject);\n      expect(data).toEqual([]);\n    });\n\n    it('skips providers that do not match', async () => {\n      telemetryProvider.supportsSubscribe.and.returnValue(false);\n      telemetryProvider.supportsRequest.and.returnValue(false);\n      telemetryProvider.request.and.returnValue(Promise.resolve([]));\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const callback = jasmine.createSpy('callback');\n      const unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {\n        strategy: 'latest'\n      });\n      expect(telemetryProvider.subscribe).not.toHaveBeenCalled();\n      expect(unsubscribe).toEqual(jasmine.any(Function));\n\n      await telemetryAPI.request(domainObject);\n      expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(\n        domainObject,\n        jasmine.any(Object)\n      );\n      expect(telemetryProvider.request).not.toHaveBeenCalled();\n    });\n\n    it('sends subscribe calls to matching providers', () => {\n      const unsubFunc = jasmine.createSpy('unsubscribe');\n      telemetryProvider.subscribe.and.returnValue(unsubFunc);\n      telemetryProvider.supportsSubscribe.and.returnValue(true);\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const callback = jasmine.createSpy('callback');\n      const unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1);\n      expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {\n        strategy: 'latest'\n      });\n      expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n      expect(telemetryProvider.subscribe).toHaveBeenCalledWith(\n        domainObject,\n        jasmine.any(Function),\n        {\n          strategy: 'latest'\n        }\n      );\n\n      const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];\n      notify('someValue');\n      expect(callback).toHaveBeenCalledWith('someValue');\n\n      expect(unsubscribe).toEqual(jasmine.any(Function));\n      expect(unsubFunc).not.toHaveBeenCalled();\n      unsubscribe();\n      expect(unsubFunc).toHaveBeenCalled();\n\n      notify('otherValue');\n      expect(callback).not.toHaveBeenCalledWith('otherValue');\n    });\n\n    it('subscribes once per object', () => {\n      const unsubFunc = jasmine.createSpy('unsubscribe');\n      telemetryProvider.subscribe.and.returnValue(unsubFunc);\n      telemetryProvider.supportsSubscribe.and.returnValue(true);\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const callback = jasmine.createSpy('callback');\n      const callbacktwo = jasmine.createSpy('callback two');\n      const unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      const unsubscribetwo = telemetryAPI.subscribe(domainObject, callbacktwo);\n\n      expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n\n      const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];\n      notify('someValue');\n      expect(callback).toHaveBeenCalledWith('someValue');\n      expect(callbacktwo).toHaveBeenCalledWith('someValue');\n\n      unsubscribe();\n      expect(unsubFunc).not.toHaveBeenCalled();\n      notify('otherValue');\n      expect(callback).not.toHaveBeenCalledWith('otherValue');\n      expect(callbacktwo).toHaveBeenCalledWith('otherValue');\n\n      unsubscribetwo();\n      expect(unsubFunc).toHaveBeenCalled();\n      notify('anotherValue');\n      expect(callback).not.toHaveBeenCalledWith('anotherValue');\n      expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');\n    });\n\n    it('only deletes subscription cache when there are no more subscribers', () => {\n      const unsubFunc = jasmine.createSpy('unsubscribe');\n      telemetryProvider.subscribe.and.returnValue(unsubFunc);\n      telemetryProvider.supportsSubscribe.and.returnValue(true);\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const callback = jasmine.createSpy('callback');\n      const callbacktwo = jasmine.createSpy('callback two');\n      const callbackThree = jasmine.createSpy('callback three');\n      const unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      const unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo);\n\n      expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n      unsubscribe();\n      const unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree);\n      // Regression test for where subscription cache was deleted on each unsubscribe, resulting in\n      // superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe,\n      // then a subsequent subscribe will result in a new subscription at the provider.\n      expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n      unsubscribeTwo();\n      unsubscribeThree();\n    });\n\n    it('does subscribe/unsubscribe', () => {\n      const unsubFunc = jasmine.createSpy('unsubscribe');\n      telemetryProvider.subscribe.and.returnValue(unsubFunc);\n      telemetryProvider.supportsSubscribe.and.returnValue(true);\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const callback = jasmine.createSpy('callback');\n      let unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n      unsubscribe();\n\n      unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      expect(telemetryProvider.subscribe.calls.count()).toBe(2);\n      unsubscribe();\n    });\n\n    it('subscribes for different object', () => {\n      const unsubFuncs = [];\n      const notifiers = [];\n      telemetryProvider.supportsSubscribe.and.returnValue(true);\n      telemetryProvider.subscribe.and.callFake(function (obj, cb) {\n        const unsubFunc = jasmine.createSpy('unsubscribe ' + unsubFuncs.length);\n        unsubFuncs.push(unsubFunc);\n        notifiers.push(cb);\n\n        return unsubFunc;\n      });\n      telemetryAPI.addProvider(telemetryProvider);\n\n      const otherDomainObject = JSON.parse(JSON.stringify(domainObject));\n      otherDomainObject.identifier.namespace = 'other';\n\n      const callback = jasmine.createSpy('callback');\n      const callbacktwo = jasmine.createSpy('callback two');\n\n      const unsubscribe = telemetryAPI.subscribe(domainObject, callback);\n      const unsubscribetwo = telemetryAPI.subscribe(otherDomainObject, callbacktwo);\n\n      expect(telemetryProvider.subscribe.calls.count()).toBe(2);\n\n      notifiers[0]('someValue');\n      expect(callback).toHaveBeenCalledWith('someValue');\n      expect(callbacktwo).not.toHaveBeenCalledWith('someValue');\n\n      notifiers[1]('anotherValue');\n      expect(callback).not.toHaveBeenCalledWith('anotherValue');\n      expect(callbacktwo).toHaveBeenCalledWith('anotherValue');\n\n      unsubscribe();\n      expect(unsubFuncs[0]).toHaveBeenCalled();\n      expect(unsubFuncs[1]).not.toHaveBeenCalled();\n\n      unsubscribetwo();\n      expect(unsubFuncs[1]).toHaveBeenCalled();\n    });\n\n    it('sends requests to matching providers', async () => {\n      const telemPromise = Promise.resolve([]);\n      telemetryProvider.supportsRequest.and.returnValue(true);\n      telemetryProvider.request.and.returnValue(telemPromise);\n      telemetryAPI.addProvider(telemetryProvider);\n\n      await telemetryAPI.request(domainObject);\n      expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(\n        domainObject,\n        jasmine.any(Object)\n      );\n      expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, jasmine.any(Object));\n    });\n\n    it('generates default request options', async () => {\n      telemetryProvider.supportsRequest.and.returnValue(true);\n      telemetryProvider.request.and.returnValue(Promise.resolve([]));\n      telemetryAPI.addProvider(telemetryProvider);\n\n      await telemetryAPI.request(domainObject);\n      const { signal } = new AbortController();\n      expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {\n        signal,\n        start: 0,\n        end: 1,\n        domain: 'system',\n        timeContext: openmct.time\n      });\n\n      expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {\n        signal,\n        start: 0,\n        end: 1,\n        domain: 'system',\n        timeContext: openmct.time\n      });\n\n      telemetryProvider.supportsRequest.calls.reset();\n      telemetryProvider.request.calls.reset();\n\n      await telemetryAPI.request(domainObject, {});\n      expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {\n        signal,\n        start: 0,\n        end: 1,\n        domain: 'system',\n        timeContext: openmct.time\n      });\n\n      expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {\n        signal,\n        start: 0,\n        end: 1,\n        domain: 'system',\n        timeContext: openmct.time\n      });\n    });\n\n    it('do not overwrite existing request options', async () => {\n      telemetryProvider.supportsRequest.and.returnValue(true);\n      telemetryProvider.request.and.returnValue(Promise.resolve([]));\n      telemetryAPI.addProvider(telemetryProvider);\n\n      await telemetryAPI.request(domainObject, {\n        start: 20,\n        end: 30,\n        domain: 'someDomain'\n      });\n      const { signal } = new AbortController();\n      expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {\n        start: 20,\n        end: 30,\n        domain: 'someDomain',\n        signal,\n        timeContext: openmct.time\n      });\n\n      expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {\n        start: 20,\n        end: 30,\n        domain: 'someDomain',\n        signal,\n        timeContext: openmct.time\n      });\n    });\n    describe('telemetry batching support', () => {\n      let callbacks;\n      let unsubFunc;\n\n      beforeEach(() => {\n        callbacks = [];\n        unsubFunc = jasmine.createSpy('unsubscribe');\n        telemetryProvider.supportsBatching = jasmine.createSpy('supportsBatching');\n        telemetryProvider.supportsBatching.and.returnValue(true);\n        telemetryProvider.supportsSubscribe.and.returnValue(true);\n\n        telemetryProvider.subscribe.and.callFake(function (obj, cb, options) {\n          callbacks.push(cb);\n\n          return unsubFunc;\n        });\n\n        telemetryAPI.addProvider(telemetryProvider);\n      });\n\n      it('caches subscriptions for batched and latest telemetry subscriptions', () => {\n        const latestCallback1 = jasmine.createSpy('latestCallback1');\n        const unsubscribeFromLatest1 = telemetryAPI.subscribe(domainObject, latestCallback1, {\n          strategy: 'latest'\n        });\n        const latestCallback2 = jasmine.createSpy('latestCallback2');\n        const unsubscribeFromLatest2 = telemetryAPI.subscribe(domainObject, latestCallback2, {\n          strategy: 'latest'\n        });\n\n        //Expect a single cached subscription for latest telemetry\n        expect(telemetryProvider.subscribe.calls.count()).toBe(1);\n\n        const batchedCallback1 = jasmine.createSpy('batchedCallback1');\n        const unsubscribeFromBatched1 = telemetryAPI.subscribe(domainObject, batchedCallback1, {\n          strategy: 'batch'\n        });\n\n        const batchedCallback2 = jasmine.createSpy('batchedCallback2');\n        const unsubscribeFromBatched2 = telemetryAPI.subscribe(domainObject, batchedCallback2, {\n          strategy: 'batch'\n        });\n\n        //Expect a single cached subscription for each strategy telemetry\n        expect(telemetryProvider.subscribe.calls.count()).toBe(2);\n\n        unsubscribeFromLatest1();\n        unsubscribeFromLatest2();\n        unsubscribeFromBatched1();\n        unsubscribeFromBatched2();\n\n        expect(unsubFunc).toHaveBeenCalledTimes(2);\n      });\n      it('subscriptions with the latest strategy are always invoked with a single value', () => {\n        const latestCallback = jasmine.createSpy('latestCallback1');\n        telemetryAPI.subscribe(domainObject, latestCallback, {\n          strategy: 'latest'\n        });\n\n        const batchedValues = [1, 2, 3];\n        callbacks.forEach((cb) => {\n          cb(batchedValues);\n        });\n\n        expect(latestCallback).toHaveBeenCalledWith(3);\n\n        const singleValue = 1;\n        callbacks.forEach((cb) => {\n          cb(singleValue);\n        });\n\n        expect(latestCallback).toHaveBeenCalledWith(1);\n      });\n\n      it('subscriptions with the batch strategy are always invoked with an array', () => {\n        const batchedCallback = jasmine.createSpy('batchedCallback1');\n        const latestCallback = jasmine.createSpy('latestCallback1');\n        telemetryAPI.subscribe(domainObject, batchedCallback, {\n          strategy: 'batch'\n        });\n        telemetryAPI.subscribe(domainObject, latestCallback, {\n          strategy: 'latest'\n        });\n\n        const batchedValues = [1, 2, 3];\n        callbacks.forEach((cb) => {\n          cb(batchedValues);\n        });\n\n        // Callbacks for the 'batch' strategy are always called with an array of values\n        expect(batchedCallback).toHaveBeenCalledWith(batchedValues);\n        // Callbacks for the 'latest' strategy are always called with a single value\n        expect(latestCallback).toHaveBeenCalledWith(3);\n\n        callbacks.forEach((cb) => {\n          cb(1);\n        });\n        // Callbacks for the 'batch' strategy are always called with an array of values, even if there is only one value\n        expect(batchedCallback).toHaveBeenCalledWith([1]);\n        // Callbacks for the 'latest' strategy are always called with a single value\n        expect(latestCallback).toHaveBeenCalledWith(1);\n      });\n\n      it('legacy providers are left unchanged, with a single subscription', () => {\n        delete telemetryProvider.supportsBatching;\n\n        const batchCallback = jasmine.createSpy('batchCallback');\n        telemetryAPI.subscribe(domainObject, batchCallback, {\n          strategy: 'batch'\n        });\n        expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');\n\n        const latestCallback = jasmine.createSpy('latestCallback');\n        telemetryAPI.subscribe(domainObject, latestCallback, {\n          strategy: 'latest'\n        });\n\n        expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');\n      });\n    });\n  });\n\n  describe('metadata', () => {\n    let mockMetadata = {};\n    let mockObjectType = {\n      definition: {}\n    };\n    beforeEach(() => {\n      telemetryAPI.addProvider({\n        key: 'mockMetadataProvider',\n        supportsMetadata() {\n          return true;\n        },\n        getMetadata() {\n          return mockMetadata;\n        }\n      });\n      openmct.types.get.and.returnValue(mockObjectType);\n    });\n\n    it('respects explicit priority', () => {\n      mockMetadata.values = [\n        {\n          key: 'name',\n          name: 'Name',\n          hints: {\n            priority: 2\n          }\n        },\n        {\n          key: 'timestamp',\n          name: 'Timestamp',\n          hints: {\n            priority: 1\n          }\n        },\n        {\n          key: 'sin',\n          name: 'Sine',\n          hints: {\n            priority: 4\n          }\n        },\n        {\n          key: 'cos',\n          name: 'Cosine',\n          hints: {\n            priority: 3\n          }\n        }\n      ];\n      let metadata = telemetryAPI.getMetadata({});\n      let values = metadata.values();\n\n      values.forEach((value, index) => {\n        expect(value.hints.priority).toBe(index + 1);\n      });\n    });\n    it('if no explicit priority, defaults to order defined', () => {\n      mockMetadata.values = [\n        {\n          key: 'name',\n          name: 'Name'\n        },\n        {\n          key: 'timestamp',\n          name: 'Timestamp'\n        },\n        {\n          key: 'sin',\n          name: 'Sine'\n        },\n        {\n          key: 'cos',\n          name: 'Cosine'\n        }\n      ];\n      let metadata = telemetryAPI.getMetadata({});\n      let values = metadata.values();\n\n      values.forEach((value, index) => {\n        expect(value.key).toBe(mockMetadata.values[index].key);\n      });\n    });\n    it('respects domain priority', () => {\n      mockMetadata.values = [\n        {\n          key: 'name',\n          name: 'Name'\n        },\n        {\n          key: 'timestamp-utc',\n          name: 'Timestamp UTC',\n          hints: {\n            domain: 2\n          }\n        },\n        {\n          key: 'timestamp-local',\n          name: 'Timestamp Local',\n          hints: {\n            domain: 1\n          }\n        },\n        {\n          key: 'sin',\n          name: 'Sine',\n          hints: {\n            range: 2\n          }\n        },\n        {\n          key: 'cos',\n          name: 'Cosine',\n          hints: {\n            range: 1\n          }\n        }\n      ];\n      let metadata = telemetryAPI.getMetadata({});\n      let values = metadata.valuesForHints(['domain']);\n\n      expect(values[0].key).toBe('timestamp-local');\n      expect(values[1].key).toBe('timestamp-utc');\n    });\n    it('respects range priority', () => {\n      mockMetadata.values = [\n        {\n          key: 'name',\n          name: 'Name'\n        },\n        {\n          key: 'timestamp-utc',\n          name: 'Timestamp UTC',\n          hints: {\n            domain: 2\n          }\n        },\n        {\n          key: 'timestamp-local',\n          name: 'Timestamp Local',\n          hints: {\n            domain: 1\n          }\n        },\n        {\n          key: 'sin',\n          name: 'Sine',\n          hints: {\n            range: 2\n          }\n        },\n        {\n          key: 'cos',\n          name: 'Cosine',\n          hints: {\n            range: 1\n          }\n        }\n      ];\n      let metadata = telemetryAPI.getMetadata({});\n      let values = metadata.valuesForHints(['range']);\n\n      expect(values[0].key).toBe('cos');\n      expect(values[1].key).toBe('sin');\n    });\n    it('respects priority and domain ordering', () => {\n      mockMetadata.values = [\n        {\n          key: 'id',\n          name: 'ID',\n          hints: {\n            priority: 2\n          }\n        },\n        {\n          key: 'name',\n          name: 'Name',\n          hints: {\n            priority: 1\n          }\n        },\n        {\n          key: 'timestamp-utc',\n          name: 'Timestamp UTC',\n          hints: {\n            domain: 2,\n            priority: 1\n          }\n        },\n        {\n          key: 'timestamp-local',\n          name: 'Timestamp Local',\n          hints: {\n            domain: 1,\n            priority: 2\n          }\n        },\n        {\n          key: 'timestamp-pst',\n          name: 'Timestamp PST',\n          hints: {\n            domain: 3,\n            priority: 2\n          }\n        },\n        {\n          key: 'sin',\n          name: 'Sine'\n        },\n        {\n          key: 'cos',\n          name: 'Cosine'\n        }\n      ];\n      let metadata = telemetryAPI.getMetadata({});\n      let values = metadata.valuesForHints(['priority', 'domain']);\n      ['timestamp-utc', 'timestamp-local', 'timestamp-pst'].forEach((key, index) => {\n        expect(values[index].key).toBe(key);\n      });\n    });\n  });\n\n  describe('telemetry collections', () => {\n    let domainObject;\n    let mockMetadata = {};\n    let mockObjectType = {\n      definition: {}\n    };\n\n    beforeEach(() => {\n      openmct.telemetry = telemetryAPI;\n      telemetryAPI.addProvider({\n        key: 'mockMetadataProvider',\n        supportsMetadata() {\n          return true;\n        },\n        getMetadata() {\n          return mockMetadata;\n        }\n      });\n      openmct.types.get.and.returnValue(mockObjectType);\n      domainObject = {\n        identifier: {\n          key: 'a',\n          namespace: 'b'\n        },\n        type: 'sample-type'\n      };\n    });\n\n    it('when requested, returns an instance of telemetry collection', () => {\n      const telemetryCollection = telemetryAPI.requestCollection(domainObject);\n\n      expect(telemetryCollection).toBeInstanceOf(TelemetryCollection);\n    });\n  });\n});\n\ndescribe('telemetry', () => {\n  let openmct;\n  let telemetryProvider;\n  let telemetryAPI;\n  let watchedSignal;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n    openmct.install(openmct.plugins.MyItems());\n\n    telemetryAPI = openmct.telemetry;\n\n    telemetryProvider = {\n      request: (obj, options) => {\n        watchedSignal = options.signal;\n\n        return Promise.resolve();\n      }\n    };\n    spyOn(telemetryAPI, 'findRequestProvider').and.returnValue(telemetryProvider);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should not abort request without navigation', async () => {\n    telemetryAPI.addProvider(telemetryProvider);\n\n    await telemetryAPI.request({});\n    expect(watchedSignal.aborted).toBe(false);\n  });\n\n  it('should abort request on navigation', (done) => {\n    telemetryAPI.addProvider(telemetryProvider);\n\n    telemetryAPI.request({}).finally(() => {\n      expect(watchedSignal.aborted).toBe(true);\n      done();\n    });\n    openmct.router.doPathChange('newPath', 'oldPath');\n  });\n});\n"
  },
  {
    "path": "src/api/telemetry/TelemetryCollection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport { TIME_CONTEXT_EVENTS } from '../time/constants.js';\nimport { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants.js';\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n\n/**\n * @typedef {import('../time/TimeContext').TimeContext} TimeContext\n */\n\n/**\n * @typedef {import('./TelemetryAPI').TelemetryRequestOptions} TelemetryRequestOptions\n */\n\n/**\n * @typedef {import('../../../openmct').OpenMCT} OpenMCT\n */\n\n/** Class representing a Telemetry Collection. */\n\nexport default class TelemetryCollection extends EventEmitter {\n  /**\n   * Creates a Telemetry Collection\n   *\n   * @param  {OpenMCT} openmct - Open MCT\n   * @param  {DomainObject} domainObject - Domain Object to use for telemetry collection\n   * @param  {TelemetryRequestOptions} options - Any options passed in for request/subscribe\n   */\n  constructor(openmct, domainObject, options = {}) {\n    super();\n\n    this.loaded = false;\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.boundedTelemetry = [];\n    this.futureBuffer = [];\n    this.parseTime = undefined;\n    this.metadata = this.openmct.telemetry.getMetadata(domainObject);\n    this.options = options;\n    this.unsubscribe = undefined;\n    this.pageState = undefined;\n    this.lastBounds = undefined;\n    this.requestAbort = undefined;\n    this.isStrategyLatest = this.options.strategy === 'latest';\n    this.dataOutsideTimeBounds = false;\n    this.modeChanged = false;\n\n    this.bootstrapBounds = this._extractBootstrapBounds(options);\n    this.isInBootstrapMode = this.bootstrapBounds !== null;\n  }\n\n  /**\n   * Returns initial bounds if provided.\n   * Supports partial bounds (only start OR only end).\n   * @private\n   * @returns  bounds or null\n   */\n  _extractBootstrapBounds(options) {\n    const hasStart = options.start !== undefined;\n    const hasEnd = options.end !== undefined;\n\n    if (!hasStart && !hasEnd) {\n      return null;\n    }\n\n    return {\n      start: hasStart ? options.start : null,\n      end: hasEnd ? options.end : null\n    };\n  }\n\n  _isBootstrapMode() {\n    return this.isInBootstrapMode;\n  }\n\n  /**\n   * Switch from bootstrap mode to normal time conductor tracking\n   * @private\n   */\n  _exitBootstrapMode() {\n    this.isInBootstrapMode = false;\n    this.bootstrapBounds = null;\n    // Clean up options\n    delete this.options?.start;\n    delete this.options?.end;\n  }\n\n  /**\n   * Get the bounds to use for filtering telemetry data.\n   * Bootstrap mode (initial load):\n   *   - Historical data: uses bootstrapBounds merged with time conductor\n   *   - Subscription data: uses lastBounds (current time conductor)\n   * Normal mode (after user interaction):\n   *   - All data: uses lastBounds (current time conductor)\n   * @private\n   * @param {boolean} isHistoricalData - true for historical, false for subscription\n   * @returns Bounds to use for filtering\n   */\n  _getBoundsForFiltering(isHistoricalData) {\n    if (this._isBootstrapMode() && isHistoricalData) {\n      const bounds = { ...this.lastBounds };\n\n      if (this.bootstrapBounds.start !== null) {\n        bounds.start = this.bootstrapBounds.start;\n      }\n      if (this.bootstrapBounds.end !== null) {\n        bounds.end = this.bootstrapBounds.end;\n      }\n\n      return bounds;\n    }\n\n    return this.lastBounds;\n  }\n\n  /**\n   * This will start the requests for historical and realtime data,\n   * as well as setting up initial values and watchers\n   */\n  load() {\n    if (this.loaded) {\n      this._error(LOADED_ERROR);\n    }\n\n    if (!Object.hasOwn(this.options, 'timeContext')) {\n      this.options.timeContext = this.openmct.time;\n    }\n    this._setTimeSystem(this.options.timeContext.getTimeSystem());\n\n    this.lastBounds = this.options.timeContext.getBounds();\n    // Override with bootstrap bounds where provided\n    if (this._isBootstrapMode()) {\n      if (this.bootstrapBounds.start !== null) {\n        this.lastBounds.start = this.bootstrapBounds.start;\n      }\n      if (this.bootstrapBounds.end !== null) {\n        this.lastBounds.end = this.bootstrapBounds.end;\n      }\n    }\n    this._watchBounds();\n    this._watchTimeSystem();\n    this._watchTimeModeChange();\n\n    this._requestHistoricalTelemetry();\n    this._initiateSubscriptionTelemetry();\n\n    this.loaded = true;\n  }\n\n  /**\n   * can/should be called by the requester of the telemetry collection\n   * to remove any listeners\n   */\n  destroy() {\n    if (this.requestAbort) {\n      this.requestAbort.abort();\n    }\n\n    this._unwatchBounds();\n    this._unwatchTimeSystem();\n    this._unwatchTimeModeChange();\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n\n    this.removeAllListeners();\n    this.loaded = false;\n  }\n\n  /**\n   * @returns {Array} All bounded telemetry\n   */\n  getAll() {\n    return this.boundedTelemetry;\n  }\n\n  /**\n   * If a historical provider exists, then historical requests will be made\n   * @private\n   */\n  async _requestHistoricalTelemetry() {\n    const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });\n    const historicalProvider = this.openmct.telemetry.findRequestProvider(\n      this.domainObject,\n      options\n    );\n\n    if (!historicalProvider) {\n      return;\n    }\n\n    let historicalData;\n\n    options.onPartialResponse = this._processNewTelemetry.bind(this);\n\n    try {\n      if (this.requestAbort) {\n        this.requestAbort.abort();\n      }\n\n      this.requestAbort = new AbortController();\n      options.signal = this.requestAbort.signal;\n      this.emit('requestStarted');\n      const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(\n        this.domainObject,\n        options\n      );\n      historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);\n    } catch (error) {\n      if (error.name !== 'AbortError') {\n        console.error('Error requesting telemetry data...');\n        this._error(error);\n      }\n    }\n\n    this.emit('requestEnded');\n    this.requestAbort = undefined;\n\n    if (!historicalData || !historicalData.length) {\n      return;\n    }\n\n    this._processNewTelemetry(historicalData, false);\n  }\n\n  /**\n   * This uses the built in subscription function from Telemetry API\n   * @private\n   */\n  _initiateSubscriptionTelemetry() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n    const options = { ...this.options };\n    //We always want to receive all available values in telemetry tables.\n    options.strategy = this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH;\n    this.unsubscribe = this.openmct.telemetry.subscribe(\n      this.domainObject,\n      (datum) => this._processNewTelemetry(datum, true),\n      options\n    );\n  }\n\n  /**\n   * Filter any new telemetry (add/page, historical, subscription) based on\n   * time bounds and dupes\n   *\n   * @param  {(Object|Object[])} telemetryData - telemetry data object or\n   * array of telemetry data objects\n   * @param  {boolean} isSubscriptionData - `true` if the telemetry data is new subscription data,\n   * @private\n   */\n  _processNewTelemetry(telemetryData, isSubscriptionData = false) {\n    if (telemetryData === undefined) {\n      return;\n    }\n\n    let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];\n    let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];\n    let parsedValue;\n    let beforeStartOfBounds;\n    let afterEndOfBounds;\n    let added = [];\n    let addedIndices = [];\n    let hasDataBeforeStartBound = false;\n    let size = this.options.size;\n    let enforceSize = size !== undefined && this.options.enforceSize;\n\n    const isHistoricalData = !isSubscriptionData;\n    const boundsToUse = this._getBoundsForFiltering(isHistoricalData);\n\n    // loop through, sort and dedupe\n    for (let datum of data) {\n      parsedValue = this.parseTime(datum);\n      beforeStartOfBounds = parsedValue < boundsToUse.start;\n      afterEndOfBounds = parsedValue > boundsToUse.end;\n      if (\n        !afterEndOfBounds &&\n        (!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD()))\n      ) {\n        let isDuplicate = false;\n        let startIndex = this._sortedIndex(datum);\n        let endIndex = undefined;\n\n        // dupe check\n        if (startIndex !== this.boundedTelemetry.length) {\n          endIndex = _.sortedLastIndexBy(this.boundedTelemetry, datum, (boundedDatum) =>\n            this.parseTime(boundedDatum)\n          );\n\n          if (endIndex > startIndex) {\n            let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);\n            isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));\n          }\n        } else if (startIndex === this.boundedTelemetry.length) {\n          isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);\n        }\n\n        if (!isDuplicate) {\n          let index = endIndex || startIndex;\n\n          this.boundedTelemetry.splice(index, 0, datum);\n          addedIndices.push(index);\n          added.push(datum);\n\n          if (!hasDataBeforeStartBound && beforeStartOfBounds) {\n            hasDataBeforeStartBound = true;\n          }\n        }\n      } else if (afterEndOfBounds) {\n        this.futureBuffer.push(datum);\n      }\n    }\n\n    if (added.length) {\n      // if latest strategy is requested, we need to check if the value is the latest unemitted value\n      if (this.isStrategyLatest) {\n        this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];\n\n        // if true, then this value has yet to be emitted\n        if (this.boundedTelemetry[0] !== latestBoundedDatum) {\n          if (hasDataBeforeStartBound) {\n            this._handleDataOutsideBounds();\n          } else {\n            this._handleDataInsideBounds();\n          }\n\n          this.emit('add', this.boundedTelemetry);\n        }\n      } else {\n        this.emit('add', added, addedIndices);\n\n        if (enforceSize && this.boundedTelemetry.length > size) {\n          const removeCount = this.boundedTelemetry.length - size;\n          const removed = this.boundedTelemetry.splice(0, removeCount);\n\n          this.emit('remove', removed);\n        }\n      }\n    }\n  }\n\n  /**\n   * Finds the correct insertion point for the given telemetry datum.\n   * Leverages lodash's `sortedIndexBy` function which implements a binary search.\n   * @private\n   */\n  _sortedIndex(datum) {\n    if (this.boundedTelemetry.length === 0) {\n      return 0;\n    }\n\n    let parsedValue = this.parseTime(datum);\n    let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);\n\n    if (parsedValue > lastValue || parsedValue === lastValue) {\n      return this.boundedTelemetry.length;\n    } else {\n      return _.sortedIndexBy(this.boundedTelemetry, datum, (boundedDatum) =>\n        this.parseTime(boundedDatum)\n      );\n    }\n  }\n\n  /**\n   * when the start time, end time, or both have been updated.\n   * data could be added OR removed here we update the current\n   * bounded telemetry\n   *\n   * @param  {TimeConductorBounds} bounds The newly updated bounds\n   * @param  {boolean} [tick] `true` if the bounds update was due to\n   * a \"tick\" event (ie. was an automatic update), false otherwise.\n   * @private\n   */\n  _bounds(bounds, isTick) {\n    if (this.modeChanged) {\n      this.modeChanged = false;\n      this._reset();\n      return;\n    }\n\n    let startChanged = this.lastBounds.start !== bounds.start;\n    let endChanged = this.lastBounds.end !== bounds.end;\n\n    this.lastBounds = bounds;\n\n    if (isTick) {\n      if (this.timeKey === undefined) {\n        return;\n      }\n\n      // need to check futureBuffer and need to check\n      // if anything has fallen out of bounds\n      let startIndex = 0;\n      let endIndex = 0;\n\n      let discarded = [];\n      let added = [];\n      let testDatum = {};\n\n      if (endChanged) {\n        testDatum[this.timeKey] = bounds.end;\n        // Calculate the new index of the last item in bounds\n        endIndex = _.sortedLastIndexBy(this.futureBuffer, testDatum, (datum) =>\n          this.parseTime(datum)\n        );\n        added = this.futureBuffer.splice(0, endIndex);\n      }\n\n      if (startChanged) {\n        testDatum[this.timeKey] = bounds.start;\n\n        // a little more complicated if not latest strategy\n        if (!this.isStrategyLatest) {\n          // Calculate the new index of the first item within the bounds\n          startIndex = _.sortedIndexBy(this.boundedTelemetry, testDatum, (datum) =>\n            this.parseTime(datum)\n          );\n          discarded = this.boundedTelemetry.splice(0, startIndex);\n        } else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {\n          // if greedyLAD is active and there is no new data to replace, don't discard\n          const isGreedyLAD = this.openmct.telemetry.greedyLAD();\n          const shouldRemove = !isGreedyLAD || (isGreedyLAD && added.length > 0);\n\n          if (shouldRemove) {\n            discarded = this.boundedTelemetry;\n            this.boundedTelemetry = [];\n            // since it IS strategy latest, we can assume there will be at least 1 datum\n            // unless no data was returned in the first request, we need to account for that\n          } else if (this.boundedTelemetry.length === 1) {\n            this._handleDataOutsideBounds();\n          }\n        }\n      }\n\n      if (discarded.length > 0) {\n        this.emit('remove', discarded);\n      }\n\n      if (added.length > 0) {\n        if (!this.isStrategyLatest) {\n          this.boundedTelemetry = [...this.boundedTelemetry, ...added];\n        } else {\n          this._handleDataInsideBounds();\n\n          added = [added[added.length - 1]];\n          this.boundedTelemetry = added;\n        }\n\n        // Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event\n        this.emit('add', added, [this.boundedTelemetry.length]);\n      }\n    } else {\n      this._handleUserBoundsChange(bounds);\n    }\n  }\n\n  /**\n   * Handle user-initiated bounds changes.\n   * Exits bootstrap mode and reloads data with new bounds.\n   * @private\n   */\n  _handleUserBoundsChange(bounds) {\n    // User changed bounds - exit bootstrap mode if active\n    if (this._isBootstrapMode()) {\n      this._exitBootstrapMode();\n    }\n\n    this.lastBounds = bounds;\n    this._reset();\n  }\n\n  _handleDataInsideBounds() {\n    if (this.dataOutsideTimeBounds) {\n      this.dataOutsideTimeBounds = false;\n      this.emit('dataInsideTimeBounds');\n    }\n  }\n\n  _handleDataOutsideBounds() {\n    if (!this.dataOutsideTimeBounds) {\n      this.dataOutsideTimeBounds = true;\n      this.emit('dataOutsideTimeBounds');\n    }\n  }\n\n  /**\n   * whenever the time system is updated need to update related values in\n   * the Telemetry Collection and reset the telemetry collection\n   *\n   * @param  {TimeSystem} timeSystem - the value of the currently applied\n   * Time System\n   * @private\n   */\n  _setTimeSystem(timeSystem) {\n    let domains = [];\n    let metadataValue = { format: timeSystem.key };\n\n    if (this.metadata) {\n      domains = this.metadata.valuesForHints(['domain']);\n      metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };\n    }\n\n    let domain = domains.find((d) => d.key === timeSystem.key);\n\n    if (domain !== undefined) {\n      // timeKey is used to create a dummy datum used for sorting\n      this.timeKey = domain.source;\n    } else {\n      this.timeKey = undefined;\n\n      // missing objects will never have a domain, if one happens to get through\n      // to this point this warning/notification does not apply\n      if (!this.openmct.objects.isMissing(this.domainObject)) {\n        this._warn(TIMESYSTEM_KEY_WARNING);\n        this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);\n      }\n    }\n\n    let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n    this.parseTime = (datum) => {\n      return valueFormatter.parse(datum);\n    };\n  }\n\n  _setTimeSystemAndFetchData(timeSystem) {\n    this._setTimeSystem(timeSystem);\n    this._reset();\n  }\n\n  _timeModeChanged() {\n    //We're need this so that when the bounds change comes in after this mode change, we can reset and request historic telemetry\n    this.modeChanged = true;\n  }\n\n  /**\n   * Reset the telemetry data of the collection, and re-request\n   * historical telemetry\n   * @private\n   *\n   * @todo handle subscriptions more granually\n   */\n  _reset() {\n    this.boundedTelemetry = [];\n    this.futureBuffer = [];\n\n    this.emit('clear');\n    this._requestHistoricalTelemetry();\n  }\n\n  /**\n   * adds the _bounds callback to the 'boundsChanged' timeAPI listener\n   * @private\n   */\n  _watchBounds() {\n    this.options.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);\n  }\n\n  /**\n   * removes the _bounds callback from the 'boundsChanged' timeAPI listener\n   * @private\n   */\n  _unwatchBounds() {\n    this.options.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);\n  }\n\n  /**\n   * adds the _timeModeChanged callback to the 'modeChanged' timeAPI listener\n   * @private\n   */\n  _watchTimeModeChange() {\n    this.options.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);\n  }\n\n  /**\n   * removes the _timeModeChanged callback from the 'modeChanged' timeAPI listener\n   * @private\n   */\n  _unwatchTimeModeChange() {\n    this.options.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);\n  }\n\n  /**\n   * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener\n   * @private\n   */\n  _watchTimeSystem() {\n    this.options.timeContext.on(\n      TIME_CONTEXT_EVENTS.timeSystemChanged,\n      this._setTimeSystemAndFetchData,\n      this\n    );\n  }\n\n  /**\n   * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener\n   * @private\n   */\n  _unwatchTimeSystem() {\n    this.options.timeContext.off(\n      TIME_CONTEXT_EVENTS.timeSystemChanged,\n      this._setTimeSystemAndFetchData,\n      this\n    );\n  }\n\n  /**\n   * will throw a new Error, for passed in message\n   * @param  {string} message Message describing the error\n   * @private\n   */\n  _error(message) {\n    throw new Error(message);\n  }\n\n  _warn(message) {\n    console.warn(message);\n  }\n}\n"
  },
  {
    "path": "src/api/telemetry/TelemetryCollectionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport { TIMESYSTEM_KEY_WARNING } from './constants.js';\n\ndescribe('Telemetry Collection', () => {\n  let openmct;\n  let mockMetadataProvider;\n  let mockMetadata = {};\n  let domainObject;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.on('start', done);\n\n    domainObject = {\n      identifier: {\n        key: 'a',\n        namespace: 'b'\n      },\n      type: 'sample-type'\n    };\n\n    mockMetadataProvider = {\n      key: 'mockMetadataProvider',\n      supportsMetadata() {\n        return true;\n      },\n      getMetadata() {\n        return mockMetadata;\n      }\n    };\n\n    openmct.telemetry.addProvider(mockMetadataProvider);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState();\n  });\n\n  it('Warns if telemetry metadata does not match the active timesystem', () => {\n    mockMetadata.values = [\n      {\n        key: 'foo',\n        name: 'Bar',\n        hints: {\n          domain: 1\n        }\n      }\n    ];\n\n    const telemetryCollection = openmct.telemetry.requestCollection(domainObject);\n    spyOn(telemetryCollection, '_warn');\n    telemetryCollection.load();\n\n    expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);\n  });\n\n  it('Does not warn if telemetry metadata matches the active timesystem', () => {\n    mockMetadata.values = [\n      {\n        key: 'utc',\n        name: 'Timestamp',\n        format: 'utc',\n        hints: {\n          domain: 1\n        }\n      }\n    ];\n\n    const telemetryCollection = openmct.telemetry.requestCollection(domainObject);\n    spyOn(telemetryCollection, '_warn');\n    telemetryCollection.load();\n\n    expect(telemetryCollection._warn).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/api/telemetry/TelemetryMetadataManager.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\n\nfunction applyReasonableDefaults(valueMetadata, index) {\n  valueMetadata.source = valueMetadata.source || valueMetadata.key;\n  valueMetadata.hints = valueMetadata.hints || {};\n\n  if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {\n    if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {\n      valueMetadata.hints.domain = valueMetadata.hints.x;\n    }\n\n    delete valueMetadata.hints.x;\n  }\n\n  if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {\n    if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {\n      valueMetadata.hints.range = valueMetadata.hints.y;\n    }\n\n    delete valueMetadata.hints.y;\n  }\n\n  if (valueMetadata.format === 'enum') {\n    if (!valueMetadata.values) {\n      valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);\n    }\n\n    if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) {\n      valueMetadata.max = Math.max(valueMetadata.values) + 1;\n    }\n\n    if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) {\n      valueMetadata.min = Math.min(valueMetadata.values) - 1;\n    }\n  }\n\n  if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) {\n    valueMetadata.hints.priority = index;\n  }\n\n  return valueMetadata;\n}\n\n/**\n * Utility class for handling and inspecting telemetry metadata.  Applies\n * reasonable defaults to simplify the task of providing metadata, while\n * also providing methods for interrogating telemetry metadata.\n */\nexport default function TelemetryMetadataManager(metadata) {\n  this.metadata = metadata;\n\n  this.valueMetadatas = this.metadata.values\n    ? this.metadata.values.map(applyReasonableDefaults)\n    : [];\n}\n\n/**\n * Get value metadata for a single key.\n */\nTelemetryMetadataManager.prototype.value = function (key) {\n  return this.valueMetadatas.filter(function (metadata) {\n    return metadata.key === key;\n  })[0];\n};\n\n/**\n * Returns all value metadatas, sorted by priority.\n */\nTelemetryMetadataManager.prototype.values = function () {\n  return this.valuesForHints(['priority']);\n};\n\n/**\n * Get an array of valueMetadatas that possess all hints requested.\n * Array is sorted based on hint priority.\n *\n */\nTelemetryMetadataManager.prototype.valuesForHints = function (hints) {\n  function hasHint(hint) {\n    // eslint-disable-next-line no-invalid-this\n    return Object.prototype.hasOwnProperty.call(this.hints, hint);\n  }\n\n  function hasHints(metadata) {\n    return hints.every(hasHint, metadata);\n  }\n\n  const matchingMetadata = this.valueMetadatas.filter(hasHints);\n  let iteratees = hints.map((hint) => {\n    return (metadata) => {\n      return metadata.hints[hint];\n    };\n  });\n\n  return _.sortBy(matchingMetadata, ...iteratees);\n};\n\n/**\n * check out of a given metadata has array values\n */\nTelemetryMetadataManager.prototype.isArrayValue = function (metadata) {\n  const regex = /\\[\\]$/g;\n  if (!metadata.format && !metadata.formatString) {\n    return false;\n  }\n\n  return (metadata.format || metadata.formatString).match(regex) !== null;\n};\n\nTelemetryMetadataManager.prototype.getFilterableValues = function () {\n  return this.valueMetadatas.filter(\n    (metadatum) => metadatum.filters && metadatum.filters.length > 0\n  );\n};\n\nTelemetryMetadataManager.prototype.getUseToUpdateInPlaceValue = function () {\n  return this.valueMetadatas.find(this.isInPlaceUpdateValue);\n};\n\nTelemetryMetadataManager.prototype.isInPlaceUpdateValue = function (metadatum) {\n  return metadatum.useToUpdateInPlace === true;\n};\n\nTelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {\n  let valueMetadata = this.valuesForHints(['range'])[0];\n\n  if (valueMetadata === undefined) {\n    valueMetadata = this.values().filter((values) => {\n      return !values.hints.domain;\n    })[0];\n  }\n\n  if (valueMetadata === undefined) {\n    valueMetadata = this.values()[0];\n  }\n\n  return valueMetadata;\n};\n"
  },
  {
    "path": "src/api/telemetry/TelemetryRequestInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class TelemetryRequestInterceptorRegistry {\n  /**\n   * A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry\n   * requests.\n   * @interface TelemetryRequestInterceptorRegistry\n   */\n  constructor() {\n    this.interceptors = [];\n  }\n\n  /**\n   * @interface TelemetryRequestInterceptorDef\n   * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request\n   * @property {function} invoke function that transforms the provided request and returns the transformed request\n   * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number\n   */\n\n  /**\n   * Register a new telemetry request interceptor.\n   *\n   * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add\n   * @method addInterceptor\n   */\n  addInterceptor(interceptorDef) {\n    //TODO: sort by priority\n    this.interceptors.push(interceptorDef);\n  }\n\n  /**\n   * Retrieve all interceptors applicable to a domain object/request.\n   * @method getInterceptors\n   * @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request\n   */\n  getInterceptors(identifier, request) {\n    return this.interceptors.filter((interceptor) => {\n      return (\n        typeof interceptor.appliesTo === 'function' && interceptor.appliesTo(identifier, request)\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "src/api/telemetry/TelemetryValueFormatter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\nimport { sprintf } from 'printj';\n\n// TODO: needs reference to formatService;\nexport default class TelemetryValueFormatter {\n  constructor(valueMetadata, formatMap) {\n    this.valueMetadata = valueMetadata;\n    this.formatMap = formatMap;\n    this.valueMetadataFormat = this.getNonArrayValue(valueMetadata.format);\n\n    const numberFormatter = {\n      parse: (x) => Number(x),\n      format: (x) => x,\n      validate: (x) => true\n    };\n\n    // Is there an existing formatter for the format specified? If not, default to number format\n    this.formatter = formatMap.get(this.valueMetadataFormat) || numberFormatter;\n    if (this.valueMetadataFormat === 'enum') {\n      this.formatter = {};\n      this.enumerations = valueMetadata.enumerations.reduce(\n        function (vm, e) {\n          vm.byValue[e.value] = e.string;\n          vm.byString[e.string] = e.value;\n\n          return vm;\n        },\n        {\n          byValue: {},\n          byString: {}\n        }\n      );\n      this.formatter.format = (value) => {\n        if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) {\n          return this.enumerations.byValue[value];\n        }\n\n        return value;\n      };\n      this.formatter.parse = (string) => {\n        if (typeof string === 'string') {\n          if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) {\n            return this.enumerations.byString[string];\n          }\n        }\n\n        return Number(string);\n      };\n    }\n\n    // Check for formatString support once instead of per format call.\n    if (valueMetadata.formatString) {\n      const baseFormat = this.formatter.format;\n      const formatString = this.getNonArrayValue(valueMetadata.formatString);\n      this.formatter.format = function (value) {\n        return sprintf(formatString, baseFormat.call(this, value));\n      };\n    }\n\n    if (this.valueMetadataFormat === 'string') {\n      this.formatter.parse = function (value) {\n        if (value === undefined) {\n          return '';\n        }\n\n        if (typeof value === 'string') {\n          return value;\n        } else {\n          return value.toString();\n        }\n      };\n\n      this.formatter.format = function (value) {\n        return value;\n      };\n\n      this.formatter.validate = function (value) {\n        return typeof value === 'string';\n      };\n    }\n  }\n\n  getNonArrayValue(value) {\n    //metadata format could have array formats ex. string[]/number[]\n    const arrayRegex = /\\[\\]$/g;\n    if (value && value.match(arrayRegex)) {\n      return value.replace(arrayRegex, '');\n    }\n\n    return value;\n  }\n\n  parse(datum) {\n    const isDatumArray = Array.isArray(datum);\n    if (_.isObject(datum)) {\n      const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];\n      if (Array.isArray(objectDatum)) {\n        return objectDatum.map((item) => {\n          return this.formatter.parse(item);\n        });\n      } else {\n        return this.formatter.parse(objectDatum);\n      }\n    }\n\n    return this.formatter.parse(datum);\n  }\n\n  format(datum) {\n    const isDatumArray = Array.isArray(datum);\n    if (_.isObject(datum)) {\n      const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];\n      if (Array.isArray(objectDatum)) {\n        return objectDatum.map((item) => {\n          return this.formatter.format(item);\n        });\n      } else {\n        return this.formatter.format(objectDatum);\n      }\n    }\n\n    return this.formatter.format(datum);\n  }\n}\n"
  },
  {
    "path": "src/api/telemetry/WebSocketWorker.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/* eslint-disable max-classes-per-file */\nexport default function installWorker() {\n  const ONE_SECOND = 1000;\n  const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];\n\n  /**\n   * Provides a WebSocket connection that is resilient to errors and dropouts.\n   * On an error or dropout, will automatically reconnect.\n   *\n   * Additionally, messages will be queued and sent only when WebSocket is\n   * connected meaning that client code does not need to check the state of\n   * the socket before sending.\n   */\n  class ResilientWebSocket extends EventTarget {\n    #webSocket;\n    #isConnected = false;\n    #isConnecting = false;\n    #messageQueue = [];\n    #reconnectTimeoutHandle;\n    #currentWaitIndex = 0;\n    #messageCallbacks = [];\n    #wsUrl;\n    #reconnecting = false;\n    #worker;\n\n    constructor(worker) {\n      super();\n      this.#worker = worker;\n    }\n\n    /**\n     * Establish a new WebSocket connection to the given URL\n     * @param {string} url\n     */\n    connect(url) {\n      this.#wsUrl = url;\n      if (this.#isConnected) {\n        throw new Error('WebSocket already connected');\n      }\n\n      if (this.#isConnecting) {\n        throw new Error('WebSocket connection in progress');\n      }\n\n      this.#isConnecting = true;\n\n      this.#webSocket = new WebSocket(url);\n      //Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.\n      // Playwright does not support forcing websocket state changes.\n      this.#worker.currentWebSocket = this.#webSocket;\n\n      const boundConnected = this.#connected.bind(this);\n      this.#webSocket.addEventListener('open', boundConnected);\n\n      const boundCleanUpAndReconnect = this.#cleanUpAndReconnect.bind(this);\n      this.#webSocket.addEventListener('error', boundCleanUpAndReconnect);\n      this.#webSocket.addEventListener('close', boundCleanUpAndReconnect);\n\n      const boundMessage = this.#message.bind(this);\n      this.#webSocket.addEventListener('message', boundMessage);\n\n      this.addEventListener(\n        'disconnected',\n        () => {\n          this.#webSocket.removeEventListener('open', boundConnected);\n          this.#webSocket.removeEventListener('error', boundCleanUpAndReconnect);\n          this.#webSocket.removeEventListener('close', boundCleanUpAndReconnect);\n        },\n        { once: true }\n      );\n    }\n\n    /**\n     * Register a callback to be invoked when a message is received on the WebSocket.\n     * This paradigm is used instead of the standard EventTarget or EventEmitter approach\n     * for performance reasons.\n     * @param {Function} callback The function to be invoked when a message is received\n     * @returns an unregister function\n     */\n    registerMessageCallback(callback) {\n      this.#messageCallbacks.push(callback);\n\n      return () => {\n        this.#messageCallbacks = this.#messageCallbacks.filter((cb) => cb !== callback);\n      };\n    }\n\n    #connected() {\n      console.info('Websocket connected.');\n      this.#isConnected = true;\n      this.#isConnecting = false;\n      this.#currentWaitIndex = 0;\n\n      if (this.#reconnecting) {\n        this.#worker.postMessage({\n          type: 'reconnected'\n        });\n        this.#reconnecting = false;\n      }\n\n      this.#flushQueue();\n    }\n\n    #cleanUpAndReconnect() {\n      console.warn('Websocket closed. Attempting to reconnect...');\n      this.disconnect();\n      this.#reconnect();\n    }\n\n    #message(event) {\n      this.#messageCallbacks.forEach((callback) => callback(event.data));\n    }\n\n    disconnect() {\n      this.#isConnected = false;\n      this.#isConnecting = false;\n\n      // On WebSocket error, both error callback and close callback are invoked, resulting in\n      // this function being called twice, and websocket being destroyed and deallocated.\n      if (this.#webSocket !== undefined && this.#webSocket !== null) {\n        this.#webSocket.close();\n      }\n\n      this.dispatchEvent(new Event('disconnected'));\n      this.#webSocket = undefined;\n    }\n\n    #reconnect() {\n      if (this.#reconnectTimeoutHandle) {\n        return;\n      }\n      this.#reconnecting = true;\n\n      this.#reconnectTimeoutHandle = setTimeout(() => {\n        this.connect(this.#wsUrl);\n\n        this.#reconnectTimeoutHandle = undefined;\n      }, FALLBACK_AND_WAIT_MS[this.#currentWaitIndex]);\n\n      if (this.#currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) {\n        this.#currentWaitIndex++;\n      }\n    }\n\n    enqueueMessage(message) {\n      this.#messageQueue.push(message);\n      this.#flushQueueIfReady();\n    }\n\n    #flushQueueIfReady() {\n      if (this.#isConnected) {\n        this.#flushQueue();\n      }\n    }\n\n    #flushQueue() {\n      while (this.#messageQueue.length > 0) {\n        if (!this.#isConnected) {\n          break;\n        }\n\n        const message = this.#messageQueue.shift();\n        this.#webSocket.send(message);\n      }\n    }\n  }\n\n  /**\n   * Handles messages over the worker interface, and\n   * sends corresponding WebSocket messages.\n   */\n  class WorkerToWebSocketMessageBroker {\n    #websocket;\n    #messageBatcher;\n\n    constructor(websocket, messageBatcher) {\n      this.#websocket = websocket;\n      this.#messageBatcher = messageBatcher;\n    }\n\n    routeMessageToHandler(message) {\n      const { type } = message.data;\n      switch (type) {\n        case 'connect':\n          this.connect(message);\n          break;\n        case 'disconnect':\n          this.disconnect(message);\n          break;\n        case 'message':\n          this.#websocket.enqueueMessage(message.data.message);\n          break;\n        case 'readyForNextBatch':\n          this.#messageBatcher.readyForNextBatch();\n          break;\n        case 'setMaxBufferSize':\n          this.#messageBatcher.setMaxBufferSize(message.data.maxBufferSize);\n          break;\n        case 'setThrottleRate':\n          this.#messageBatcher.setThrottleRate(message.data.throttleRate);\n          break;\n        case 'setThrottleMessagePattern':\n          this.#messageBatcher.setThrottleMessagePattern(message.data.throttleMessagePattern);\n          break;\n        default:\n          throw new Error(`Unknown message type: ${type}`);\n      }\n    }\n    connect(message) {\n      const { url } = message.data;\n      this.#websocket.connect(url);\n    }\n    disconnect() {\n      this.#websocket.disconnect();\n    }\n  }\n\n  /**\n   * Responsible for buffering messages\n   */\n  class MessageBuffer {\n    #buffer;\n    #currentBufferLength;\n    #dropped;\n    #maxBufferSize;\n    #readyForNextBatch;\n    #worker;\n    #throttledSendNextBatch;\n    #throttleMessagePattern;\n\n    constructor(worker) {\n      // No dropping telemetry unless we're explicitly told to.\n      this.#maxBufferSize = Number.POSITIVE_INFINITY;\n      this.#readyForNextBatch = false;\n      this.#worker = worker;\n      this.#resetBatch();\n      this.setThrottleRate(ONE_SECOND);\n    }\n    #resetBatch() {\n      //this.#batch = {};\n      this.#buffer = [];\n      this.#currentBufferLength = 0;\n      this.#dropped = false;\n    }\n\n    addMessageToBuffer(message) {\n      this.#buffer.push(message);\n      this.#currentBufferLength += message.length;\n\n      for (\n        let i = 0;\n        this.#currentBufferLength > this.#maxBufferSize && i < this.#buffer.length;\n        i++\n      ) {\n        const messageToConsider = this.#buffer[i];\n        if (this.#shouldThrottle(messageToConsider)) {\n          this.#buffer.splice(i, 1);\n          this.#currentBufferLength -= messageToConsider.length;\n          this.#dropped = true;\n        }\n      }\n\n      if (this.#readyForNextBatch) {\n        this.#throttledSendNextBatch();\n      }\n    }\n\n    #shouldThrottle(message) {\n      return (\n        this.#throttleMessagePattern !== undefined && this.#throttleMessagePattern.test(message)\n      );\n    }\n\n    setMaxBufferSize(maxBufferSize) {\n      this.#maxBufferSize = maxBufferSize;\n    }\n    setThrottleRate(throttleRate) {\n      this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), throttleRate);\n    }\n    /**\n     * Indicates that client code is ready to receive the next batch of\n     * messages. If a batch is available, it will be immediately sent.\n     * Otherwise a flag will be set to send the next batch as soon as\n     * any new data is available.\n     */\n    readyForNextBatch() {\n      if (this.#hasData()) {\n        this.#throttledSendNextBatch();\n      } else {\n        this.#readyForNextBatch = true;\n      }\n    }\n    #sendNextBatch() {\n      const buffer = this.#buffer;\n      const dropped = this.#dropped;\n      const currentBufferLength = this.#currentBufferLength;\n\n      this.#resetBatch();\n      this.#worker.postMessage({\n        type: 'batch',\n        dropped,\n        currentBufferLength: currentBufferLength,\n        maxBufferSize: this.#maxBufferSize,\n        batch: buffer\n      });\n\n      this.#readyForNextBatch = false;\n    }\n    #hasData() {\n      return this.#currentBufferLength > 0;\n    }\n    setThrottleMessagePattern(priorityMessagePattern) {\n      this.#throttleMessagePattern = new RegExp(priorityMessagePattern, 'm');\n    }\n  }\n\n  function throttle(callback, wait) {\n    let last = 0;\n    let throttling = false;\n\n    return function (...args) {\n      if (throttling) {\n        return;\n      }\n\n      const now = performance.now();\n      const timeSinceLast = now - last;\n\n      if (timeSinceLast >= wait) {\n        last = now;\n        callback(...args);\n      } else if (!throttling) {\n        throttling = true;\n\n        setTimeout(() => {\n          last = performance.now();\n          throttling = false;\n          callback(...args);\n        }, wait - timeSinceLast);\n      }\n    };\n  }\n\n  const websocket = new ResilientWebSocket(self);\n  const messageBuffer = new MessageBuffer(self);\n  const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBuffer);\n\n  self.addEventListener('message', (message) => {\n    workerBroker.routeMessageToHandler(message);\n  });\n  websocket.registerMessageCallback((data) => {\n    messageBuffer.addMessageToBuffer(data);\n  });\n\n  self.websocketInstance = websocket;\n}\n"
  },
  {
    "path": "src/api/telemetry/constants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const TIMESYSTEM_KEY_WARNING =\n  'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';\nexport const TIMESYSTEM_KEY_NOTIFICATION =\n  'Telemetry metadata does not match the active time system.';\nexport const LOADED_ERROR = 'Telemetry Collection has already been loaded.';\n"
  },
  {
    "path": "src/api/time/GlobalTimeContext.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport TimeContext from './TimeContext.js';\n\n/**\n * @typedef {import('./TimeAPI').TimeConductorBounds} TimeConductorBounds\n */\n\n/**\n * The GlobalContext handles getting and setting time of the openmct application in general.\n * Views will use this context unless they specify an alternate/independent time context\n */\nclass GlobalTimeContext extends TimeContext {\n  constructor() {\n    super();\n\n    //The Time Of Interest\n    this.toi = undefined;\n  }\n\n  /**\n   * Get or set the start and end time of the time conductor. Basic validation\n   * of bounds is performed.\n   *\n   * @param {TimeConductorBounds} newBounds\n   * @throws {Error} Validation error\n   * @returns {TimeConductorBounds}\n   * @override\n   */\n  bounds(newBounds) {\n    if (arguments.length > 0) {\n      super.bounds.call(this, ...arguments);\n      // If a bounds change results in a TOI outside of the current\n      // bounds, unset it\n      if (this.toi < newBounds.start || this.toi > newBounds.end) {\n        this.timeOfInterest(undefined);\n      }\n    }\n\n    //Return a copy to prevent direct mutation of time conductor bounds.\n    return JSON.parse(JSON.stringify(this.boundsVal));\n  }\n\n  /**\n   * Update bounds based on provided time and current offsets\n   * @param {number} timestamp A time from which bounds will be calculated\n   * using current offsets.\n   * @override\n   */\n  tick(timestamp) {\n    super.tick.call(this, ...arguments);\n\n    // If a bounds change results in a TOI outside of the current\n    // bounds, unset it\n    if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {\n      this.timeOfInterest(undefined);\n    }\n  }\n\n  /**\n   * Get or set the Time of Interest. The Time of Interest is a single point\n   * in time, and constitutes the temporal focus of application views. It can\n   * be manipulated by the user from the time conductor or from other views.\n   * The time of interest can effectively be unset by assigning a value of\n   * 'undefined'.\n   * @param newTOI\n   * @returns {number} the current time of interest\n   */\n  timeOfInterest(newTOI) {\n    if (arguments.length > 0) {\n      this.toi = newTOI;\n      /**\n       * The Time of Interest has moved.\n       * @event timeOfInterest\n       * @property {number} timeOfInterest time of interest\n       */\n      this.emit('timeOfInterest', this.toi);\n    }\n\n    return this.toi;\n  }\n}\n\nexport default GlobalTimeContext;\n"
  },
  {
    "path": "src/api/time/IndependentTimeContext.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';\nimport TimeContext from './TimeContext.js';\n\n/**\n * @typedef {import('./TimeAPI.js').default} TimeAPI\n * @typedef {import('./GlobalTimeContext.js').default} GlobalTimeContext\n * @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem\n * @typedef {import('./TimeContext.js').Mode} Mode\n * @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds\n * @typedef {import('./TimeAPI.js').ClockOffsets} ClockOffsets\n */\n\n/**\n * The IndependentTimeContext handles getting and setting time of the openmct application in general.\n * Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.\n */\nclass IndependentTimeContext extends TimeContext {\n  /**\n   * @param {import('openmct').OpenMCT} openmct - The Open MCT application instance.\n   * @param {TimeAPI & GlobalTimeContext} globalTimeContext - The global time context.\n   * @param {import('openmct').ObjectPath} objectPath - The path of objects.\n   */\n  constructor(openmct, globalTimeContext, objectPath) {\n    super();\n    /** @type {any} */\n    this.openmct = openmct;\n    /** @type {Function[]} */\n    this.unlisteners = [];\n    /** @type {TimeAPI & GlobalTimeContext | undefined} */\n    this.globalTimeContext = globalTimeContext;\n    /** @type {TimeAPI & GlobalTimeContext | undefined} */\n    this.upstreamTimeContext = this.globalTimeContext;\n    /** @type {Array<any>} */\n    this.objectPath = objectPath;\n    this.refreshContext = this.refreshContext.bind(this);\n    this.resetContext = this.resetContext.bind(this);\n    this.removeIndependentContext = this.removeIndependentContext.bind(this);\n\n    this.refreshContext();\n\n    this.globalTimeContext.on('refreshContext', this.refreshContext);\n    this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);\n  }\n\n  /**\n   * @deprecated\n   * @override\n   */\n  bounds() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.bounds(...arguments);\n    } else {\n      return super.bounds(...arguments);\n    }\n  }\n\n  /**\n   * @override\n   */\n  getBounds() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.getBounds();\n    } else {\n      return super.getBounds();\n    }\n  }\n\n  /**\n   * @override\n   */\n  setBounds() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.setBounds(...arguments);\n    } else {\n      return super.setBounds(...arguments);\n    }\n  }\n\n  /**\n   * @override\n   */\n  tick() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.tick(...arguments);\n    } else {\n      return super.tick(...arguments);\n    }\n  }\n\n  /**\n   * @override\n   */\n  clockOffsets() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.clockOffsets(...arguments);\n    } else {\n      return super.clockOffsets(...arguments);\n    }\n  }\n\n  /**\n   * @override\n   */\n  getClockOffsets() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.getClockOffsets();\n    } else {\n      return super.getClockOffsets();\n    }\n  }\n\n  /**\n   * @override\n   */\n  setClockOffsets() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.setClockOffsets(...arguments);\n    } else {\n      return super.setClockOffsets(...arguments);\n    }\n  }\n\n  /**\n   *\n   * @param {number} newTOI\n   * @returns {number}\n   */\n  timeOfInterest(newTOI) {\n    return this.globalTimeContext.timeOfInterest(...arguments);\n  }\n\n  /**\n   *\n   * @param {TimeSystem | string} timeSystemOrKey\n   * @param {TimeConductorBounds} bounds\n   * @returns {TimeSystem}\n   * @override\n   */\n  timeSystem(timeSystemOrKey, bounds) {\n    return this.globalTimeContext.setTimeSystem(...arguments);\n  }\n\n  /**\n   * Get the time system of the TimeAPI.\n   * @returns {TimeSystem} The currently applied time system\n   * @method getTimeSystem\n   * @override\n   */\n  getTimeSystem() {\n    return this.globalTimeContext.getTimeSystem();\n  }\n\n  /**\n   * Set the active clock. Tick source will be immediately subscribed to\n   * and ticking will begin. Offsets from 'now' must also be provided.\n   *\n   * @param {Clock || string} keyOrClock The clock to activate, or its key\n   * @param {ClockOffsets} offsets on each tick these will be used to calculate\n   * the start and end bounds. This maintains a sliding time window of a fixed\n   * width that automatically updates.\n   * @fires module:openmct.TimeAPI~clock\n   * @return {Clock} the currently active clock;\n   */\n  clock(keyOrClock, offsets) {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.clock(...arguments);\n    }\n\n    if (arguments.length === 2) {\n      let clock;\n\n      if (typeof keyOrClock === 'string') {\n        clock = this.globalTimeContext.clocks.get(keyOrClock);\n        if (clock === undefined) {\n          throw \"Unknown clock '\" + keyOrClock + \"'. Has it been registered with 'addClock'?\";\n        }\n      } else if (typeof keyOrClock === 'object') {\n        clock = keyOrClock;\n        if (!this.globalTimeContext.clocks.has(clock.key)) {\n          throw \"Unknown clock '\" + keyOrClock.key + \"'. Has it been registered with 'addClock'?\";\n        }\n      }\n\n      const previousClock = this.activeClock;\n      if (previousClock !== undefined) {\n        previousClock.off('tick', this.tick);\n      }\n\n      this.activeClock = clock;\n\n      /**\n       * The active clock has changed.\n       * @event clock\n       * @property {Clock} clock The newly activated clock, or undefined\n       * if the system is no longer following a clock source\n       */\n      this.emit('clock', this.activeClock);\n      this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);\n\n      if (this.activeClock !== undefined) {\n        //set the mode here or isRealtime will be false even if we're in clock mode\n        this.setMode(REALTIME_MODE_KEY);\n\n        this.clockOffsets(offsets);\n        this.activeClock.on('tick', this.tick);\n      }\n    } else if (arguments.length === 1) {\n      throw 'When setting the clock, clock offsets must also be provided';\n    }\n\n    return this.activeClock;\n  }\n\n  /**\n   * Get the active clock.\n   * @return {Clock} the currently active clock;\n   */\n  getClock() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.getClock();\n    }\n\n    return this.activeClock;\n  }\n\n  /**\n   * Set the active clock. Tick source will be immediately subscribed to\n   * and the currently ticking will begin.\n   * Offsets from 'now', if provided, will be used to set realtime mode offsets\n   *\n   * @param {Clock || string} keyOrClock The clock to activate, or its key\n   * @fires module:openmct.TimeAPI~clock\n   * @return {Clock} the currently active clock;\n   */\n  setClock(keyOrClock) {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.setClock(...arguments);\n    }\n\n    let clock;\n\n    if (typeof keyOrClock === 'string') {\n      clock = this.globalTimeContext.clocks.get(keyOrClock);\n      if (clock === undefined) {\n        throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;\n      }\n    } else if (typeof keyOrClock === 'object') {\n      clock = keyOrClock;\n      if (!this.globalTimeContext.clocks.has(clock.key)) {\n        throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;\n      }\n    }\n\n    const previousClock = this.activeClock;\n    if (previousClock) {\n      previousClock.off('tick', this.tick);\n    }\n\n    this.activeClock = clock;\n    this.activeClock.on('tick', this.tick);\n\n    /**\n     * The active clock has changed.\n     * @event clock\n     * @property {Clock} clock The newly activated clock, or undefined\n     * if the system is no longer following a clock source\n     */\n    this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);\n\n    return this.activeClock;\n  }\n\n  /**\n   * Get the current mode.\n   * @return {Mode} the current mode;\n   * @override\n   */\n  getMode() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.getMode();\n    }\n\n    return this.mode;\n  }\n\n  /**\n   * Set the mode to either fixed or realtime.\n   *\n   * @param {Mode} mode The mode to activate\n   * @param {TimeConductorBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width\n   * @return {Mode | undefined} the currently active mode;\n   */\n  setMode(mode, offsetsOrBounds) {\n    if (!mode) {\n      return;\n    }\n\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.setMode(...arguments);\n    }\n\n    if (mode === MODES.realtime) {\n      // TODO: This should probably happen up front in creating an independent time context\n      // TODO: not just in time every time setMode is called\n      if (this.activeClock === undefined) {\n        this.activeClock = this.globalTimeContext.getClock();\n        this.emit('clock', this.activeClock);\n        this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);\n        this.activeClock.on('tick', this.tick);\n      }\n\n      if (this.activeClock === undefined) {\n        throw `Unknown clock. Has a clock been registered with 'addClock'?`;\n      }\n    }\n\n    if (mode !== this.mode) {\n      this.mode = mode;\n      /**\n       * The active mode has changed.\n       * @event modeChanged\n       * @property {Mode} mode The newly activated mode\n       */\n      this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));\n    }\n\n    //We are also going to set bounds here\n    if (offsetsOrBounds !== undefined) {\n      if (this.mode === REALTIME_MODE_KEY) {\n        this.setClockOffsets(offsetsOrBounds);\n      } else {\n        this.setBounds(offsetsOrBounds);\n      }\n    }\n\n    return this.mode;\n  }\n\n  /**\n   * @returns {boolean}\n   * @override\n   */\n  isRealTime() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.isRealTime(...arguments);\n    } else {\n      return super.isRealTime(...arguments);\n    }\n  }\n\n  /**\n   * @returns {boolean}\n   * @override\n   */\n  isFixed() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.isFixed(...arguments);\n    } else {\n      return super.isFixed(...arguments);\n    }\n  }\n\n  /**\n   * @returns {number}\n   * @override\n   */\n  now() {\n    if (this.upstreamTimeContext) {\n      return this.upstreamTimeContext.now(...arguments);\n    } else {\n      return super.now(...arguments);\n    }\n  }\n\n  /**\n   * Causes this time context to follow another time context (either the global context, or another upstream time context)\n   * This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.\n   */\n  followTimeContext() {\n    this.stopFollowingTimeContext();\n    if (this.upstreamTimeContext) {\n      Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {\n        const thisTimeContext = this;\n        this.upstreamTimeContext.on(eventName, passthrough);\n        this.unlisteners.push(() => {\n          thisTimeContext.upstreamTimeContext.off(eventName, passthrough);\n        });\n        function passthrough() {\n          thisTimeContext.emit(eventName, ...arguments);\n        }\n      });\n    }\n  }\n\n  /**\n   * Stops following any upstream time context\n   */\n  stopFollowingTimeContext() {\n    this.unlisteners.forEach((unlisten) => unlisten());\n    this.unlisteners = [];\n  }\n\n  /**\n   * Reset the time context from the global time context\n   */\n  resetContext() {\n    if (this.upstreamTimeContext) {\n      this.stopFollowingTimeContext();\n      this.upstreamTimeContext = undefined;\n    }\n  }\n\n  /**\n   * Refresh the time context, following any upstream time contexts as necessary\n   * @param {string} [viewKey] The key of the view to refresh\n   */\n  refreshContext(viewKey) {\n    const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);\n    if (viewKey && key === viewKey) {\n      return;\n    }\n\n    //this is necessary as the upstream context gets reassigned after this\n    this.stopFollowingTimeContext();\n\n    this.upstreamTimeContext = this.getUpstreamContext();\n    this.followTimeContext();\n\n    // Emit bounds so that views that are changing context get the upstream bounds\n    this.emit('bounds', this.getBounds());\n    this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());\n    // Also emit the mode in case it's different from previous time context\n    if (this.getMode()) {\n      this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));\n    }\n  }\n\n  /**\n   * @returns {boolean} True if this time context has an independent context, false otherwise\n   */\n  hasOwnContext() {\n    return this.upstreamTimeContext === undefined;\n  }\n\n  /**\n   * Get the upstream time context of this time context\n   * @returns {TimeAPI & GlobalTimeContext | undefined} The upstream time context\n   */\n  getUpstreamContext() {\n    // If a view has an independent context, don't return an upstream context\n    // Be aware that when a new independent time context is created, we assign the global context as default\n    if (this.hasOwnContext()) {\n      return undefined;\n    }\n\n    let timeContext = this.globalTimeContext;\n    this.objectPath.some((item, index) => {\n      const key = this.openmct.objects.makeKeyString(item.identifier);\n      // we're only interested in parents, not self, so index > 0\n      const itemContext = this.globalTimeContext.independentContexts.get(key);\n      if (index > 0 && itemContext && itemContext.hasOwnContext()) {\n        //upstream time context\n        timeContext = itemContext;\n\n        return true;\n      }\n\n      return false;\n    });\n\n    return timeContext;\n  }\n\n  /**\n   * Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context)\n   * This needs to be separate from refreshContext\n   */\n  removeIndependentContext(viewKey) {\n    const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);\n    if (viewKey && key === viewKey) {\n      //this is necessary as the upstream context gets reassigned after this\n      this.stopFollowingTimeContext();\n      if (this.activeClock !== undefined) {\n        this.activeClock.off('tick', this.tick);\n      }\n\n      let timeContext = this.globalTimeContext;\n\n      this.objectPath.some((item, index) => {\n        const objectKey = this.openmct.objects.makeKeyString(item.identifier);\n        // we're only interested in any parents, not self, so index > 0\n        const itemContext = this.globalTimeContext.independentContexts.get(objectKey);\n        if (index > 0 && itemContext && itemContext.hasOwnContext()) {\n          //upstream time context\n          timeContext = itemContext;\n\n          return true;\n        }\n\n        return false;\n      });\n\n      this.upstreamTimeContext = timeContext;\n\n      this.followTimeContext();\n\n      // Emit bounds so that views that are changing context get the upstream bounds\n      this.emit('bounds', this.getBounds());\n      this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());\n      // Also emit the mode in case it's different from the global time context\n      if (this.getMode()) {\n        this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));\n      }\n      // now that the view's context is set, tell others to check theirs in case they were following this view's context.\n      this.globalTimeContext.emit('refreshContext', viewKey);\n    }\n  }\n\n  #copy(object) {\n    return JSON.parse(JSON.stringify(object));\n  }\n}\n\nexport default IndependentTimeContext;\n"
  },
  {
    "path": "src/api/time/TimeAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';\nimport IndependentTimeContext from '@/api/time/IndependentTimeContext';\n\nimport { TIME_CONTEXT_EVENTS } from './constants';\nimport GlobalTimeContext from './GlobalTimeContext.js';\n\n/**\n * @typedef {import('./TimeContext.js').default} TimeContext\n */\n\n/**\n * @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds\n */\n\n/**\n * @typedef {import('./TimeContext.js').ClockOffsets} ClockOffsets\n */\n\n/**\n * A TimeSystem provides meaning to the values returned by the TimeAPI. Open\n * MCT supports multiple different types of time values, although all are\n * intrinsically represented by numbers, the meaning of those numbers can\n * differ depending on context.\n *\n * A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},\n * which represents integer values as ms in the Unix epoch. An example of\n * another time system might be \"sols\" for a Martian mission. TimeSystems do\n * not address the issue of converting between time systems.\n *\n * @typedef {Object} TimeSystem\n * @property {string} key A unique identifier\n * @property {string} name A human-readable descriptor\n * @property {string} [cssClass] Specify a css class defining an icon for\n * this time system. This will be visible next to the time system in the\n * menu in the Time Conductor\n * @property {string} timeFormat The key of a format to use when displaying\n * discrete timestamps from this time system\n * @property {string} [durationFormat] The key of a format to use when\n * displaying a duration or relative span of time in this time system.\n */\n\n/**\n * The public API for setting and querying the temporal state of the\n * application. The concept of time is integral to Open MCT, and at least\n * one {@link TimeSystem}, as well as some default time bounds must be\n * registered and enabled via {@link TimeAPI.addTimeSystem} and\n * {@link TimeAPI.timeSystem} respectively for Open MCT to work.\n *\n * Time-sensitive views will typically respond to changes to bounds or other\n * properties of the time conductor and update the data displayed based on\n * the temporal state of the application. The current time bounds are also\n * used in queries for historical data.\n *\n * The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are\n * fired when properties of the time conductor change, which are documented\n * below.\n *\n * @class\n * @extends {GlobalTimeContext}\n */\nclass TimeAPI extends GlobalTimeContext {\n  constructor(openmct) {\n    super();\n    this.openmct = openmct;\n    this.independentContexts = new Map();\n  }\n\n  /**\n   * Register a new time system. Once registered it can activated using\n   * {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).\n   * @param {TimeSystem} timeSystem A time system object.\n   */\n  addTimeSystem(timeSystem) {\n    this.timeSystems.set(timeSystem.key, timeSystem);\n  }\n\n  /**\n   * @returns {TimeSystem[]}\n   */\n  getAllTimeSystems() {\n    return Array.from(this.timeSystems.values());\n  }\n\n  /**\n   * Clocks provide a timing source that is used to\n   * automatically update the time bounds of the data displayed in Open MCT.\n   *\n   * @typedef {Object} Clock\n   * @property {string} key A unique identifier\n   * @property {string} name A human-readable name. The name will be used to\n   * represent this clock in the Time Conductor UI\n   * @property {string} description A longer description, ideally identifying\n   * what the clock ticks on.\n   * @property {function} currentValue Returns the last value generated by a tick, or a default value\n   * if no ticking has yet occurred\n   * @see {LocalClock}\n   */\n\n  /**\n   * Register a new Clock.\n   * @param {Clock} clock\n   */\n  addClock(clock) {\n    this.clocks.set(clock.key, clock);\n  }\n\n  /**\n   * @returns {Clock[]}\n   */\n  getAllClocks() {\n    return Array.from(this.clocks.values());\n  }\n\n  /**\n   * Get or set an independent time context which follows the TimeAPI timeSystem,\n   * but with different bounds for a given domain object\n   * @param {string} keyString The keyString identifier of the domain object these offsets are set for\n   * @param {TimeConductorBounds | ClockOffsets} boundsOrOffsets either bounds if in fixed mode, or offsets if in realtime mode\n   * @param {string} clockKey the key for the real time clock to use\n   */\n  addIndependentContext(keyString, boundsOrOffsets, clockKey) {\n    let timeContext = this.getIndependentContext(keyString);\n\n    //stop following upstream time context since the view has its own\n    timeContext.resetContext();\n\n    if (clockKey) {\n      timeContext.setClock(clockKey);\n      timeContext.setMode(REALTIME_MODE_KEY, boundsOrOffsets);\n    } else {\n      timeContext.setMode(FIXED_MODE_KEY, boundsOrOffsets);\n    }\n\n    // Also emit the mode in case it's different from the previous time context\n    timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));\n\n    // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context\n    this.emit('refreshContext', keyString);\n\n    return () => {\n      //follow any upstream time context\n      this.emit('removeOwnContext', keyString);\n    };\n  }\n\n  /**\n   * Get the independent time context which follows the TimeAPI timeSystem,\n   * but with different offsets.\n   * @param {string} key The identifier key of the domain object these offsets\n   * @returns {IndependentTimeContext} The independent time context\n   */\n  getIndependentContext(key) {\n    return this.independentContexts.get(key);\n  }\n\n  /**\n   * Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.\n   * Otherwise, the global time context will be returned.\n   * @param {Array} objectPath The view's objectPath\n   * @returns {TimeContext | GlobalTimeContext} The time context\n   */\n  getContextForView(objectPath) {\n    if (!objectPath || !Array.isArray(objectPath)) {\n      throw new Error('No objectPath provided');\n    }\n\n    const viewKey =\n      objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);\n\n    if (!viewKey) {\n      // Return the global time context\n      return this;\n    }\n\n    let viewTimeContext = this.getIndependentContext(viewKey);\n\n    if (!viewTimeContext) {\n      // If the context doesn't exist yet, create it.\n      viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);\n      this.independentContexts.set(viewKey, viewTimeContext);\n    } else {\n      // If it already exists, compare the objectPath to see if it needs to be updated.\n      const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);\n      const newPath = this.openmct.objects.getRelativePath(objectPath);\n\n      if (currentPath !== newPath) {\n        // If the path has changed, update the context.\n        this.independentContexts.delete(viewKey);\n        viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);\n        this.independentContexts.set(viewKey, viewTimeContext);\n      }\n    }\n\n    return viewTimeContext;\n  }\n}\n\nexport default TimeAPI;\n"
  },
  {
    "path": "src/api/time/TimeAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct } from 'utils/testing';\n\nimport TimeAPI from './TimeAPI.js';\n\ndescribe('The Time API', function () {\n  let api;\n  let timeSystemKey;\n  let timeSystem;\n  let clockKey;\n  let clock;\n  let bounds;\n  let eventListener;\n  let toi;\n  let openmct;\n\n  beforeEach(function () {\n    openmct = createOpenMct();\n    api = new TimeAPI(openmct);\n    timeSystemKey = 'timeSystemKey';\n    timeSystem = { key: timeSystemKey };\n    clockKey = 'someClockKey';\n    clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n    clock.currentValue.and.returnValue(100);\n    clock.key = clockKey;\n    bounds = {\n      start: 0,\n      end: 1\n    };\n    eventListener = jasmine.createSpy('eventListener');\n    toi = 111;\n  });\n\n  it('Supports setting and querying of time of interest', function () {\n    expect(api.timeOfInterest()).not.toBe(toi);\n    api.timeOfInterest(toi);\n    expect(api.timeOfInterest()).toBe(toi);\n  });\n\n  it('[Legacy TimeAPI]: Allows setting of valid bounds', function () {\n    bounds = {\n      start: 0,\n      end: 1\n    };\n    expect(api.bounds()).not.toBe(bounds);\n    expect(api.bounds.bind(api, bounds)).not.toThrow();\n    expect(api.bounds()).toEqual(bounds);\n  });\n\n  it('Allows setting of valid bounds', function () {\n    bounds = {\n      start: 0,\n      end: 1\n    };\n    expect(api.getBounds()).not.toBe(bounds);\n    expect(api.setBounds.bind(api, bounds)).not.toThrow();\n    expect(api.getBounds()).toEqual(bounds);\n  });\n\n  it('[Legacy TimeAPI]: Disallows setting of invalid bounds', function () {\n    bounds = {\n      start: 1,\n      end: 0\n    };\n    expect(api.bounds()).not.toEqual(bounds);\n    expect(api.bounds.bind(api, bounds)).toThrow();\n    expect(api.bounds()).not.toEqual(bounds);\n\n    bounds = { start: 1 };\n    expect(api.bounds()).not.toEqual(bounds);\n    expect(api.bounds.bind(api, bounds)).toThrow();\n    expect(api.bounds()).not.toEqual(bounds);\n  });\n\n  it('Disallows setting of invalid bounds', function () {\n    bounds = {\n      start: 1,\n      end: 0\n    };\n    expect(api.getBounds()).not.toEqual(bounds);\n    expect(api.setBounds.bind(api, bounds)).toThrow();\n    expect(api.getBounds()).not.toEqual(bounds);\n\n    bounds = { start: 1 };\n    expect(api.getBounds()).not.toEqual(bounds);\n    expect(api.setBounds.bind(api, bounds)).toThrow();\n    expect(api.getBounds()).not.toEqual(bounds);\n  });\n\n  it('[Legacy TimeAPI]: Allows setting of previously registered time system with bounds', function () {\n    api.addTimeSystem(timeSystem);\n    expect(api.timeSystem()).not.toBe(timeSystem);\n    expect(function () {\n      api.timeSystem(timeSystem, bounds);\n    }).not.toThrow();\n    expect(api.timeSystem()).toEqual(timeSystem);\n  });\n\n  it('Allows setting of previously registered time system with bounds', function () {\n    api.addTimeSystem(timeSystem);\n    expect(api.getTimeSystem()).not.toBe(timeSystem);\n    expect(function () {\n      api.setTimeSystem(timeSystem, bounds);\n    }).not.toThrow();\n    expect(api.getTimeSystem()).toEqual(timeSystem);\n  });\n\n  it('[Legacy TimeAPI]: Disallows setting of time system without bounds', function () {\n    api.addTimeSystem(timeSystem);\n    expect(api.timeSystem()).not.toBe(timeSystem);\n    expect(function () {\n      api.timeSystem(timeSystemKey);\n    }).toThrow();\n    expect(api.timeSystem()).not.toBe(timeSystem);\n  });\n\n  it('Allows setting of time system without bounds', function () {\n    api.addTimeSystem(timeSystem);\n    expect(api.getTimeSystem()).not.toBe(timeSystem);\n    expect(function () {\n      api.setTimeSystem(timeSystemKey);\n    }).not.toThrow();\n    expect(api.getTimeSystem()).not.toBe(timeSystem);\n  });\n\n  it('Disallows setting of invalid time system', function () {\n    expect(function () {\n      api.setTimeSystem();\n    }).toThrow();\n    expect(function () {\n      api.setTimeSystem('invalidTimeSystemKey');\n    }).toThrow();\n    expect(function () {\n      api.setTimeSystem({\n        key: 'invalidTimeSystemKey'\n      });\n    }).toThrow();\n    expect(function () {\n      api.setTimeSystem(42);\n    }).toThrow();\n  });\n\n  it('allows setting of timesystem without bounds with clock', function () {\n    api.addTimeSystem(timeSystem);\n    api.addClock(clock);\n    api.clock(clockKey, {\n      start: 0,\n      end: 1\n    });\n    expect(api.timeSystem()).not.toBe(timeSystem);\n    expect(function () {\n      api.timeSystem(timeSystemKey);\n    }).not.toThrow();\n    expect(api.timeSystem()).toEqual(timeSystem);\n  });\n\n  it('Emits a legacy event when time system changes', function () {\n    api.addTimeSystem(timeSystem);\n    expect(eventListener).not.toHaveBeenCalled();\n    api.on('timeSystem', eventListener);\n    api.timeSystem(timeSystemKey, bounds);\n    expect(eventListener).toHaveBeenCalledWith(timeSystem);\n  });\n\n  it('Emits an event when time system changes', function () {\n    api.addTimeSystem(timeSystem);\n    expect(eventListener).not.toHaveBeenCalled();\n    api.on('timeSystemChanged', eventListener);\n    api.timeSystem(timeSystemKey, bounds);\n    expect(eventListener).toHaveBeenCalledWith(timeSystem);\n  });\n\n  it('Emits an event when time of interest changes', function () {\n    expect(eventListener).not.toHaveBeenCalled();\n    api.on('timeOfInterest', eventListener);\n    api.timeOfInterest(toi);\n    expect(eventListener).toHaveBeenCalledWith(toi);\n  });\n\n  it('Emits a legacy event when bounds change', function () {\n    expect(eventListener).not.toHaveBeenCalled();\n    api.on('bounds', eventListener);\n    api.bounds(bounds);\n    expect(eventListener).toHaveBeenCalledWith(bounds, false);\n  });\n\n  it('Emits an event when bounds change', function () {\n    expect(eventListener).not.toHaveBeenCalled();\n    api.on('boundsChanged', eventListener);\n    api.bounds(bounds);\n    expect(eventListener).toHaveBeenCalledWith(bounds, false);\n  });\n\n  it('If bounds are set and TOI lies inside them, do not change TOI', function () {\n    api.timeOfInterest(6);\n    api.bounds({\n      start: 1,\n      end: 10\n    });\n    expect(api.timeOfInterest()).toEqual(6);\n  });\n\n  it('If bounds are set and TOI lies outside them, reset TOI', function () {\n    api.timeOfInterest(11);\n    api.bounds({\n      start: 1,\n      end: 10\n    });\n    expect(api.timeOfInterest()).toBeUndefined();\n  });\n\n  it('Maintains delta during tick', function () {\n    const initialBounds = { start: 100, end: 200 };\n    api.bounds(initialBounds);\n    const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);\n    mockTickSource.key = 'mct';\n    mockTickSource.currentValue.and.returnValue(150);\n    api.addClock(mockTickSource);\n    api.clock('mct', { start: 0, end: 100 });\n\n    // Simulate a tick event\n    const tickCallback = mockTickSource.on.calls.mostRecent().args[1];\n    tickCallback(150);\n\n    const newBounds = api.bounds();\n    expect(newBounds.end - newBounds.start).toEqual(initialBounds.end - initialBounds.start);\n  });\n\n  it('Allows registered time system to be activated', function () {\n    api.addClock(clock);\n    api.clock(clockKey, { start: 0, end: 100 });\n    api.addTimeSystem(timeSystem);\n    api.timeSystem(timeSystemKey);\n    expect(api.timeSystem().key).toEqual(timeSystemKey);\n  });\n\n  it('Allows a registered tick source to be activated', function () {\n    const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);\n    mockTickSource.key = 'mockTickSource';\n    mockTickSource.currentValue.and.returnValue(50);\n    api.addClock(mockTickSource);\n    api.clock(mockTickSource.key, { start: 0, end: 100 });\n\n    expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));\n  });\n\n  describe(' when enabling a tick source', function () {\n    let mockTickSource;\n    let anotherMockTickSource;\n    const mockOffsets = {\n      start: 0,\n      end: 1\n    };\n\n    beforeEach(function () {\n      mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n      mockTickSource.currentValue.and.returnValue(10);\n      mockTickSource.key = 'mts';\n\n      anotherMockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n      anotherMockTickSource.key = 'amts';\n      anotherMockTickSource.currentValue.and.returnValue(10);\n\n      api.addClock(mockTickSource);\n      api.addClock(anotherMockTickSource);\n    });\n\n    it('[Legacy TimeAPI]: sets bounds based on current value', function () {\n      api.clock('mts', mockOffsets);\n      expect(api.bounds()).toEqual({\n        start: 10,\n        end: 11\n      });\n    });\n\n    it('does not set bounds based on current value', function () {\n      api.setClock('mts');\n      expect(api.getBounds()).toEqual({});\n    });\n\n    it('does not set invalid clock', function () {\n      expect(function () {\n        api.setClock();\n      }).toThrow();\n      expect(function () {\n        api.setClock({});\n      }).toThrow();\n      expect(function () {\n        api.setClock('invalidClockKey');\n      }).toThrow();\n    });\n\n    it('[Legacy TimeAPI]: a new tick listener is registered', function () {\n      api.clock('mts', mockOffsets);\n      expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));\n    });\n\n    it('a new tick listener is registered', function () {\n      api.setClock('mts', mockOffsets);\n      expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));\n    });\n\n    it('listener of existing tick source is reregistered', function () {\n      api.clock('mts', mockOffsets);\n      api.clock('amts', mockOffsets);\n      expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));\n    });\n\n    it('[Legacy TimeAPI]: Allows the active clock to be set and unset', function () {\n      expect(api.clock()).toBeUndefined();\n      api.clock('mts', mockOffsets);\n      expect(api.clock()).toBeDefined();\n      // Unset the clock\n      api.stopClock();\n      expect(api.clock()).toBeUndefined();\n    });\n\n    it('Provides a default time context', () => {\n      const timeContext = api.getContextForView([]);\n      expect(timeContext).not.toBe(null);\n    });\n\n    it('Without a clock, is in fixed time mode', () => {\n      const timeContext = api.getContextForView([]);\n      expect(timeContext.isRealTime()).toBe(false);\n    });\n\n    it('Provided a clock, is in real-time mode', () => {\n      const timeContext = api.getContextForView([]);\n      timeContext.clock('mts', {\n        start: 0,\n        end: 1\n      });\n      expect(timeContext.isRealTime()).toBe(true);\n    });\n  });\n\n  it('on tick, observes offsets, and indicates tick in bounds callback', function () {\n    const mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n    mockTickSource.currentValue.and.returnValue(100);\n    let tickCallback;\n    const boundsCallback = jasmine.createSpy('boundsCallback');\n    const clockOffsets = {\n      start: -100,\n      end: 100\n    };\n    mockTickSource.key = 'mts';\n\n    api.addClock(mockTickSource);\n    api.clock('mts', clockOffsets);\n\n    api.on('bounds', boundsCallback);\n\n    tickCallback = mockTickSource.on.calls.mostRecent().args[1];\n    tickCallback(1000);\n    expect(boundsCallback).toHaveBeenCalledWith(\n      {\n        start: 900,\n        end: 1100\n      },\n      true\n    );\n  });\n});\n"
  },
  {
    "path": "src/api/time/TimeContext.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport { FIXED_MODE_KEY, MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';\n\n/**\n * @typedef {import('../../utils/clock/DefaultClock.js').default} Clock\n */\n\n/**\n * @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem\n */\n\n/**\n * @typedef {Object} TimeConductorBounds\n * @property {number } start The start time displayed by the time conductor\n * in ms since epoch. Epoch determined by currently active time system\n * @property {number} end The end time displayed by the time conductor in ms\n * since epoch.\n */\n\n/**\n * Clock offsets are used to calculate temporal bounds when the system is\n * ticking on a clock source.\n *\n * @typedef {Object} ClockOffsets\n * @property {number} start A time span relative to the current value of the\n * ticking clock, from which start bounds will be calculated. This value must\n * be < 0. When a clock is active, bounds will be calculated automatically\n * based on the value provided by the clock, and the defined clock offsets.\n * @property {number} end A time span relative to the current value of the\n * ticking clock, from which end bounds will be calculated. This value must\n * be >= 0.\n */\n\n/**\n * @typedef {Object} ValidationResult\n * @property {boolean} valid Result of the validation - true or false.\n * @property {string} message An error message if valid is false.\n */\n\n/**\n * @typedef {'fixed' | 'realtime'} Mode The time conductor mode.\n */\n\n/**\n * @class TimeContext\n * @extends EventEmitter\n */\nclass TimeContext extends EventEmitter {\n  constructor() {\n    super();\n\n    /**\n     * The time systems available to the TimeAPI.\n     * @type {Map<string, TimeSystem>}\n     */\n    this.timeSystems = new Map();\n\n    /**\n     * The currently applied time system.\n     * @type {TimeSystem | undefined}\n     */\n    this.system = undefined;\n\n    /**\n     * The clocks available to the TimeAPI.\n     * @type {Map<string, import('../../utils/clock/DefaultClock.js').default>}\n     */\n    this.clocks = new Map();\n\n    /**\n     * The current bounds of the time conductor.\n     * @type {TimeConductorBounds}\n     */\n    this.boundsVal = {\n      start: undefined,\n      end: undefined\n    };\n\n    /**\n     * The currently active clock.\n     * @type {Clock | undefined}\n     */\n    this.activeClock = undefined;\n    this.offsets = undefined;\n    this.mode = undefined;\n    this.warnCounts = {};\n\n    this.tick = this.tick.bind(this);\n  }\n\n  /**\n   * Get or set the time system of the TimeAPI.\n   * @param {TimeSystem | string} timeSystemOrKey\n   * @param {TimeConductorBounds} bounds\n   * @returns {TimeSystem} The currently applied time system\n   * @deprecated This method is deprecated. Use \"getTimeSystem\" and \"setTimeSystem\" instead.\n   */\n  timeSystem(timeSystemOrKey, bounds) {\n    this.#warnMethodDeprecated('\"timeSystem\"', '\"getTimeSystem\" and \"setTimeSystem\"');\n\n    if (arguments.length >= 1) {\n      if (arguments.length === 1 && !this.activeClock) {\n        throw new Error('Must specify bounds when changing time system without an active clock.');\n      }\n\n      let timeSystem;\n\n      if (timeSystemOrKey === undefined) {\n        throw 'Please provide a time system';\n      }\n\n      if (typeof timeSystemOrKey === 'string') {\n        timeSystem = this.timeSystems.get(timeSystemOrKey);\n\n        if (timeSystem === undefined) {\n          throw (\n            'Unknown time system ' +\n            timeSystemOrKey +\n            \". Has it been registered with 'addTimeSystem'?\"\n          );\n        }\n      } else if (typeof timeSystemOrKey === 'object') {\n        timeSystem = timeSystemOrKey;\n\n        if (!this.timeSystems.has(timeSystem.key)) {\n          throw (\n            'Unknown time system ' +\n            timeSystem.key +\n            \". Has it been registered with 'addTimeSystem'?\"\n          );\n        }\n      } else {\n        throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';\n      }\n\n      this.system = this.#copy(timeSystem);\n\n      /**\n       * The time system used by the time\n       * conductor has changed. A change in Time System will always be\n       * followed by a bounds event specifying new query bounds.\n       * @type {TimeSystem}\n       */\n      const system = this.#copy(this.system);\n      this.emit('timeSystem', system);\n      this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);\n\n      if (bounds) {\n        this.bounds(bounds);\n      }\n    }\n\n    return this.system;\n  }\n\n  /**\n   * Validate the given bounds. This can be used for pre-validation of bounds,\n   * for example by views validating user inputs.\n   * @param {TimeConductorBounds} bounds The start and end time of the conductor.\n   * @returns {ValidationResult} A validation error, or true if valid\n   */\n  validateBounds(bounds) {\n    if (\n      bounds.start === undefined ||\n      bounds.end === undefined ||\n      isNaN(bounds.start) ||\n      isNaN(bounds.end)\n    ) {\n      return {\n        valid: false,\n        message: 'Start and end must be specified as integer values'\n      };\n    } else if (bounds.start >= bounds.end) {\n      return {\n        valid: false,\n        message: 'Start bound must be less than end bound'\n      };\n    }\n\n    return {\n      valid: true,\n      message: ''\n    };\n  }\n\n  /**\n   * Get or set the start and end time of the time conductor. Basic validation\n   * of bounds is performed.\n   *\n   * @param {TimeConductorBounds} [newBounds] The new bounds to set. If not provided, current bounds will be returned.\n   * @throws {Error} Validation error\n   * @returns {TimeConductorBounds} The current bounds of the time conductor.\n   * @deprecated This method is deprecated. Use \"getBounds\" and \"setBounds\" instead.\n   */\n  bounds(newBounds) {\n    this.#warnMethodDeprecated('\"bounds\"', '\"getBounds\" and \"setBounds\"');\n\n    if (arguments.length > 0) {\n      const validationResult = this.validateBounds(newBounds);\n      if (validationResult.valid !== true) {\n        throw new Error(validationResult.message);\n      }\n\n      //Create a copy to avoid direct mutation of conductor bounds\n      this.boundsVal = this.#copy(newBounds);\n      /**\n       * The start time, end time, or both have been updated.\n       * @event bounds\n       * @property {TimeConductorBounds} bounds The newly updated bounds\n       * @property {boolean} [tick] `true` if the bounds update was due to\n       * a \"tick\" event (ie. was an automatic update), false otherwise.\n       */\n      this.emit('bounds', this.boundsVal, false);\n      this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);\n    }\n\n    //Return a copy to prevent direct mutation of time conductor bounds.\n    return this.#copy(this.boundsVal);\n  }\n\n  /**\n   * Validate the given offsets. This can be used for pre-validation of\n   * offsets, for example by views validating user inputs.\n   * @param {ClockOffsets} offsets The start and end offsets from a 'now' value.\n   * @returns {ValidationResult} A validation error, and true/false if valid or not\n   */\n  validateOffsets(offsets) {\n    if (\n      offsets.start === undefined ||\n      offsets.end === undefined ||\n      isNaN(offsets.start) ||\n      isNaN(offsets.end)\n    ) {\n      return {\n        valid: false,\n        message: 'Start and end offsets must be specified as integer values'\n      };\n    } else if (offsets.start >= offsets.end) {\n      return {\n        valid: false,\n        message: 'Start offset must be less than end offset'\n      };\n    }\n\n    return {\n      valid: true,\n      message: ''\n    };\n  }\n\n  /**\n   * Get or set the currently applied clock offsets. If no parameter is provided,\n   * the current value will be returned. If provided, the new value will be\n   * used as the new clock offsets.\n   * @param {ClockOffsets} [offsets] The new clock offsets to set. If not provided, current offsets will be returned.\n   * @returns {ClockOffsets} The current clock offsets.\n   * @deprecated This method is deprecated. Use \"getClockOffsets\" and \"setClockOffsets\" instead.\n   */\n  clockOffsets(offsets) {\n    this.#warnMethodDeprecated('\"clockOffsets\"', '\"getClockOffsets\" and \"setClockOffsets\"');\n\n    if (arguments.length > 0) {\n      const validationResult = this.validateOffsets(offsets);\n      if (validationResult.valid !== true) {\n        throw new Error(validationResult.message);\n      }\n\n      this.offsets = offsets;\n\n      const currentValue = this.activeClock.currentValue();\n      const newBounds = {\n        start: currentValue + offsets.start,\n        end: currentValue + offsets.end\n      };\n\n      this.bounds(newBounds);\n\n      /**\n       * Event that is triggered when clock offsets change.\n       * @event clockOffsets\n       * @property {ClockOffsets} clockOffsets The newly activated clock\n       * offsets.\n       */\n      this.emit('clockOffsets', offsets);\n    }\n\n    return this.offsets;\n  }\n\n  /**\n   * Stop following the currently active clock. This will\n   * revert all views to showing a static time frame defined by the current\n   * bounds.\n   * @deprecated This method is deprecated.\n   */\n  stopClock() {\n    this.#warnMethodDeprecated('\"stopClock\"');\n\n    this.setMode(FIXED_MODE_KEY);\n  }\n\n  /**\n   * Set the active clock. Tick source will be immediately subscribed to\n   * and ticking will begin. Offsets from 'now' must also be provided.\n   *\n   * @param {string|Clock} keyOrClock The clock to activate, or its key\n   * @param {ClockOffsets} offsets on each tick these will be used to calculate\n   * the start and end bounds. This maintains a sliding time window of a fixed\n   * width that automatically updates.\n   * (Legacy) Emits a \"clock\" event with the new clock.\n   * Emits a \"clockChanged\" event with the new clock.\n   * @return {Clock|undefined} the currently active clock; undefined if in fixed mode\n   * @deprecated This method is deprecated. Use \"getClock\" and \"setClock\" instead.\n   */\n  clock(keyOrClock, offsets) {\n    this.#warnMethodDeprecated('\"clock\"', '\"getClock\" and \"setClock\"');\n\n    if (arguments.length === 2) {\n      let clock;\n\n      if (typeof keyOrClock === 'string') {\n        clock = this.clocks.get(keyOrClock);\n        if (clock === undefined) {\n          throw \"Unknown clock '\" + keyOrClock + \"'. Has it been registered with 'addClock'?\";\n        }\n      } else if (typeof keyOrClock === 'object') {\n        clock = keyOrClock;\n        if (!this.clocks.has(clock.key)) {\n          throw \"Unknown clock '\" + keyOrClock.key + \"'. Has it been registered with 'addClock'?\";\n        }\n      }\n\n      const previousClock = this.activeClock;\n      if (previousClock !== undefined) {\n        previousClock.off('tick', this.tick);\n      }\n\n      this.activeClock = clock;\n\n      /**\n       * The active clock has changed.\n       * @event clock\n       * @property {Clock} clock The newly activated clock, or undefined\n       * if the system is no longer following a clock source\n       */\n      this.emit('clock', this.activeClock);\n      this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);\n\n      if (this.activeClock !== undefined) {\n        //set the mode or isRealtime will be false even though we're in clock mode\n        this.setMode(REALTIME_MODE_KEY);\n\n        this.clockOffsets(offsets);\n        this.activeClock.on('tick', this.tick);\n      }\n    } else if (arguments.length === 1) {\n      throw 'When setting the clock, clock offsets must also be provided';\n    }\n\n    return this.isRealTime() ? this.activeClock : undefined;\n  }\n\n  /**\n   * Update bounds based on provided time and current offsets.\n   * @param {number} timestamp A time from which bounds will be calculated\n   * using current offsets.\n   */\n  tick(timestamp) {\n    // always emit the timestamp\n    this.emit('tick', timestamp);\n\n    if (this.mode === REALTIME_MODE_KEY) {\n      const newBounds = {\n        start: timestamp + this.offsets.start,\n        end: timestamp + this.offsets.end\n      };\n\n      this.boundsVal = newBounds;\n      // \"bounds\" will be deprecated in a future release\n      this.emit('bounds', this.boundsVal, true);\n      this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);\n    }\n  }\n\n  /**\n   * Get the timestamp of the current clock\n   * @returns {number} current timestamp of current clock regardless of mode\n   */\n\n  now() {\n    return this.activeClock.currentValue();\n  }\n\n  /**\n   * Get the time system of the TimeAPI.\n   * @returns {TimeSystem} The currently applied time system\n   */\n  getTimeSystem() {\n    return this.system;\n  }\n\n  /**\n   * Set the time system of the TimeAPI.\n   * Emits a \"timeSystem\" event with the new time system.\n   * @param {TimeSystem | string} timeSystemOrKey\n   * @param {TimeConductorBounds} bounds\n   */\n  setTimeSystem(timeSystemOrKey, bounds) {\n    if (timeSystemOrKey === undefined) {\n      throw 'Please provide a time system';\n    }\n\n    let timeSystem;\n\n    if (typeof timeSystemOrKey === 'string') {\n      timeSystem = this.timeSystems.get(timeSystemOrKey);\n\n      if (timeSystem === undefined) {\n        throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;\n      }\n    } else if (typeof timeSystemOrKey === 'object') {\n      timeSystem = timeSystemOrKey;\n\n      if (!this.timeSystems.has(timeSystem.key)) {\n        throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;\n      }\n    } else {\n      throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';\n    }\n\n    this.system = this.#copy(timeSystem);\n    /**\n     * The time system used by the time\n     * conductor has changed. A change in Time System will always be\n     * followed by a bounds event specifying new query bounds.\n     *\n     * @property {TimeSystem} The value of the currently applied\n     * Time System\n     * */\n    this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));\n    this.emit('timeSystem', this.#copy(this.system));\n\n    if (bounds) {\n      this.setBounds(bounds);\n    }\n  }\n\n  /**\n   * Get the start and end time of the time conductor. Basic validation\n   * of bounds is performed.\n   * @returns {TimeConductorBounds} The current bounds of the time conductor.\n   */\n  getBounds() {\n    //Return a copy to prevent direct mutation of time conductor bounds.\n    return this.#copy(this.boundsVal);\n  }\n\n  /**\n   * Set the start and end time of the time conductor. Basic validation\n   * of bounds is performed.\n   *\n   * @param {TimeConductorBounds} newBounds The new bounds to set.\n   * @throws {Error} Validation error if bounds are invalid\n   */\n  setBounds(newBounds) {\n    const validationResult = this.validateBounds(newBounds);\n    if (validationResult.valid !== true) {\n      throw new Error(validationResult.message);\n    }\n\n    //Create a copy to avoid direct mutation of conductor bounds\n    this.boundsVal = this.#copy(newBounds);\n    /**\n     * The start time, end time, or both have been updated.\n     * @event bounds\n     * @property {TimeConductorBounds} bounds The newly updated bounds\n     * @property {boolean} [tick] `true` if the bounds update was due to\n     * a \"tick\" event (i.e. was an automatic update), false otherwise.\n     */\n    this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);\n    this.emit('bounds', this.boundsVal, false);\n  }\n\n  /**\n   * Get the active clock.\n   * @return {Clock|undefined} the currently active clock; undefined if in fixed mode.\n   */\n  getClock() {\n    return this.activeClock;\n  }\n\n  /**\n   * Set the active clock. Tick source will be immediately subscribed to\n   * and the currently ticking will begin.\n   * Offsets from 'now', if provided, will be used to set realtime mode offsets\n   *\n   * @param {string|Clock} keyOrClock The clock to activate, or its key\n   */\n  setClock(keyOrClock) {\n    let clock;\n\n    if (typeof keyOrClock === 'string') {\n      clock = this.clocks.get(keyOrClock);\n      if (clock === undefined) {\n        throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;\n      }\n    } else if (typeof keyOrClock === 'object') {\n      clock = keyOrClock;\n      if (!this.clocks.has(clock.key)) {\n        throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;\n      }\n    }\n\n    const previousClock = this.activeClock;\n    if (previousClock) {\n      previousClock.off('tick', this.tick);\n    }\n\n    this.activeClock = clock;\n    this.activeClock.on('tick', this.tick);\n\n    /**\n     * The active clock has changed.\n     * @event clock\n     * @property {TimeContext} clock The newly activated clock, or undefined\n     * if the system is no longer following a clock source\n     */\n    this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);\n    this.emit('clock', this.activeClock);\n  }\n\n  /**\n   * Get the current mode.\n   * @return {Mode} the current mode\n   */\n  getMode() {\n    return this.mode;\n  }\n\n  /**\n   * Set the mode to either fixed or realtime.\n   *\n   * @param {Mode} mode The mode to activate\n   * @param {TimeConductorBounds|ClockOffsets} offsetsOrBounds A time window of a fixed width\n   * @fires module:openmct.TimeAPI~clock\n   * @return {Mode | undefined} the currently active mode\n   */\n  setMode(mode, offsetsOrBounds) {\n    if (!mode) {\n      return;\n    }\n\n    if (mode === MODES.realtime && this.activeClock === undefined) {\n      throw `Unknown clock. Has a clock been registered with 'addClock'?`;\n    }\n\n    if (mode !== this.mode) {\n      this.mode = mode;\n      /**\n       * The active mode has changed.\n       * @event modeChanged\n       * @property {Mode} mode The newly activated mode\n       */\n      this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));\n    }\n\n    if (offsetsOrBounds !== undefined) {\n      if (this.isRealTime()) {\n        this.setClockOffsets(offsetsOrBounds);\n      } else {\n        this.setBounds(offsetsOrBounds);\n      }\n    }\n  }\n\n  /**\n   * Checks if this time context is in realtime mode or not.\n   * @returns {boolean} true if this context is in real-time mode, false if not\n   */\n  isRealTime() {\n    return this.mode === MODES.realtime;\n  }\n\n  /**\n   * Checks if this time context is in fixed mode or not.\n   * @returns {boolean} true if this context is in fixed mode, false if not\n   */\n  isFixed() {\n    return this.mode === MODES.fixed;\n  }\n\n  /**\n   * Get the currently applied clock offsets.\n   * @returns {ClockOffsets} The current clock offsets.\n   */\n  getClockOffsets() {\n    return this.offsets;\n  }\n\n  /**\n   * Set the currently applied clock offsets.\n   * @param {ClockOffsets} offsets The new clock offsets to set.\n   */\n  setClockOffsets(offsets) {\n    const validationResult = this.validateOffsets(offsets);\n    if (validationResult.valid !== true) {\n      throw new Error(validationResult.message);\n    }\n\n    this.offsets = this.#copy(offsets);\n\n    const currentValue = this.activeClock.currentValue();\n    const newBounds = {\n      start: currentValue + offsets.start,\n      end: currentValue + offsets.end\n    };\n\n    this.setBounds(newBounds);\n\n    /**\n     * Event that is triggered when clock offsets change.\n     * @event clockOffsets\n     * @property {ClockOffsets} clockOffsets The newly activated clock\n     * offsets.\n     */\n    this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));\n  }\n\n  /**\n   * Prints a warning to the console when a deprecated method is used. Limits\n   * the number of times a warning is printed per unique method and newMethod\n   * combination.\n   * @param {string} method the deprecated method\n   * @param {string} [newMethod] the new method to use instead\n   * @returns\n   */\n  #warnMethodDeprecated(method, newMethod) {\n    const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination\n\n    const key = `${method}.${newMethod}`;\n    const currentWarnCount = this.warnCounts[key] || 0;\n\n    if (currentWarnCount >= MAX_CALLS) {\n      return; // Don't warn if already warned once\n    }\n\n    this.warnCounts[key] = currentWarnCount + 1;\n\n    let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;\n\n    if (newMethod) {\n      message += ` Please use the ${newMethod} API method(s) instead.`;\n    }\n\n    // TODO: add docs and point to them in warning.\n    //  For more information and migration instructions, visit [link to documentation or migration guide].\n\n    console.warn(message);\n  }\n\n  /**\n   * Deep copy an object.\n   * @param {object} object The object to copy\n   * @returns {object} The copied object\n   */\n  #copy(object) {\n    return JSON.parse(JSON.stringify(object));\n  }\n}\n\nexport default TimeContext;\n\n/**\n@typedef {{start: number, end: number}} Bounds\n*/\n"
  },
  {
    "path": "src/api/time/constants.js",
    "content": "export const TIME_CONTEXT_EVENTS = {\n  //old API events - to be deprecated\n  bounds: 'bounds',\n  clock: 'clock',\n  timeSystem: 'timeSystem',\n  clockOffsets: 'clockOffsets',\n  //new API events\n  tick: 'tick',\n  modeChanged: 'modeChanged',\n  boundsChanged: 'boundsChanged',\n  clockChanged: 'clockChanged',\n  timeSystemChanged: 'timeSystemChanged',\n  clockOffsetsChanged: 'clockOffsetsChanged'\n};\n\nexport const REALTIME_MODE_KEY = 'realtime';\nexport const FIXED_MODE_KEY = 'fixed';\n\nexport const MODES = {\n  [FIXED_MODE_KEY]: FIXED_MODE_KEY,\n  [REALTIME_MODE_KEY]: REALTIME_MODE_KEY\n};\n"
  },
  {
    "path": "src/api/time/independentTimeAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct } from 'utils/testing';\n\nimport TimeAPI from './TimeAPI.js';\ndescribe('The Independent Time API', function () {\n  let api;\n  let domainObjectKey;\n  let clockKey;\n  let clock;\n  let bounds;\n  let independentBounds;\n  let eventListener;\n  let openmct;\n\n  beforeEach(function () {\n    openmct = createOpenMct();\n    api = new TimeAPI(openmct);\n    clockKey = 'someClockKey';\n    clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n    clock.currentValue.and.returnValue(100);\n    clock.key = clockKey;\n    api.addClock(clock);\n    domainObjectKey = 'test-key';\n    bounds = {\n      start: 0,\n      end: 1\n    };\n    api.bounds(bounds);\n    independentBounds = {\n      start: 10,\n      end: 11\n    };\n    eventListener = jasmine.createSpy('eventListener');\n  });\n\n  it('Creates an independent time context', () => {\n    let timeContext = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: domainObjectKey\n        }\n      }\n    ]);\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    destroyTimeContext();\n  });\n\n  it('Gets an independent time context given the objectPath', () => {\n    let timeContext = api.getContextForView([\n      { identifier: domainObjectKey },\n      {\n        identifier: {\n          namespace: '',\n          key: 'blah'\n        }\n      }\n    ]);\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    destroyTimeContext();\n  });\n\n  it('defaults to the global time context given the objectPath', () => {\n    let timeContext = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: 'blah'\n        }\n      }\n    ]);\n    expect(timeContext.bounds()).toEqual(bounds);\n  });\n\n  it('follows a parent time context given the objectPath', () => {\n    api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: 'blah'\n        }\n      }\n    ]);\n    let destroyTimeContext = api.addIndependentContext('blah', independentBounds);\n    let timeContext = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: domainObjectKey\n        }\n      },\n      {\n        identifier: {\n          namespace: '',\n          key: 'blah'\n        }\n      }\n    ]);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    destroyTimeContext();\n    expect(timeContext.bounds()).toEqual(bounds);\n  });\n\n  it(\"uses an object's independent time context if the parent doesn't have one\", () => {\n    const domainObjectKey2 = `${domainObjectKey}-2`;\n    const domainObjectKey3 = `${domainObjectKey}-3`;\n    let timeContext = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: domainObjectKey\n        }\n      }\n    ]);\n    let timeContext2 = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: domainObjectKey2\n        }\n      }\n    ]);\n    let timeContext3 = api.getContextForView([\n      {\n        identifier: {\n          namespace: '',\n          key: domainObjectKey3\n        }\n      }\n    ]);\n    // all bounds follow global time context\n    expect(timeContext.bounds()).toEqual(bounds);\n    expect(timeContext2.bounds()).toEqual(bounds);\n    expect(timeContext3.bounds()).toEqual(bounds);\n    // only first item has own context\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    expect(timeContext2.bounds()).toEqual(bounds);\n    expect(timeContext3.bounds()).toEqual(bounds);\n    // first and second item have own context\n    let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    expect(timeContext2.bounds()).toEqual(independentBounds);\n    expect(timeContext3.bounds()).toEqual(bounds);\n    // all items have own time context\n    let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds);\n    expect(timeContext.bounds()).toEqual(independentBounds);\n    expect(timeContext2.bounds()).toEqual(independentBounds);\n    expect(timeContext3.bounds()).toEqual(independentBounds);\n    //remove own contexts one at a time - should revert to global time context\n    destroyTimeContext();\n    expect(timeContext.bounds()).toEqual(bounds);\n    expect(timeContext2.bounds()).toEqual(independentBounds);\n    expect(timeContext3.bounds()).toEqual(independentBounds);\n    destroyTimeContext2();\n    expect(timeContext.bounds()).toEqual(bounds);\n    expect(timeContext2.bounds()).toEqual(bounds);\n    expect(timeContext3.bounds()).toEqual(independentBounds);\n    destroyTimeContext3();\n    expect(timeContext.bounds()).toEqual(bounds);\n    expect(timeContext2.bounds()).toEqual(bounds);\n    expect(timeContext3.bounds()).toEqual(bounds);\n  });\n\n  it('Allows setting of valid bounds', function () {\n    bounds = {\n      start: 0,\n      end: 1\n    };\n    let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(timeContext.bounds()).not.toEqual(bounds);\n    timeContext.bounds(bounds);\n    expect(timeContext.bounds()).toEqual(bounds);\n    destroyTimeContext();\n  });\n\n  it('Disallows setting of invalid bounds', function () {\n    bounds = {\n      start: 1,\n      end: 0\n    };\n\n    let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(timeContext.bounds()).not.toBe(bounds);\n\n    expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();\n    expect(timeContext.bounds()).not.toEqual(bounds);\n\n    bounds = { start: 1 };\n    expect(timeContext.bounds()).not.toEqual(bounds);\n    expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();\n    expect(timeContext.bounds()).not.toEqual(bounds);\n    destroyTimeContext();\n  });\n\n  it('Emits an event when bounds change', function () {\n    let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);\n    let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n    expect(eventListener).not.toHaveBeenCalled();\n    timeContext.on('bounds', eventListener);\n    timeContext.bounds(bounds);\n    expect(eventListener).toHaveBeenCalledWith(bounds, false);\n    destroyTimeContext();\n  });\n\n  it('Emits an event when bounds change on the global context', function () {\n    let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);\n    expect(eventListener).not.toHaveBeenCalled();\n    timeContext.on('bounds', eventListener);\n    timeContext.bounds(bounds);\n    expect(eventListener).toHaveBeenCalledWith(bounds, false);\n  });\n\n  describe(' when using real time clock', function () {\n    const mockOffsets = {\n      start: 10,\n      end: 11\n    };\n\n    it('Emits an event when bounds change based on current value', function () {\n      let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);\n      let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);\n      expect(eventListener).not.toHaveBeenCalled();\n      timeContext.clock('someClockKey', mockOffsets);\n      timeContext.on('bounds', eventListener);\n      timeContext.tick(10);\n      expect(eventListener).toHaveBeenCalledWith(\n        {\n          start: 20,\n          end: 21\n        },\n        true\n      );\n      destroyTimeContext();\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/tooltips/ToolTip.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\n\nimport TooltipComponent from './components/TooltipComponent.vue';\n\nclass Tooltip extends EventEmitter {\n  constructor(\n    { toolTipText, toolTipLocation, parentElement, cssClasses } = {\n      tooltipText: '',\n      toolTipLocation: 'below',\n      parentElement: null,\n      cssClasses: []\n    }\n  ) {\n    super();\n\n    const { vNode, destroy } = mount({\n      components: {\n        TooltipComponent: TooltipComponent\n      },\n      provide: {\n        toolTipText,\n        toolTipLocation,\n        parentElement,\n        cssClasses\n      },\n      template: '<tooltip-component toolTipText=\"toolTipText\"></tooltip-component>'\n    });\n\n    this.component = vNode.componentInstance;\n    this._destroy = destroy;\n\n    this.isActive = null;\n  }\n\n  destroy() {\n    if (!this.isActive) {\n      return;\n    }\n    this._destroy();\n    this.isActive = false;\n  }\n\n  /**\n   * @private\n   **/\n  show() {\n    document.body.appendChild(this.component.$el);\n    this.isActive = true;\n  }\n}\n\nexport default Tooltip;\n"
  },
  {
    "path": "src/api/tooltips/ToolTipAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Tooltip from './ToolTip.js';\n\n/**\n * @readonly\n * @enum {string} TooltipLocation\n * @property {string} ABOVE The string for locating tooltips above an element\n * @property {string} BELOW The string for locating tooltips below an element\n * @property {string} RIGHT The pixel-spatial annotation type\n * @property {string} LEFT The temporal annotation type\n * @property {string} CENTER The plot-spatial annotation type\n */\nconst TOOLTIP_LOCATIONS = Object.freeze({\n  ABOVE: 'above',\n  BELOW: 'below',\n  RIGHT: 'right',\n  LEFT: 'left',\n  CENTER: 'center'\n});\n\n/**\n * The TooltipAPI is responsible for adding custom tooltips to\n * the desired elements on the screen\n *\n * @constructor\n */\n\nclass TooltipAPI {\n  constructor() {\n    this.activeToolTips = [];\n    this.TOOLTIP_LOCATIONS = TOOLTIP_LOCATIONS;\n  }\n\n  /**\n   * @private for platform-internal use\n   */\n  showTooltip(tooltip) {\n    this.removeAllTooltips();\n    this.activeToolTips.push(tooltip);\n    tooltip.show();\n  }\n\n  /**\n   * API method to allow for removing all tooltips\n   */\n  removeAllTooltips() {\n    if (!this.activeToolTips?.length) {\n      return;\n    }\n    for (let i = this.activeToolTips.length - 1; i > -1; i--) {\n      this.activeToolTips[i].destroy();\n      this.activeToolTips.splice(i, 1);\n    }\n  }\n\n  /**\n   * A description of option properties that can be passed into the tooltip\n   * @typedef {Object} TooltipOptions\n   * @property {string} tooltipText text to show in the tooltip\n   * @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement\n   * @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to\n   * @property {Array} cssClasses css classes to use with the tool tip element\n   */\n\n  /**\n   * Tooltips take an options object that consists of the string, tooltipLocation, a parentElement, and an array of cssClasses\n   * @param {TooltipOptions} options\n   */\n  tooltip(options) {\n    let tooltip = new Tooltip(options);\n\n    this.showTooltip(tooltip);\n\n    return tooltip;\n  }\n}\n\nexport default TooltipAPI;\n"
  },
  {
    "path": "src/api/tooltips/components/TooltipComponent.vue",
    "content": "<!--\nOpen MCT, Copyright (c) 2014-2024, United States Government\nas represented by the Administrator of the National Aeronautics and Space\nAdministration. All rights reserved.\n\nOpen MCT is licensed under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0.\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations\nunder the License.\n\nOpen MCT includes source code licensed under additional open source\nlicenses. See the Open Source Licenses file (LICENSES.md) included with\nthis source code distribution or the Licensing information page available\nat runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"tooltip-wrapper\"\n    class=\"c-tooltip-wrapper\"\n    :class=\"cssClasses\"\n    :style=\"toolTipLocationStyle\"\n    role=\"tooltip\"\n    aria-labelledby=\"tooltip-text\"\n    aria-live=\"polite\"\n  >\n    <span id=\"tooltip-text\" class=\"c-tooltip\">\n      {{ toolTipText }}\n    </span>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['toolTipText', 'toolTipLocation', 'parentElement', 'cssClasses'],\n  computed: {\n    toolTipCoordinates() {\n      return this.parentElement.getBoundingClientRect();\n    },\n    toolTipLocationStyle() {\n      const { top, left, height, width } = this.toolTipCoordinates;\n      let toolTipLocationStyle = {};\n\n      if (this.toolTipLocation === 'above') {\n        toolTipLocationStyle = { top: `${top - 5}px`, left: `${left}px` };\n      }\n      if (this.toolTipLocation === 'below') {\n        toolTipLocationStyle = { top: `${top + height}px`, left: `${left}px` };\n      }\n      if (this.toolTipLocation === 'right') {\n        toolTipLocationStyle = { top: `${top}px`, left: `${left + width}px` };\n      }\n      if (this.toolTipLocation === 'left') {\n        toolTipLocationStyle = { top: `${top}px`, left: `${left - width}px` };\n      }\n      if (this.toolTipLocation === 'center') {\n        toolTipLocationStyle = { top: `${top + height / 2}px`, left: `${left + width / 2}px` };\n      }\n\n      return toolTipLocationStyle;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/api/tooltips/components/tooltip-component.scss",
    "content": ".c-tooltip-wrapper {\n  @include menuOuter();\n  max-width: 200px;\n  height: auto;\n  width: auto;\n  padding: $interiorMargin $interiorMarginLg;\n  overflow-wrap: break-word;\n  pointer-events: none;\n  position: absolute;\n  z-index: 100;\n}\n\n.c-tooltip {\n  font-style: italic;\n}\n"
  },
  {
    "path": "src/api/tooltips/tooltipMixins.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst tooltipHelpers = {\n  methods: {\n    async getTelemetryPathString(telemetryIdentifier) {\n      let telemetryPathString = '';\n      if (!this.domainObject?.identifier) {\n        return;\n      }\n      const telemetryPath = await this.openmct.objects.getTelemetryPath(\n        this.domainObject.identifier,\n        telemetryIdentifier\n      );\n      if (telemetryPath.length) {\n        telemetryPathString = telemetryPath.join(' / ');\n      }\n      return telemetryPathString;\n    },\n    async getObjectPath(objectIdentifier) {\n      if (!objectIdentifier && !this.domainObject) {\n        return;\n      }\n      const domainObjectIdentifier = objectIdentifier || this.domainObject.identifier;\n      const objectPathList = await this.openmct.objects.getOriginalPath(domainObjectIdentifier);\n      objectPathList.pop();\n      return objectPathList\n        .map((pathItem) => pathItem.name)\n        .reverse()\n        .join(' / ');\n    },\n    buildToolTip(tooltipText, tooltipLocation, elementRef, cssClasses) {\n      if (!tooltipText || tooltipText.length < 1) {\n        return;\n      }\n      let parentElement = this.$refs[elementRef];\n      if (Array.isArray(parentElement)) {\n        parentElement = parentElement[0];\n      }\n      this.tooltip = this.openmct.tooltips.tooltip({\n        toolTipText: tooltipText,\n        toolTipLocation: tooltipLocation,\n        parentElement: parentElement,\n        cssClasses\n      });\n    },\n    hideToolTip() {\n      this.tooltip?.destroy();\n      this.tooltip = null;\n    }\n  }\n};\n\nexport default tooltipHelpers;\n"
  },
  {
    "path": "src/api/types/Type.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A Type describes a kind of domain object that may appear or be\n * created within Open MCT.\n */\nexport default class Type {\n  /**\n   * @param {TypeDefinition} definition\n   */\n  constructor(definition) {\n    this.definition = definition;\n    if (definition.key) {\n      this.key = definition.key;\n    }\n  }\n  /**\n   * Convert a legacy type definition to the new format.\n   * @param {LegacyTypeDefinition} legacyDefinition\n   * @returns {TypeDefinition}\n   */\n  static definitionFromLegacyDefinition(legacyDefinition) {\n    let definition = {};\n    definition.name = legacyDefinition.name;\n    definition.cssClass = legacyDefinition.cssClass;\n    definition.description = legacyDefinition.description;\n    definition.form = legacyDefinition.properties;\n    if (legacyDefinition.telemetry !== undefined) {\n      let telemetry = {\n        values: []\n      };\n\n      if (legacyDefinition.telemetry.domains !== undefined) {\n        legacyDefinition.telemetry.domains.forEach((domain, index) => {\n          domain.hints = {\n            domain: index\n          };\n          telemetry.values.push(domain);\n        });\n      }\n\n      if (legacyDefinition.telemetry.ranges !== undefined) {\n        legacyDefinition.telemetry.ranges.forEach((range, index) => {\n          range.hints = {\n            range: index\n          };\n          telemetry.values.push(range);\n        });\n      }\n\n      definition.telemetry = telemetry;\n    }\n\n    if (legacyDefinition.model) {\n      definition.initialize = function (model) {\n        for (let [k, v] of Object.entries(legacyDefinition.model)) {\n          model[k] = JSON.parse(JSON.stringify(v));\n        }\n      };\n    }\n\n    if (legacyDefinition.features && legacyDefinition.features.includes('creation')) {\n      definition.creatable = true;\n    }\n\n    return definition;\n  }\n  /**\n   * Check if a domain object is an instance of this type.\n   * @param {DomainObject} domainObject\n   * @returns {boolean} true if the domain object is of this type\n   */\n  check(domainObject) {\n    // Depends on assignment from MCT.\n    return domainObject.type === this.key;\n  }\n  /**\n   * Get a definition for this type that can be registered using the\n   * legacy bundle format.\n   * @private\n   */\n  toLegacyDefinition() {\n    const def = {};\n    def.name = this.definition.name;\n    def.cssClass = this.definition.cssClass;\n    def.description = this.definition.description;\n    def.properties = this.definition.form;\n\n    if (this.definition.initialize) {\n      def.model = {};\n      this.definition.initialize(def.model);\n    }\n\n    if (this.definition.creatable) {\n      def.features = ['creation'];\n    }\n\n    return def;\n  }\n}\n\n/**\n * @typedef {Object} TypeDefinition\n * @property {string} [key]\n * @property {string} name\n * @property {string} cssClass\n * @property {string} description\n * @property {Form} form\n * @property {Telemetry} telemetry\n * @property {function(Object): void} initialize\n * @property {boolean} creatable\n */\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n"
  },
  {
    "path": "src/api/types/TypeRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Type from './Type.js';\n\nconst UNKNOWN_TYPE = new Type({\n  key: 'unknown',\n  name: 'Unknown Type',\n  cssClass: 'icon-object-unknown'\n});\n\n/**\n * @typedef TypeDefinition\n * @property {string} label the name for this type of object\n * @property {string} description a longer-form description of this type\n * @property {function(domainObject:DomainObject): void} [initialize] a function which initializes\n *           the model for new domain objects of this type\n * @property {boolean} [creatable=false] true if users should be allowed to\n *           create this type (default: false)\n * @property {string} [cssClass] the CSS class to apply for icons\n */\n\n/**\n * A TypeRegistry maintains the definitions for different types\n * that domain objects may have.\n * @interface TypeRegistry\n */\nexport default class TypeRegistry {\n  constructor() {\n    /**\n     * @type {Record<string, Type>}\n     */\n    this.types = {};\n  }\n  /**\n   * Register a new object type.\n   *\n   * @param {string} typeKey a string identifier for this type\n   * @param {TypeDefinition} typeDef the type to add\n   */\n  addType(typeKey, typeDef) {\n    this.standardizeType(typeDef);\n    this.types[typeKey] = new Type(typeDef);\n  }\n  /**\n   * Takes a typeDef, standardizes it, and logs warnings about unsupported\n   * usage.\n   * @private\n   */\n  standardizeType(typeDef) {\n    if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {\n      if (!typeDef.name) {\n        typeDef.name = typeDef.label;\n      }\n\n      delete typeDef.label;\n    }\n  }\n  /**\n   * List keys for all registered types.\n   * @returns {string[]} all registered type keys\n   */\n  listKeys() {\n    return Object.keys(this.types);\n  }\n  /**\n   * Retrieve a registered type by its key.\n   * @param {string} typeKey the key for this type\n   * @returns {Type} the registered type\n   */\n  get(typeKey) {\n    return this.types[typeKey] || UNKNOWN_TYPE;\n  }\n  /**\n   * List all registered types.\n   * @returns {Type[]} all registered types\n   */\n  getAllTypes() {\n    return this.types;\n  }\n  /**\n   * Import legacy types.\n   * @param {TypeDefinition[]} types the types to import\n   */\n  importLegacyTypes(types) {\n    types\n      .filter((t) => this.get(t.key) === UNKNOWN_TYPE)\n      .forEach((type) => {\n        let def = Type.definitionFromLegacyDefinition(type);\n        this.addType(type.key, def);\n      });\n  }\n}\n"
  },
  {
    "path": "src/api/types/TypeRegistrySpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport TypeRegistry from './TypeRegistry.js';\n\ndescribe('The Type API', function () {\n  let typeRegistryInstance;\n\n  beforeEach(function () {\n    typeRegistryInstance = new TypeRegistry();\n    typeRegistryInstance.addType('testType', {\n      name: 'Test Type',\n      description: 'This is a test type.',\n      creatable: true\n    });\n  });\n\n  it('types can be standardized', function () {\n    typeRegistryInstance.addType('standardizationTestType', {\n      label: 'Test Type',\n      description: 'This is a test type.',\n      creatable: true\n    });\n    typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);\n    expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();\n    expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');\n  });\n\n  it('new types are registered successfully and can be retrieved', function () {\n    expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');\n  });\n\n  it('type registry contains new keys', function () {\n    expect(typeRegistryInstance.listKeys()).toContain('testType');\n  });\n});\n"
  },
  {
    "path": "src/api/user/ActiveRoleSynchronizer.js",
    "content": "import { ACTIVE_ROLE_BROADCAST_CHANNEL_NAME } from './constants.js';\n\nclass ActiveRoleSynchronizer {\n  #roleChannel;\n\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.#roleChannel = new BroadcastChannel(ACTIVE_ROLE_BROADCAST_CHANNEL_NAME);\n    this.setActiveRoleFromChannelMessage = this.setActiveRoleFromChannelMessage.bind(this);\n\n    this.subscribeToRoleChanges(this.setActiveRoleFromChannelMessage);\n  }\n  subscribeToRoleChanges(callback) {\n    this.#roleChannel.addEventListener('message', callback);\n  }\n  unsubscribeFromRoleChanges(callback) {\n    this.#roleChannel.removeEventListener('message', callback);\n  }\n\n  setActiveRoleFromChannelMessage(event) {\n    const role = event.data;\n    this.openmct.user.setActiveRole(role);\n  }\n  broadcastNewRole(role) {\n    if (!this.#roleChannel.name) {\n      return false;\n    }\n\n    this.#roleChannel.postMessage(role);\n  }\n  destroy() {\n    this.unsubscribeFromRoleChanges(this.setActiveRoleFromChannelMessage);\n    this.#roleChannel.close();\n  }\n}\n\nexport default ActiveRoleSynchronizer;\n"
  },
  {
    "path": "src/api/user/StatusAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\n\n/**\n * The StatusAPI is used to get and set various statuses linked to the current logged in user.\n * This includes the ability to set the status of the current user as a response to a poll question,\n * set the poll question itself, and the ability to set the status of mission actions.\n *\n * @augments EventEmitter\n */\nexport default class StatusAPI extends EventEmitter {\n  /** @type {UserAPI} */\n  #userAPI;\n  /** @type {OpenMCT} */\n  #openmct;\n\n  /**\n   * @param {UserAPI} userAPI\n   * @param {OpenMCT} openmct\n   */\n  constructor(userAPI, openmct) {\n    super();\n    this.#userAPI = userAPI;\n    this.#openmct = openmct;\n\n    this.onProviderStatusChange = this.onProviderStatusChange.bind(this);\n    this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);\n    this.onMissionActionStatusChange = this.onMissionActionStatusChange.bind(this);\n    this.listenToStatusEvents = this.listenToStatusEvents.bind(this);\n\n    this.#openmct.once('destroy', () => {\n      const provider = this.#userAPI.getProvider();\n\n      if (typeof provider?.off === 'function') {\n        provider.off('statusChange', this.onProviderStatusChange);\n        provider.off('pollQuestionChange', this.onProviderPollQuestionChange);\n        provider.off('missionActionStatusChange', this.onMissionActionStatusChange);\n      }\n    });\n\n    this.#userAPI.on('providerAdded', this.listenToStatusEvents);\n  }\n\n  /**\n   * Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.\n   * @returns {Promise<PollQuestion>}\n   */\n  getPollQuestion() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getPollQuestion) {\n      return provider.getPollQuestion();\n    } else {\n      this.#userAPI.error('User provider does not support polling questions');\n    }\n  }\n\n  /**\n   * Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.\n   * @param {string} questionText - The text of the question\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  async setPollQuestion(questionText) {\n    const canSetPollQuestion = await this.canSetPollQuestion();\n\n    if (canSetPollQuestion) {\n      const provider = this.#userAPI.getProvider();\n\n      const result = await provider.setPollQuestion(questionText);\n\n      try {\n        await this.resetAllStatuses();\n      } catch (error) {\n        console.warn('Poll question set but unable to clear operator statuses.');\n        console.error(error);\n      }\n\n      return result;\n    } else {\n      this.#userAPI.error('User provider does not support setting polling question');\n    }\n  }\n\n  /**\n   * Can the currently logged in user set the operator status poll question.\n   * @returns {Promise<Boolean>}\n   */\n  canSetPollQuestion() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.canSetPollQuestion) {\n      return provider.canSetPollQuestion();\n    } else {\n      return Promise.resolve(false);\n    }\n  }\n\n  /**\n   * Can the currently logged in user set the mission status.\n   * @returns {Promise<Boolean>} true if the currently logged in user can set the mission status, false otherwise.\n   */\n  canSetMissionStatus() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.canSetMissionStatus) {\n      return provider.canSetMissionStatus();\n    } else {\n      return Promise.resolve(false);\n    }\n  }\n\n  /**\n   * Fetch the current status for the given mission action\n   * @param {MissionAction} action\n   * @returns {string}\n   */\n  getStatusForMissionAction(action) {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getStatusForMissionAction) {\n      return provider.getStatusForMissionAction(action);\n    } else {\n      this.#userAPI.error('User provider does not support getting mission action status');\n    }\n  }\n\n  /**\n   * Fetch the list of possible mission status options (GO, NO-GO, etc.)\n   * @returns {Promise<MissionStatusOption[]>} the complete list of possible mission statuses\n   */\n  async getPossibleMissionActionStatuses() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getPossibleMissionActionStatuses) {\n      const possibleOptions = await provider.getPossibleMissionActionStatuses();\n\n      return possibleOptions;\n    } else {\n      this.#userAPI.error('User provider does not support mission status options');\n    }\n  }\n\n  /**\n   * Fetch the list of possible mission actions\n   * @returns {Promise<string[]>} the list of possible mission actions\n   */\n  async getPossibleMissionActions() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getPossibleMissionActions) {\n      const possibleActions = await provider.getPossibleMissionActions();\n\n      return possibleActions;\n    } else {\n      this.#userAPI.error('User provider does not support mission statuses');\n    }\n  }\n\n  /**\n   * @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.\n   */\n  async getPossibleStatuses() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getPossibleStatuses) {\n      const possibleStatuses = (await provider.getPossibleStatuses()) || [];\n\n      return possibleStatuses.map((status) => status);\n    } else {\n      this.#userAPI.error('User provider cannot provide statuses');\n    }\n  }\n\n  /**\n   * @param {import(\"./UserAPI\").Role} role The role to fetch the current status for.\n   * @returns {Promise<Status>} the current status of the provided role\n   */\n  async getStatusForRole(role) {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getStatusForRole) {\n      const status = await provider.getStatusForRole(role);\n\n      return status;\n    } else {\n      this.#userAPI.error('User provider does not support role status');\n    }\n  }\n\n  /**\n   * @param {import(\"./UserAPI\").Role} role\n   * @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role\n   * @see StatusUserProvider\n   */\n  canProvideStatusForRole(role) {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.canProvideStatusForRole) {\n      return Promise.resolve(provider.canProvideStatusForRole(role));\n    } else {\n      return Promise.resolve(false);\n    }\n  }\n\n  /**\n   * @param {import(\"./UserAPI\").Role} role The role to set the status for.\n   * @param {Status} status The status to set for the provided role\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  setStatusForRole(status) {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.setStatusForRole) {\n      const activeRole = this.#userAPI.getActiveRole();\n      if (!provider.canProvideStatusForRole(activeRole)) {\n        return false;\n      }\n\n      return provider.setStatusForRole(activeRole, status);\n    } else {\n      this.#userAPI.error('User provider does not support setting role status');\n    }\n  }\n\n  /**\n   * @param {MissionAction} action\n   * @param {MissionStatusOption} status\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  setStatusForMissionAction(action, status) {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.setStatusForMissionAction) {\n      return provider.setStatusForMissionAction(action, status);\n    } else {\n      this.#userAPI.error('User provider does not support setting mission role status');\n    }\n  }\n\n  /**\n   * Resets the status of the provided role back to its default status.\n   * @param {import(\"./UserAPI\").Role} role The role to set the status for.\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  async resetStatusForRole(role) {\n    const provider = this.#userAPI.getProvider();\n    const defaultStatus = await this.getDefaultStatusForRole(role);\n\n    if (provider.setStatusForRole) {\n      return provider.setStatusForRole(role, defaultStatus);\n    } else {\n      this.#userAPI.error('User provider does not support resetting role status');\n    }\n  }\n\n  /**\n   * Resets the status of all operators to their default status\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  async resetAllStatuses() {\n    const allStatusRoles = await this.getAllStatusRoles();\n\n    return Promise.all(allStatusRoles.map((role) => this.resetStatusForRole(role)));\n  }\n\n  /**\n   * The default status. This is the status that will be used before the user has selected any status.\n   * @param {import(\"./UserAPI\").Role} role\n   * @returns {Promise<Status>} the default operator status if no other has been set.\n   */\n  async getDefaultStatusForRole(role) {\n    const provider = this.#userAPI.getProvider();\n    const defaultStatus = await provider.getDefaultStatusForRole(role);\n\n    return defaultStatus;\n  }\n\n  /**\n   * All possible status roles. A status role is a user role that can provide status. In some systems\n   * this may be all user roles, but there may be cases where some users are not are not polled\n   * for status if they do not have a real-time operational role.\n   *\n   * @returns {Promise<Array<import(\"./UserAPI\").Role>>} the default operator status if no other has been set.\n   */\n  getAllStatusRoles() {\n    const provider = this.#userAPI.getProvider();\n\n    if (provider.getAllStatusRoles) {\n      return provider.getAllStatusRoles();\n    } else {\n      this.#userAPI.error('User provider cannot provide all status roles');\n    }\n  }\n\n  /**\n   * @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.\n   * @see StatusUserProvider\n   */\n  async canProvideStatusForCurrentUser() {\n    const provider = this.#userAPI.getProvider();\n\n    if (!provider) {\n      return false;\n    }\n    const activeStatusRole = await this.#userAPI.getActiveRole();\n    const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);\n\n    return canProvideStatus;\n  }\n\n  /**\n   * Listen to status events from the UserProvider\n   * @private\n   */\n  listenToStatusEvents(provider) {\n    if (typeof provider.on === 'function') {\n      provider.on('statusChange', this.onProviderStatusChange);\n      provider.on('pollQuestionChange', this.onProviderPollQuestionChange);\n      provider.on('missionActionStatusChange', this.onMissionActionStatusChange);\n    }\n  }\n\n  /**\n   * Emit a status change event\n   * @private\n   */\n  onProviderStatusChange(newStatus) {\n    this.emit('statusChange', newStatus);\n  }\n\n  /**\n   * Emit a poll question change event\n   * @private\n   */\n  onProviderPollQuestionChange(pollQuestion) {\n    this.emit('pollQuestionChange', pollQuestion);\n  }\n\n  /**\n   * Emit a mission action status change event\n   * @private\n   */\n  onMissionActionStatusChange({ action, status }) {\n    this.emit('missionActionStatusChange', { action, status });\n  }\n}\n\n/**\n * @typedef {import('./UserAPI').default} UserAPI\n */\n\n/**\n * @typedef {import('../../../openmct').OpenMCT} OpenMCT\n */\n\n/**\n * @typedef {import('./UserProvider')} UserProvider\n */\n\n/**\n * @typedef {import('./StatusUserProvider')} StatusUserProvider\n */\n\n/**\n * The PollQuestion type\n * @typedef {Object} PollQuestion\n * @property {string} question - The question to be presented to users\n * @property {number} timestamp - The time that the poll question was set.\n */\n\n/**\n * @typedef {Object} MissionAction\n * @property {string} key A unique identifier for this action\n * @property {string} label A human readable label for this action\n */\n\n/**\n * The MissionStatusOption type, extends Status.\n * @typedef {Object} MissionStatusOption\n * @property {string} key - A unique identifier for this status.\n * @property {string} label - A human-readable label for this status.\n * @property {number} timestamp - The time that the status was set.\n * @property {string} color - A color to be used when displaying the mission status.\n */\n\n/**\n * The Status type\n * @typedef {Object} Status\n * @property {string} key - A unique identifier for this status\n * @property {string} label - A human readable label for this status\n * @property {number} timestamp - The time that the status was set.\n */\n"
  },
  {
    "path": "src/api/user/StatusUserProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport UserProvider from './UserProvider.js';\n\nexport default class StatusUserProvider extends UserProvider {\n  /**\n   * @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to listen to\n   * @param {Function} callback a function to invoke when this event occurs\n   */\n  on(event, callback) {}\n  /**\n   * @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to stop listen to\n   * @param {Function} callback the callback function used to register the listener\n   */\n  off(event, callback) {}\n  /**\n   * @returns {import(\"./StatusAPI\").PollQuestion} the current status poll question\n   */\n  async getPollQuestion() {}\n  /**\n   * @param {import(\"./StatusAPI\").PollQuestion} pollQuestion a new poll question to set\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false\n   */\n  async setPollQuestion(pollQuestion) {}\n  /**\n   * @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false\n   */\n  async canSetPollQuestion() {}\n  /**\n   * @returns {Promise<Array<import(\"./StatusAPI\").Status>>} a list of the possible statuses that an operator can be in\n   */\n  async getPossibleStatuses() {}\n  /**\n   * @param {import(\"./UserAPI\").Role} role\n   * @returns {Promise<import(\"./StatusAPI\").Status}\n   */\n  async getStatusForRole(role) {}\n  /**\n   * @param {import(\"./UserAPI\").Role} role\n   * @returns {Promise<import(\"./StatusAPI\").Status}\n   */\n  async getDefaultStatusForRole(role) {}\n  /**\n   * @param {import(\"./UserAPI\").Role} role\n   * @param {*} status\n   * @returns {Promise<Boolean>} true if operation was successful, otherwise false.\n   */\n  async setStatusForRole(role, status) {}\n  /**\n   * @param {import(\"./UserAPI\").Role} role\n   * @returns {Promise<Boolean} true if the user provider can provide status for the given role\n   */\n  async canProvideStatusForRole(role) {}\n  /**\n   * @returns {Promise<Array<import(\"./UserAPI\").Role>>} a list of all available status roles, if user permissions allow it.\n   */\n  async getAllStatusRoles() {}\n  /**\n   * @returns {Promise<import(\"./UserAPI\").Role>} the active status role for the currently logged in user\n   */\n}\n"
  },
  {
    "path": "src/api/user/StoragePersistence.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants.js';\n\nclass StoragePersistence {\n  getActiveRole() {\n    return localStorage.getItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);\n  }\n  setActiveRole(role) {\n    return localStorage.setItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY, role);\n  }\n  clearActiveRole() {\n    return localStorage.removeItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);\n  }\n}\n\nexport default new StoragePersistence();\n"
  },
  {
    "path": "src/api/user/User.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * The example User class.\n */\nexport default class User {\n  constructor(id, name) {\n    this.id = id;\n    this.name = name;\n\n    this.getId = this.getId.bind(this);\n    this.getName = this.getName.bind(this);\n  }\n\n  getId() {\n    return this.id;\n  }\n\n  getName() {\n    return this.name;\n  }\n}\n"
  },
  {
    "path": "src/api/user/UserAPI.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants.js';\nimport StatusAPI from './StatusAPI.js';\nimport StoragePersistence from './StoragePersistence.js';\nimport User from './User.js';\n\nclass UserAPI extends EventEmitter {\n  /**\n   * @type {OpenMCT}\n   */\n  #openmct;\n  /**\n   * @param {OpenMCT} openmct\n   */\n  constructor(openmct) {\n    super();\n\n    this.#openmct = openmct;\n    this._provider = undefined;\n\n    this.User = User;\n    this.status = new StatusAPI(this, openmct);\n  }\n\n  /**\n   * Set the user provider for the user API. This allows you\n   *  to specify ONE user provider to be used with Open MCT.\n   * @method setProvider\n   * @param {module:openmct.UserAPI~UserProvider} provider the new\n   *        user provider\n   */\n  setProvider(provider) {\n    if (this.hasProvider()) {\n      this.error(MULTIPLE_PROVIDER_ERROR);\n    }\n\n    this._provider = provider;\n    this.emit('providerAdded', this._provider);\n  }\n\n  getProvider() {\n    return this._provider;\n  }\n\n  /**\n   * Return true if the user provider has been set.\n   *\n   * @returns {boolean} true if the user provider exists\n   */\n  hasProvider() {\n    return this._provider !== undefined;\n  }\n\n  /**\n   * If a user provider is set, it will return a copy of a user object from\n   * the provider. If the user is not logged in, it will return undefined;\n   *\n   * @returns {Function|Promise} user provider 'getCurrentUser' method\n   * @throws Will throw an error if no user provider is set\n   */\n  getCurrentUser() {\n    if (!this.hasProvider()) {\n      return Promise.resolve(undefined);\n    } else {\n      return this._provider.getCurrentUser();\n    }\n  }\n  /**\n   *  If a user provider is set, it will return an array of possible roles\n   *  that can be selected by the current user\n   *  @returns {Array}\n   *  @throws Will throw an error if no user provider is set\n   */\n\n  getPossibleRoles() {\n    if (!this.hasProvider()) {\n      this.error(NO_PROVIDER_ERROR);\n    }\n    return this._provider.getPossibleRoles();\n  }\n  /**\n   * If a user provider is set, it will return the active role or null\n   * @returns {string|null}\n   */\n  getActiveRole() {\n    if (!this.hasProvider()) {\n      return null;\n    }\n\n    // get from session storage\n    const sessionStorageValue = StoragePersistence.getActiveRole();\n\n    return sessionStorageValue;\n  }\n  /**\n   * Set the active role in session storage\n   * @returns {undefined}\n   */\n  setActiveRole(role) {\n    if (!role) {\n      StoragePersistence.clearActiveRole();\n    } else {\n      StoragePersistence.setActiveRole(role);\n    }\n    this.emit('roleChanged', role);\n  }\n\n  /**\n   * Will return if a role can provide a operator status response\n   * @returns {boolean}\n   */\n  canProvideStatusForRole() {\n    if (!this.hasProvider()) {\n      return null;\n    }\n    const activeRole = this.getActiveRole();\n\n    return this._provider.canProvideStatusForRole?.(activeRole);\n  }\n\n  /**\n   * If a user provider is set, it will return the user provider's\n   * 'isLoggedIn' method\n   *\n   * @returns {Function|Boolean} user provider 'isLoggedIn' method\n   * @throws Will throw an error if no user provider is set\n   */\n  isLoggedIn() {\n    if (!this.hasProvider()) {\n      return false;\n    }\n\n    return this._provider.isLoggedIn();\n  }\n\n  /**\n   * If a user provider is set, it will return a call to it's\n   * 'hasRole' method\n   *\n   * @returns {Function|boolean} user provider 'isLoggedIn' method\n   * @param {string} roleId id of role to check for\n   * @throws Will throw an error if no user provider is set\n   */\n  hasRole(roleId) {\n    this.noProviderCheck();\n\n    return this._provider.hasRole(roleId);\n  }\n\n  /**\n   * Checks if a provider is set and if not, will throw error\n   *\n   * @private\n   * @throws Will throw an error if no user provider is set\n   */\n  noProviderCheck() {\n    if (!this.hasProvider()) {\n      this.error(NO_PROVIDER_ERROR);\n    }\n  }\n\n  /**\n   * Utility function for throwing errors\n   *\n   * @private\n   * @param {string} error description of error\n   * @throws Will throw error passed in\n   */\n  error(error) {\n    throw new Error(error);\n  }\n}\n\nexport default UserAPI;\n\n/**\n * @typedef {string} Role\n * @typedef {import('../../MCT.js').MCT} OpenMCT\n * @typedef {{statusStyles: Record<string, StatusStyleDefinition>}} UserAPIConfiguration\n * @typedef {Object} UserProvider\n */\n\n/**\n * @typedef {Object} StatusStyleDefinition\n * @property {string} iconClass The icon class to apply to the status indicator when this status is active \"icon-circle-slash\",\n * @property {string} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. \"icon-status-poll-question-mark\"\n * @property {string} statusClass The class to apply to the indicator when this status is active eg. \"s-status-error\"\n * @property {string} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg.\"#9900cc\"\n * @property {string} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. \"#fff\"\n */\n"
  },
  {
    "path": "src/api/user/UserAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider.js';\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport { MULTIPLE_PROVIDER_ERROR } from './constants.js';\n\ndescribe('The User API', () => {\n  let openmct;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    const activeOverlays = openmct.overlays.activeOverlays;\n    activeOverlays.forEach((overlay) => overlay.dismiss());\n\n    return resetApplicationState(openmct);\n  });\n\n  describe('with regard to user providers', () => {\n    it('allows you to specify a user provider', () => {\n      openmct.user.on('providerAdded', (provider) => {\n        expect(provider).toBeInstanceOf(ExampleUserProvider);\n      });\n      openmct.user.setProvider(new ExampleUserProvider(openmct));\n    });\n\n    it('prevents more than one user provider from being set', () => {\n      openmct.user.setProvider(new ExampleUserProvider(openmct));\n\n      expect(() => {\n        openmct.user.setProvider({});\n      }).toThrow(new Error(MULTIPLE_PROVIDER_ERROR));\n    });\n\n    it('provides a check for an existing user provider', () => {\n      expect(openmct.user.hasProvider()).toBeFalse();\n\n      openmct.user.setProvider(new ExampleUserProvider(openmct));\n\n      expect(openmct.user.hasProvider()).toBeTrue();\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/user/UserProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A user provider is responsible for providing information about the currently\n * logged in user. This includes information about the user's roles, and whether\n * the user is currently logged in.\n */\nexport default class UserProvider {\n  /**\n   * @returns {Promise<User>} A promise that resolves with the currently logged in user\n   */\n  getCurrentUser() {}\n  /**\n   * @returns {boolean} true if a user is currently logged in, otherwise false\n   */\n  isLoggedIn() {}\n  /**\n   * @param {string} role\n   * @returns {Promise<boolean>} true if the current user has the given role\n   */\n  hasRole(role) {}\n}\n"
  },
  {
    "path": "src/api/user/UserStatusAPISpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\n\ndescribe('The User Status API', () => {\n  let openmct;\n  let userProvider;\n  let mockUser;\n\n  beforeEach(() => {\n    userProvider = jasmine.createSpyObj('userProvider', [\n      'setPollQuestion',\n      'getPollQuestion',\n      'getCurrentUser',\n      'getPossibleRoles',\n      'getPossibleStatuses',\n      'getAllStatusRoles',\n      'canSetPollQuestion',\n      'isLoggedIn',\n      'on'\n    ]);\n    openmct = createOpenMct();\n    mockUser = new openmct.user.User('test-user', 'A test user');\n    userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));\n    userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));\n    userProvider.getPossibleRoles.and.returnValue(Promise.resolve([]));\n    userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));\n    userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));\n    userProvider.isLoggedIn.and.returnValue(true);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the poll question', () => {\n    it('can be set via a user status provider if supported', () => {\n      openmct.user.setProvider(userProvider);\n      userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));\n\n      return openmct.user.status.setPollQuestion('This is a poll question').then(() => {\n        expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question');\n      });\n    });\n    // fit('emits an event when the poll question changes', () => {\n    //     const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback');\n    //     let pollQuestionListener;\n\n    //     userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));\n    //     userProvider.on.and.callFake((eventName, listener) => {\n    //         if (eventName === 'pollQuestionChange') {\n    //             pollQuestionListener = listener;\n    //         }\n    //     });\n\n    //     openmct.user.on('pollQuestionChange', pollQuestionChangeCallback);\n\n    //     openmct.user.setProvider(userProvider);\n\n    //     return openmct.user.status.setPollQuestion('This is a poll question').then(() => {\n    //         expect(pollQuestionListener).toBeDefined();\n    //         pollQuestionListener();\n    //         expect(pollQuestionChangeCallback).toHaveBeenCalled();\n\n    //         const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0];\n    //         expect(pollQuestion.question).toBe('This is a poll question');\n\n    //         openmct.user.off('pollQuestionChange', pollQuestionChangeCallback);\n    //     });\n    // });\n    it('cannot be set if the user is not permitted', () => {\n      openmct.user.setProvider(userProvider);\n      userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));\n\n      return openmct.user.status\n        .setPollQuestion('This is a poll question')\n        .catch((error) => {\n          expect(error).toBeInstanceOf(Error);\n        })\n        .finally(() => {\n          expect(userProvider.setPollQuestion).not.toHaveBeenCalled();\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "src/api/user/constants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const MULTIPLE_PROVIDER_ERROR = 'Only one user provider may be set at a time.';\nexport const NO_PROVIDER_ERROR = 'No user provider has been set.';\n\nexport const ACTIVE_ROLE_LOCAL_STORAGE_KEY = 'ACTIVE_USER_ROLE';\nexport const ACTIVE_ROLE_BROADCAST_CHANNEL_NAME = 'ActiveRoleChannel';\n"
  },
  {
    "path": "src/exporters/CSVExporter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport CSV from 'comma-separated-values';\nimport { saveAs } from 'file-saver';\n\nclass CSVExporter {\n  export(rows, options) {\n    let headers = (options && options.headers) || Object.keys(rows[0] || {}).sort();\n    let filename = (options && options.filename) || 'export.csv';\n    let csvText = new CSV(rows, { header: headers }).encode();\n    let blob = new Blob([csvText], { type: 'text/csv' });\n    saveAs(blob, filename);\n  }\n}\n\nexport default CSVExporter;\n"
  },
  {
    "path": "src/exporters/ImageExporter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Class defining an image exporter for JPG/PNG output.\n * Originally created by hudsonfoo on 09/02/16\n */\n\nfunction sanitizeFilename(filename) {\n  const replacedPeriods = filename.replace(/\\./g, '_');\n  const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\\-.\\s]/g, '');\n\n  // Handle leading/trailing spaces and periods\n  const trimmedFilename = safeFilename.trim().replace(/^\\.+|\\.+$/g, '');\n\n  return trimmedFilename;\n}\n\nimport { saveAs } from 'file-saver';\nimport html2canvas from 'html2canvas';\nimport { v4 as uuid } from 'uuid';\n\nclass ImageExporter {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n  /**\n   * Converts an HTML element into a PNG or JPG Blob.\n   * @private\n   * @param {node} element that will be converted to an image\n   * @param {Object} options Image options.\n   * @returns {promise}\n   */\n  renderElement(element, { imageType, className, thumbnailSize }) {\n    const self = this;\n    const overlays = this.openmct.overlays;\n    const dialog = overlays.dialog({\n      iconClass: 'info',\n      message: 'Capturing image, please wait...',\n      buttons: [\n        {\n          label: 'Cancel',\n          emphasis: true,\n          callback: function () {\n            dialog.dismiss();\n          }\n        }\n      ]\n    });\n\n    let mimeType = 'image/png';\n    if (imageType === 'jpg') {\n      mimeType = 'image/jpeg';\n    }\n\n    let exportId = undefined;\n    let oldId = undefined;\n    if (className) {\n      const newUUID = uuid();\n      exportId = `$export-element-${newUUID}`;\n      oldId = element.id;\n      element.id = exportId;\n    }\n\n    return html2canvas(element, {\n      useCORS: true,\n      allowTaint: true,\n      logging: false,\n      onclone: function (document) {\n        if (className) {\n          const clonedElement = document.getElementById(exportId);\n          clonedElement.classList.add(className);\n        }\n\n        element.id = oldId;\n      },\n      removeContainer: true // Set to false to debug what html2canvas renders\n    })\n      .then((canvas) => {\n        dialog.dismiss();\n\n        return new Promise(function (resolve, reject) {\n          if (thumbnailSize) {\n            const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);\n\n            return canvas.toBlob(\n              (blob) =>\n                resolve({\n                  blob,\n                  thumbnail\n                }),\n              mimeType\n            );\n          }\n\n          return canvas.toBlob((blob) => resolve({ blob }), mimeType);\n        });\n      })\n      .catch((error) => {\n        dialog.dismiss();\n\n        console.error('error capturing image', error);\n        const errorDialog = overlays.dialog({\n          iconClass: 'error',\n          message: 'Image was not captured successfully!',\n          buttons: [\n            {\n              label: 'Ok',\n              emphasis: true,\n              callback: function () {\n                errorDialog.dismiss();\n              }\n            }\n          ]\n        });\n      });\n  }\n\n  getThumbnail(canvas, mimeType, size) {\n    const thumbnailCanvas = document.createElement('canvas');\n    thumbnailCanvas.setAttribute('width', size.width);\n    thumbnailCanvas.setAttribute('height', size.height);\n    const ctx = thumbnailCanvas.getContext('2d');\n    ctx.globalCompositeOperation = 'copy';\n    ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);\n\n    return thumbnailCanvas.toDataURL(mimeType);\n  }\n\n  /**\n   * Takes a screenshot of a DOM node and exports to JPG.\n   * @param {node} element to be exported\n   * @param {string} filename the exported image\n   * @param {string} className to be added to element before capturing (optional)\n   * @returns {promise}\n   */\n  async exportJPG(element, filename, className) {\n    const processedFilename = sanitizeFilename(filename);\n\n    const img = await this.renderElement(element, {\n      imageType: 'jpg',\n      className\n    });\n    saveAs(img.blob, processedFilename);\n  }\n\n  /**\n   * Takes a screenshot of a DOM node and exports to PNG.\n   * @param {node} element to be exported\n   * @param {string} filename the exported image\n   * @param {string} className to be added to element before capturing (optional)\n   * @returns {promise}\n   */\n  async exportPNG(element, filename, className) {\n    const processedFilename = sanitizeFilename(filename);\n\n    const img = await this.renderElement(element, {\n      imageType: 'png',\n      className\n    });\n    saveAs(img.blob, processedFilename);\n  }\n\n  /**\n   * Takes a screenshot of a DOM node in PNG format.\n   * @param {node} element to be exported\n   * @param {string} filename the exported image\n   * @returns {promise}\n   */\n\n  exportPNGtoSRC(element, options) {\n    return this.renderElement(element, {\n      imageType: 'png',\n      ...options\n    });\n  }\n}\n\nexport default ImageExporter;\n"
  },
  {
    "path": "src/exporters/ImageExporterSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../utils/testing.js';\nimport ImageExporter from './ImageExporter.js';\n\ndescribe('The Image Exporter', () => {\n  let openmct;\n  let imageExporter;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('basic instantiation', () => {\n    it('can be instantiated', () => {\n      imageExporter = new ImageExporter(openmct);\n\n      expect(imageExporter).not.toEqual(null);\n    });\n    it('can render an element to a blob', async () => {\n      const mockHeadElement = document.createElement('h1');\n      const mockTextNode = document.createTextNode('foo bar');\n      mockHeadElement.appendChild(mockTextNode);\n      document.body.appendChild(mockHeadElement);\n      imageExporter = new ImageExporter(openmct);\n      const returnedBlob = await imageExporter.renderElement(document.body, {\n        imageType: 'png'\n      });\n      expect(returnedBlob).not.toEqual(null);\n      expect(returnedBlob.blob).not.toEqual(null);\n      expect(returnedBlob.blob).toBeInstanceOf(Blob);\n    });\n  });\n});\n"
  },
  {
    "path": "src/exporters/JSONExporter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { saveAs } from 'file-saver';\n\nclass JSONExporter {\n  export(obj, options) {\n    let filename = (options && options.filename) || 'test-export.json';\n    let jsonText = JSON.stringify(obj);\n    let blob = new Blob([jsonText], { type: 'application/json' });\n    saveAs(blob, filename);\n  }\n}\n\nexport default JSONExporter;\n"
  },
  {
    "path": "src/plugins/CouchDBSearchFolder/plugin.js",
    "content": "export default function (folderName, couchPlugin, searchFilter) {\n  const DEFAULT_NAME = 'CouchDB Documents';\n\n  return function install(openmct) {\n    const couchProvider = couchPlugin.couchProvider;\n    //replace any non-letter/non-number with a hyphen\n    const couchSearchId = (folderName || DEFAULT_NAME).replace(/[^a-zA-Z0-9]/g, '-');\n    const couchSearchName = `couch-search-${couchSearchId}`;\n\n    openmct.objects.addRoot({\n      namespace: couchSearchName,\n      key: couchSearchName\n    });\n\n    openmct.objects.addProvider(couchSearchName, {\n      get(identifier) {\n        if (identifier.key !== couchSearchName) {\n          return undefined;\n        } else {\n          return Promise.resolve({\n            identifier,\n            type: 'folder',\n            name: folderName || DEFAULT_NAME,\n            location: 'ROOT'\n          });\n        }\n      },\n      search() {\n        return Promise.resolve([]);\n      }\n    });\n\n    openmct.composition.addProvider({\n      appliesTo(domainObject) {\n        return (\n          domainObject.identifier.namespace === couchSearchName &&\n          domainObject.identifier.key === couchSearchName\n        );\n      },\n      load() {\n        let searchResults;\n\n        if (searchFilter.viewName !== undefined) {\n          // Use a view to search, instead of an _all_docs find\n          searchResults = couchProvider.getObjectsByView(searchFilter);\n        } else {\n          // Use the _find endpoint to search _all_docs\n          searchResults = couchProvider.getObjectsByFilter(searchFilter);\n        }\n\n        return searchResults;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/CouchDBSearchFolder/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport CouchDBSearchFolderPlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  let identifier = {\n    namespace: 'couch-search-CouchDB-Documents',\n    key: 'couch-search-CouchDB-Documents'\n  };\n  let testPath = '/test/db';\n  let openmct;\n  let composition;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n\n    let couchPlugin = openmct.plugins.CouchDB(testPath);\n    openmct.install(couchPlugin);\n    openmct.install(\n      new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {\n        selector: {\n          model: {\n            type: 'plan'\n          }\n        }\n      })\n    );\n\n    spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(\n      Promise.resolve([\n        {\n          identifier: {\n            key: '1',\n            namespace: 'mct'\n          }\n        },\n        {\n          identifier: {\n            key: '2',\n            namespace: 'mct'\n          }\n        }\n      ])\n    );\n\n    spyOn(couchPlugin.couchProvider, 'get').and.callFake((id) => {\n      return Promise.resolve({\n        identifier: id\n      });\n    });\n\n    return new Promise((resolve) => {\n      openmct.once('start', resolve);\n      openmct.startHeadless();\n    }).then(() => {\n      composition = openmct.composition.get({ identifier });\n    });\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('provides a folder to hold plans', () => {\n    return openmct.objects.get(identifier).then((object) => {\n      expect(object).toEqual({\n        identifier,\n        type: 'folder',\n        name: 'CouchDB Documents',\n        location: 'ROOT'\n      });\n    });\n  });\n\n  it('provides composition for couch search folders', () => {\n    return composition.load().then((objects) => {\n      expect(objects.length).toEqual(2);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/DeviceClassifier/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Agent from '../../utils/agent/Agent.js';\nimport DeviceClassifier from './src/DeviceClassifier.js';\n\nexport default () => {\n  return (openmct) => {\n    openmct.on('start', () => {\n      const agent = new Agent(window);\n      DeviceClassifier(agent, window.document);\n    });\n  };\n};\n"
  },
  {
    "path": "src/plugins/DeviceClassifier/src/DeviceClassifier.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Runs at application startup and adds a subset of the following\n * CSS classes to the body of the document, depending on device\n * attributes:\n *\n * * `mobile`: Phones or tablets.\n * * `phone`: Phones specifically.\n * * `tablet`: Tablets specifically.\n * * `desktop`: Non-mobile devices.\n * * `portrait`: Devices in a portrait-style orientation.\n * * `landscape`: Devices in a landscape-style orientation.\n * * `touch`: Device supports touch events.\n *\n * @param {utils/agent/Agent} agent\n *        the service used to examine the user agent\n * @param document the HTML DOM document object\n * @constructor\n */\nimport DeviceMatchers from './DeviceMatchers.js';\n\nexport default (agent, document) => {\n  const body = document.body;\n\n  Object.keys(DeviceMatchers).forEach((key, index, array) => {\n    if (DeviceMatchers[key](agent)) {\n      body.classList.add(key);\n    }\n  });\n\n  if (agent.isMobile()) {\n    const mediaQuery = window.matchMedia('(orientation: landscape)');\n    function eventHandler(event) {\n      if (event.matches) {\n        body.classList.remove('portrait');\n        body.classList.add('landscape');\n      } else {\n        body.classList.remove('landscape');\n        body.classList.add('portrait');\n      }\n    }\n\n    if (mediaQuery.addEventListener) {\n      mediaQuery.addEventListener(`change`, eventHandler);\n    } else {\n      // Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16\n      mediaQuery.addListener(eventHandler);\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/DeviceClassifier/src/DeviceClassifierSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport DeviceClassifier from './DeviceClassifier.js';\nimport DeviceMatchers from './DeviceMatchers.js';\n\nconst AGENT_METHODS = ['isMobile', 'isPhone', 'isTablet', 'isPortrait', 'isLandscape', 'isTouch'];\nconst TEST_PERMUTATIONS = [\n  ['isMobile', 'isPhone', 'isTouch', 'isPortrait'],\n  ['isMobile', 'isPhone', 'isTouch', 'isLandscape'],\n  ['isMobile', 'isTablet', 'isTouch', 'isPortrait'],\n  ['isMobile', 'isTablet', 'isTouch', 'isLandscape'],\n  ['isTouch'],\n  []\n];\n\ndescribe('DeviceClassifier', function () {\n  let mockAgent;\n  let mockDocument;\n  let mockClassList;\n\n  beforeEach(function () {\n    mockAgent = jasmine.createSpyObj('agent', AGENT_METHODS);\n\n    mockClassList = jasmine.createSpyObj('classList', ['add']);\n\n    mockDocument = jasmine.createSpyObj('document', {}, { body: { classList: mockClassList } });\n\n    AGENT_METHODS.forEach(function (m) {\n      mockAgent[m].and.returnValue(false);\n    });\n  });\n\n  TEST_PERMUTATIONS.forEach(function (trueMethods) {\n    const summary =\n      trueMethods.length === 0\n        ? 'device has no detected characteristics'\n        : 'device ' + trueMethods.join(', ');\n\n    describe('when ' + summary, function () {\n      beforeEach(function () {\n        trueMethods.forEach(function (m) {\n          mockAgent[m].and.returnValue(true);\n        });\n\n        DeviceClassifier(mockAgent, mockDocument);\n      });\n\n      it('adds classes for matching, detected characteristics', function () {\n        Object.keys(DeviceMatchers)\n          .filter(function (m) {\n            return DeviceMatchers[m](mockAgent);\n          })\n          .forEach(function (key) {\n            expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key);\n          });\n      });\n\n      it('does not add classes for non-matching characteristics', function () {\n        Object.keys(DeviceMatchers)\n          .filter(function (m) {\n            return !DeviceMatchers[m](mockAgent);\n          })\n          .forEach(function (key) {\n            expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith(key);\n          });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/DeviceClassifier/src/DeviceMatchers.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * An object containing key-value pairs, where keys are symbolic of\n * device attributes, and values are functions that take the\n * `agent` as inputs and return boolean values indicating\n * whether or not the current device has these attributes.\n *\n * For internal use by the mobile support bundle.\n *\n * @private\n */\n\nexport default {\n  mobile: function (agent) {\n    return agent.isMobile();\n  },\n  phone: function (agent) {\n    return agent.isPhone();\n  },\n  tablet: function (agent) {\n    return agent.isTablet();\n  },\n  desktop: function (agent) {\n    return !agent.isMobile();\n  },\n  portrait: function (agent) {\n    return agent.isPortrait();\n  },\n  landscape: function (agent) {\n    return agent.isLandscape();\n  },\n  touch: function (agent) {\n    return agent.isTouch();\n  }\n};\n"
  },
  {
    "path": "src/plugins/DeviceClassifier/src/DeviceMatchersSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport DeviceMatchers from './DeviceMatchers.js';\n\ndescribe('DeviceMatchers', function () {\n  let mockAgent;\n\n  beforeEach(function () {\n    mockAgent = jasmine.createSpyObj('agent', [\n      'isMobile',\n      'isPhone',\n      'isTablet',\n      'isPortrait',\n      'isLandscape',\n      'isTouch'\n    ]);\n  });\n\n  it('detects when a device is a desktop device', function () {\n    mockAgent.isMobile.and.returnValue(false);\n    expect(DeviceMatchers.desktop(mockAgent)).toBe(true);\n    mockAgent.isMobile.and.returnValue(true);\n    expect(DeviceMatchers.desktop(mockAgent)).toBe(false);\n  });\n\n  function method(deviceType) {\n    return 'is' + deviceType[0].toUpperCase() + deviceType.slice(1);\n  }\n\n  ['mobile', 'phone', 'tablet', 'landscape', 'portrait', 'landscape', 'touch'].forEach(\n    function (deviceType) {\n      it('detects when a device is a ' + deviceType + ' device', function () {\n        mockAgent[method(deviceType)].and.returnValue(true);\n        expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);\n        mockAgent[method(deviceType)].and.returnValue(false);\n        expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);\n      });\n    }\n  );\n});\n"
  },
  {
    "path": "src/plugins/ISOTimeFormat/ISOTimeFormat.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class ISOTimeFormat {\n  constructor() {\n    this.key = 'iso';\n  }\n\n  format(value) {\n    if (value !== undefined) {\n      return new Date(value).toISOString();\n    } else {\n      return value;\n    }\n  }\n\n  parse(text) {\n    if (typeof text === 'number' || text === undefined) {\n      return text;\n    }\n\n    return Date.parse(text);\n  }\n\n  validate(text) {\n    return !isNaN(Date.parse(text));\n  }\n}\n"
  },
  {
    "path": "src/plugins/ISOTimeFormat/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ISOTimeFormat from './ISOTimeFormat.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.telemetry.addFormat(new ISOTimeFormat());\n  };\n}\n"
  },
  {
    "path": "src/plugins/ISOTimeFormat/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ISOTimeFormat from './ISOTimeFormat.js';\n\ndescribe('the plugin', () => {\n  const ISO_KEY = 'iso';\n  const JUNK = 'junk';\n  const MOON_LANDING_TIMESTAMP = -14256000000;\n  const MOON_LANDING_DATESTRING = '1969-07-20T00:00:00.000Z';\n  let isoFormatter;\n\n  beforeEach(() => {\n    isoFormatter = new ISOTimeFormat();\n  });\n\n  describe('creates a new ISO based formatter', function () {\n    it(\"with the key 'iso'\", () => {\n      expect(isoFormatter.key).toBe(ISO_KEY);\n    });\n\n    it('that will format a timestamp in ISO standard format', () => {\n      expect(isoFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(MOON_LANDING_DATESTRING);\n    });\n\n    it('that will parse an ISO Date String into milliseconds', () => {\n      expect(isoFormatter.parse(MOON_LANDING_DATESTRING)).toBe(MOON_LANDING_TIMESTAMP);\n    });\n\n    it('that will validate correctly', () => {\n      expect(isoFormatter.validate(MOON_LANDING_DATESTRING)).toBe(true);\n      expect(isoFormatter.validate(JUNK)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function ladTableCompositionPolicy(openmct) {\n  return function (parent, child) {\n    if (parent.type === 'LadTable') {\n      return openmct.telemetry.isTelemetryObject(child);\n    } else if (parent.type === 'LadTableSet') {\n      return child.type === 'LadTable';\n    }\n\n    return true;\n  };\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableConfiguration.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { markRaw } from 'vue';\nexport default class LADTableConfiguration extends EventEmitter {\n  constructor(domainObject, openmct) {\n    super();\n\n    this.domainObject = domainObject;\n\n    // Prevent Vue from making this a Proxy, otherwise\n    // it cannot access any private methods (like #mutate()).\n    this.openmct = markRaw(openmct);\n\n    this.objectMutated = this.objectMutated.bind(this);\n    this.unlistenFromMutation = openmct.objects.observe(\n      domainObject,\n      'configuration',\n      this.objectMutated\n    );\n  }\n\n  getConfiguration() {\n    const configuration = this.domainObject.configuration ?? {};\n    configuration.hiddenColumns = configuration.hiddenColumns ?? {};\n    configuration.isFixedLayout = configuration.isFixedLayout ?? true;\n    configuration.objectStyles = configuration.objectStyles ?? {};\n\n    return configuration;\n  }\n\n  updateConfiguration(configuration) {\n    this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);\n  }\n\n  objectMutated(configuration) {\n    if (configuration !== undefined) {\n      this.emit('change', configuration);\n    }\n  }\n\n  destroy() {\n    this.unlistenFromMutation();\n  }\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableConfigurationViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport LadTableConfiguration from './components/LadTableConfiguration.vue';\n\nexport default function LADTableConfigurationViewProvider(openmct) {\n  return {\n    key: 'lad-table-configuration',\n    name: 'Config',\n    canView(selection) {\n      if (selection.length !== 1 || selection[0].length === 0) {\n        return false;\n      }\n\n      const object = selection[0][0].context.item;\n\n      return object?.type === 'LadTable' || object?.type === 'LadTableSet';\n    },\n    view(selection) {\n      let _destroy = null;\n\n      return {\n        show(element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                LadTableConfiguration\n              },\n              provide: {\n                openmct\n              },\n              template: '<LadTableConfiguration />'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority() {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy() {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableSetViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LadTableSetView from './LadTableSetView.js';\n\nexport default function LADTableSetViewProvider(openmct) {\n  return {\n    key: 'LadTableSet',\n    name: 'LAD Table Set',\n    cssClass: 'icon-tabular-lad-set',\n    canView: function (domainObject) {\n      return domainObject.type === 'LadTableSet';\n    },\n    canEdit: function (domainObject) {\n      return domainObject.type === 'LadTableSet';\n    },\n    view: function (domainObject, objectPath) {\n      return new LadTableSetView(openmct, domainObject, objectPath);\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport LadTable from './components/LadTable.vue';\nimport LADTableConfiguration from './LADTableConfiguration.js';\n\nexport default class LADTableView {\n  constructor(openmct, domainObject, objectPath) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this.component = null;\n    this._destroy = null;\n  }\n\n  show(element, isEditing, { renderWhenVisible }) {\n    let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);\n\n    const { vNode, destroy } = mount(\n      {\n        el: element,\n        components: {\n          LadTable\n        },\n        provide: {\n          openmct: this.openmct,\n          currentView: this,\n          ladTableConfiguration,\n          renderWhenVisible\n        },\n        data: () => {\n          return {\n            domainObject: this.domainObject,\n            objectPath: this.objectPath\n          };\n        },\n        template:\n          '<lad-table ref=\"ladTable\" :domain-object=\"domainObject\" :object-path=\"objectPath\"></lad-table>'\n      },\n      {\n        app: this.openmct.app,\n        element\n      }\n    );\n    this.component = vNode.componentInstance;\n    this._destroy = destroy;\n  }\n\n  getViewContext() {\n    if (!this.component) {\n      return {};\n    }\n\n    return this.component.$refs.ladTable.getViewContext();\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n    }\n    this.component = null;\n  }\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LADTableViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LADTableView from './LADTableView.js';\n\nexport default class LADTableViewProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.name = 'LAD Table';\n    this.key = 'LadTable';\n    this.cssClass = 'icon-tabular-lad';\n  }\n\n  canView(domainObject) {\n    const supportsComposition = this.openmct.composition.supportsComposition(domainObject);\n    const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);\n    const isLadTable = domainObject.type === 'LadTable';\n    const isConditionSet = domainObject.type === 'conditionSet';\n\n    return !isConditionSet && (isLadTable || (providesTelemetry && supportsComposition));\n  }\n\n  canEdit(domainObject) {\n    return domainObject.type === 'LadTable';\n  }\n\n  view(domainObject, objectPath) {\n    return new LADTableView(this.openmct, domainObject, objectPath);\n  }\n}\n"
  },
  {
    "path": "src/plugins/LADTable/LadTableSetView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport LadTableSet from './components/LadTableSet.vue';\nimport LADTableConfiguration from './LADTableConfiguration.js';\n\nexport default class LadTableSetView {\n  constructor(openmct, domainObject, objectPath) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this._destroy = null;\n    this.component = null;\n  }\n\n  show(element, isEditing, { renderWhenVisible }) {\n    let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);\n\n    const { vNode, destroy } = mount(\n      {\n        el: element,\n        components: {\n          LadTableSet\n        },\n        provide: {\n          openmct: this.openmct,\n          objectPath: this.objectPath,\n          currentView: this,\n          ladTableConfiguration,\n          renderWhenVisible\n        },\n        data: () => {\n          return {\n            domainObject: this.domainObject\n          };\n        },\n        template: '<lad-table-set ref=\"ladTableSet\" :domain-object=\"domainObject\"></lad-table-set>'\n      },\n      {\n        app: this.openmct.app,\n        element\n      }\n    );\n    this._destroy = destroy;\n    this.component = vNode.componentInstance;\n  }\n\n  getViewContext() {\n    if (!this.component) {\n      return {};\n    }\n\n    return this.component.$refs.ladTableSet.getViewContext();\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/LADTable/ViewActions.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst expandColumns = {\n  name: 'Expand Columns',\n  key: 'lad-expand-columns',\n  description: 'Increase column widths to fit currently available data.',\n  cssClass: 'icon-arrows-right-left labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().toggleFixedLayout();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst autosizeColumns = {\n  name: 'Autosize Columns',\n  key: 'lad-autosize-columns',\n  description: 'Automatically size columns to fit the table into the available space.',\n  cssClass: 'icon-expand labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().toggleFixedLayout();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst viewActions = [expandColumns, autosizeColumns];\n\nviewActions.forEach((action) => {\n  action.appliesTo = (objectPath, view = {}) => {\n    const viewContext = view.getViewContext && view.getViewContext();\n    if (!viewContext) {\n      return false;\n    }\n\n    return viewContext.type === 'lad-table';\n  };\n});\n\nexport default viewActions;\n"
  },
  {
    "path": "src/plugins/LADTable/components/LadRow.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <tr\n    ref=\"tableRow\"\n    class=\"js-lad-table__body__row c-table__selectable-row\"\n    aria-label=\"lad row\"\n    @click=\"clickedRow\"\n    @contextmenu.prevent=\"showContextMenu\"\n  >\n    <td\n      ref=\"tableCell\"\n      scope=\"row\"\n      aria-label=\"lad name\"\n      class=\"js-first-data\"\n      @mouseover.ctrl=\"showToolTip\"\n      @mouseleave=\"hideToolTip\"\n    >\n      {{ domainObject.name }}\n    </td>\n    <td v-if=\"showTimestamp\" aria-label=\"lad timestamp\" class=\"js-second-data\">\n      {{ formattedTimestamp }}\n    </td>\n    <td aria-label=\"lad value\" class=\"js-third-data\" :class=\"valueClasses\">{{ value }}</td>\n    <td v-if=\"hasUnits\" class=\"js-units\">\n      {{ unit }}\n    </td>\n    <td v-if=\"showType\" aria-label=\"lad type\" class=\"js-type-data\">{{ typeLabel }}</td>\n    <td\n      v-for=\"limit in formattedLimitValues\"\n      :key=\"limit.key\"\n      aria-label=\"lad limit value\"\n      class=\"js-limit-data\"\n    >\n      {{ limit.value }}\n    </td>\n  </tr>\n</template>\n\n<script>\nimport { objectPathToUrl } from '/src/tools/url.js';\nimport { REMOVE_ACTION_KEY } from '@/plugins/remove/RemoveAction.js';\nimport { VIEW_DATUM_ACTION_KEY } from '@/plugins/viewDatumAction/ViewDatumAction.js';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\nimport { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\n\nconst BLANK_VALUE = '---';\nconst CONTEXT_MENU_ACTIONS = [\n  VIEW_DATUM_ACTION_KEY,\n  VIEW_HISTORICAL_DATA_ACTION_KEY,\n  REMOVE_ACTION_KEY\n];\n\nexport default {\n  mixins: [tooltipHelpers],\n  inject: ['openmct', 'currentView', 'renderWhenVisible'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    pathToTable: {\n      type: Array,\n      required: true\n    },\n    hasUnits: {\n      type: Boolean,\n      required: true\n    },\n    isStale: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    configuration: {\n      type: Object,\n      required: true\n    },\n    limitDefinition: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    limitColumnNames: {\n      // for ordering\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['row-context-click'],\n  data() {\n    return {\n      datum: undefined,\n      timestamp: undefined,\n      timestampKey: undefined,\n      valueKey: null,\n      composition: [],\n      unit: ''\n    };\n  },\n  computed: {\n    value() {\n      if (!this.datum || this.isAggregate) {\n        return BLANK_VALUE;\n      }\n\n      return this.formats[this.valueKey].format(this.datum);\n    },\n    formattedLimitValues() {\n      if (!this.valueKey) {\n        return [];\n      }\n      return this.limitColumnNames.map((column) => {\n        if (this.limitDefinition?.[column.key]) {\n          const highValue = this.limitDefinition[column.key].high[this.valueKey];\n          const lowValue = this.limitDefinition[column.key].low[this.valueKey];\n          return {\n            key: column.key,\n            value: `${lowValue} → ${highValue}`\n          };\n        } else {\n          return {\n            key: column.key,\n            value: BLANK_VALUE\n          };\n        }\n      });\n    },\n    typeLabel() {\n      if (this.isAggregate) {\n        return 'Aggregate';\n      }\n\n      return 'Telemetry';\n    },\n    isAggregate() {\n      return this.composition && this.composition.length > 0;\n    },\n    valueClasses() {\n      let classes = [];\n\n      if (this.isStale) {\n        classes.push('is-stale');\n      }\n\n      if (this.datum) {\n        const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);\n\n        if (limit) {\n          classes.push(limit.cssClass);\n        }\n      }\n\n      return classes;\n    },\n    formattedTimestamp() {\n      if (!this.timestamp || this.isAggregate) {\n        return BLANK_VALUE;\n      }\n\n      return this.timeSystemFormat.format(this.timestamp);\n    },\n    timeSystemFormat() {\n      if (!this.formats[this.timestampKey]) {\n        console.warn(\n          `No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`\n        );\n      }\n\n      return this.formats[this.timestampKey];\n    },\n    objectPath() {\n      return [this.domainObject, ...this.pathToTable];\n    },\n    showTimestamp() {\n      return !this.configuration?.hiddenColumns?.timestamp;\n    },\n    showType() {\n      return !this.configuration?.hiddenColumns?.type;\n    }\n  },\n  async mounted() {\n    this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n    this.formats = this.openmct.telemetry.getFormatMap(this.metadata);\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    const compositionCollection = this.openmct.composition.get(this.domainObject);\n    if (compositionCollection) {\n      this.composition = await compositionCollection.load();\n    }\n\n    this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n\n    this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);\n\n    this.openmct.time.on('timeSystem', this.updateTimeSystem);\n\n    this.timestampKey = this.openmct.time.getTimeSystem().key;\n\n    this.valueMetadata = undefined;\n\n    if (this.metadata) {\n      this.valueMetadata =\n        this.metadata.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);\n    }\n\n    this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;\n\n    this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {\n      size: 1,\n      strategy: 'latest',\n      timeContext: this.timeContext\n    });\n    this.telemetryCollection.on('add', this.setLatestValues);\n    this.telemetryCollection.on('clear', this.resetValues);\n    this.telemetryCollection.load();\n\n    if (this.hasUnits) {\n      this.setUnit();\n    }\n\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n  },\n  unmounted() {\n    this.openmct.time.off('timeSystem', this.updateTimeSystem);\n    this.telemetryCollection.off('add', this.setLatestValues);\n    this.telemetryCollection.off('clear', this.resetValues);\n\n    this.telemetryCollection.destroy();\n  },\n  methods: {\n    updateView() {\n      if (!this.updatingView) {\n        this.updatingView = this.renderWhenVisible(() => {\n          this.timestamp = this.getParsedTimestamp(this.latestDatum);\n          this.datum = this.latestDatum;\n          this.updatingView = false;\n        });\n      }\n    },\n    clickedRow(event) {\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        this.preview(this.objectPath);\n      } else {\n        const resultUrl = objectPathToUrl(this.openmct, this.objectPath);\n        this.openmct.router.navigate(resultUrl);\n      }\n    },\n    preview(objectPath) {\n      if (this.previewAction.appliesTo(objectPath)) {\n        this.previewAction.invoke(objectPath);\n      }\n    },\n    setLatestValues(data) {\n      this.latestDatum = data[data.length - 1];\n      this.updateView();\n    },\n    updateTimeSystem(timeSystem) {\n      this.timestampKey = timeSystem.key;\n    },\n    updateViewContext() {\n      this.$emit('row-context-click', {\n        viewHistoricalData: true,\n        viewDatumAction: true,\n        getDatum: () => {\n          return this.datum;\n        }\n      });\n    },\n    showContextMenu(event) {\n      this.updateViewContext();\n\n      const actions = CONTEXT_MENU_ACTIONS.map((key) => this.openmct.actions.getAction(key));\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        actions,\n        this.objectPath,\n        this.currentView\n      );\n      if (menuItems.length) {\n        this.openmct.menus.showMenu(event.x, event.y, menuItems);\n      }\n    },\n    resetValues() {\n      this.timestamp = undefined;\n      this.datum = undefined;\n    },\n    getParsedTimestamp(timestamp) {\n      if (this.timeSystemFormat) {\n        return this.timeSystemFormat.parse(timestamp);\n      }\n    },\n    setUnit() {\n      this.unit = this.valueMetadata ? this.valueMetadata.unit : '';\n    },\n    firstNonDomainAttribute(metadata) {\n      return metadata\n        .values()\n        .find((metadatum) => metadatum.hints.domain === undefined && metadatum.key !== 'name');\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'tableCell');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/LADTable/components/LadTable.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    id=\"lad-table-drop-area\"\n    class=\"c-lad-table-wrapper u-style-receiver js-style-receiver\"\n    :class=\"staleClass\"\n  >\n    <table class=\"c-table c-lad-table\" :class=\"applyLayoutClass\">\n      <thead>\n        <tr>\n          <th scope=\"col\">Name</th>\n          <th v-if=\"showTimestamp\" scope=\"col\">Timestamp</th>\n          <th scope=\"col\">Value</th>\n          <th v-if=\"hasUnits\" scope=\"col\">Units</th>\n          <th v-if=\"showType\" scope=\"col\">Type</th>\n          <th v-for=\"limitColumn in limitColumnNames\" :key=\"limitColumn.key\" scope=\"col\">\n            {{ limitColumn.label }}\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        <LadRow\n          v-for=\"ladRow in items\"\n          :key=\"ladRow.key\"\n          :domain-object=\"ladRow.domainObject\"\n          :limit-definition=\"ladRow.limitDefinition\"\n          :limit-column-names=\"limitColumnNames\"\n          :path-to-table=\"objectPath\"\n          :has-units=\"hasUnits\"\n          :is-stale=\"staleObjects.includes(ladRow.key)\"\n          :configuration=\"configuration\"\n          @row-context-click=\"updateViewContext\"\n        />\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script>\nimport { nextTick, toRaw } from 'vue';\n\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport LadRow from './LadRow.vue';\n\nexport default {\n  components: {\n    LadRow\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'currentView', 'ladTableConfiguration'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  data() {\n    return {\n      items: [],\n      viewContext: {},\n      configuration: this.ladTableConfiguration.getConfiguration()\n    };\n  },\n  computed: {\n    hasUnits() {\n      let itemsWithUnits = this.items.filter((item) => {\n        let metadata = this.openmct.telemetry.getMetadata(item.domainObject);\n        const valueMetadatas = metadata ? metadata.valueMetadatas : [];\n\n        return this.metadataHasUnits(valueMetadatas);\n      });\n\n      return itemsWithUnits.length !== 0 && !this.configuration?.hiddenColumns?.units;\n    },\n    limitColumnNames() {\n      const limitDefinitions = [];\n\n      this.items.forEach((item) => {\n        if (item.limitDefinition) {\n          const limits = Object.keys(item.limitDefinition);\n          limits.forEach((limit) => {\n            const limitAlreadyAdded = limitDefinitions.some((limitDef) => limitDef.key === limit);\n            const limitHidden = this.configuration?.hiddenColumns?.[limit];\n            if (!limitAlreadyAdded && !limitHidden) {\n              limitDefinitions.push({ label: `Limit ${limit}`, key: limit });\n            }\n          });\n        }\n      });\n      return limitDefinitions;\n    },\n    showTimestamp() {\n      return !this.configuration?.hiddenColumns?.timestamp;\n    },\n    showType() {\n      return !this.configuration?.hiddenColumns?.type;\n    },\n    staleClass() {\n      return this.isStale ? 'is-stale' : '';\n    },\n    applyLayoutClass() {\n      if (this.configuration.isFixedLayout) {\n        return 'fixed-layout';\n      }\n\n      return '';\n    }\n  },\n  watch: {\n    configuration: {\n      handler(newVal) {\n        if (this.viewActionsCollection) {\n          if (newVal.isFixedLayout) {\n            this.viewActionsCollection.show(['lad-expand-columns']);\n            this.viewActionsCollection.hide(['lad-autosize-columns']);\n          } else {\n            this.viewActionsCollection.show(['lad-autosize-columns']);\n            this.viewActionsCollection.hide(['lad-expand-columns']);\n          }\n        }\n      },\n      deep: true\n    }\n  },\n  async mounted() {\n    this.ladTableConfiguration.on('change', this.handleConfigurationChange);\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('add', this.addItem);\n    this.composition.on('remove', this.removeItem);\n    this.composition.on('reorder', this.reorder);\n    this.composition.load();\n    await nextTick();\n    this.viewActionsCollection = this.openmct.actions.getActionsCollection(\n      this.objectPath,\n      this.currentView\n    );\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n\n    this.initializeViewActions();\n  },\n  unmounted() {\n    this.ladTableConfiguration.off('change', this.handleConfigurationChange);\n\n    this.composition.off('add', this.addItem);\n    this.composition.off('remove', this.removeItem);\n    this.composition.off('reorder', this.reorder);\n  },\n  methods: {\n    async addItem(domainObject) {\n      let item = {};\n      item.domainObject = domainObject;\n      item.key = this.openmct.objects.makeKeyString(domainObject.identifier);\n      item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();\n\n      this.items.push(item);\n      this.subscribeToStaleness(domainObject);\n    },\n    async removeItem(identifier) {\n      const keystring = this.openmct.objects.makeKeyString(identifier);\n\n      const index = this.items.findIndex((item) => keystring === item.key);\n      this.items.splice(index, 1);\n\n      const domainObject = await this.openmct.objects.get(keystring);\n      this.triggerUnsubscribeFromStaleness(domainObject);\n    },\n    reorder(reorderPlan) {\n      const oldItems = this.items.slice();\n      reorderPlan.forEach((reorderEvent) => {\n        this.items[reorderEvent.newIndex] = oldItems[reorderEvent.oldIndex];\n      });\n    },\n    metadataHasUnits(valueMetadatas) {\n      const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);\n\n      return metadataWithUnits.length > 0;\n    },\n    handleConfigurationChange(configuration) {\n      this.configuration = configuration;\n    },\n    updateViewContext(rowContext) {\n      this.viewContext.row = rowContext;\n    },\n    getViewContext() {\n      return {\n        ...this.viewContext,\n        type: 'lad-table',\n        toggleFixedLayout: this.toggleFixedLayout\n      };\n    },\n    toggleFixedLayout() {\n      const config = structuredClone(toRaw(this.configuration));\n\n      config.isFixedLayout = !this.configuration.isFixedLayout;\n      this.ladTableConfiguration.updateConfiguration(config);\n    },\n    initializeViewActions() {\n      if (this.configuration.isFixedLayout) {\n        this.viewActionsCollection.show(['lad-expand-columns']);\n        this.viewActionsCollection.hide(['lad-autosize-columns']);\n      } else {\n        this.viewActionsCollection.hide(['lad-expand-columns']);\n        this.viewActionsCollection.show(['lad-autosize-columns']);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/LADTable/components/LadTableConfiguration.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspect-properties\">\n    <div class=\"c-inspect-properties__header\">Table Column Visibility</div>\n    <ul class=\"c-inspect-properties__section\">\n      <li v-for=\"(title, key) in headers\" :key=\"key\" class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Show or hide column\">\n          <label :for=\"key + 'ColumnControl'\">{{ title }}</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            :id=\"key + 'ColumnControl'\"\n            type=\"checkbox\"\n            :checked=\"configuration.hiddenColumns[key] !== true\"\n            @change=\"toggleColumn(key)\"\n          />\n          <span v-if=\"!isEditing && configuration.hiddenColumns[key] !== true\">Visible</span>\n        </div>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport LADTableConfiguration from '../LADTableConfiguration.js';\n\nexport default {\n  inject: ['openmct'],\n  data() {\n    const selection = this.openmct.selection.get();\n    const domainObject = selection[0][0].context.item;\n    const ladTableConfiguration = new LADTableConfiguration(domainObject, this.openmct);\n\n    return {\n      ladTableConfiguration,\n      isEditing: this.openmct.editor.isEditing(),\n      configuration: ladTableConfiguration.getConfiguration(),\n      items: [],\n      ladTableObjects: [],\n      ladTelemetryObjects: {}\n    };\n  },\n  computed: {\n    headers() {\n      const fullHeaders = {\n        timestamp: 'Timestamp',\n        type: 'Type'\n      };\n      // check hasUnits and limitColumnName and add then to fullHeaders\n      this.items.forEach((item) => {\n        if (item.hasUnits) {\n          fullHeaders.units = 'Units';\n        }\n        if (item.limitDefinition) {\n          const limits = Object.keys(item.limitDefinition);\n          limits.forEach((limit) => {\n            fullHeaders[limit] = limit;\n          });\n        }\n      });\n      return fullHeaders;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.toggleEdit);\n    this.composition = this.openmct.composition.get(this.ladTableConfiguration.domainObject);\n\n    if (this.ladTableConfiguration.domainObject.type === 'LadTable') {\n      this.composition.on('add', this.addItem);\n      this.composition.on('remove', this.removeItem);\n    } else {\n      this.compositions = [];\n      this.composition.on('add', this.addLadTable);\n      this.composition.on('remove', this.removeLadTable);\n    }\n\n    this.composition.load();\n  },\n  unmounted() {\n    this.ladTableConfiguration.destroy();\n    this.openmct.editor.off('isEditing', this.toggleEdit);\n\n    if (this.ladTableConfiguration.domainObject.type === 'LadTable') {\n      this.composition.off('add', this.addItem);\n      this.composition.off('remove', this.removeItem);\n    } else {\n      this.composition.off('add', this.addLadTable);\n      this.composition.off('remove', this.removeLadTable);\n      this.compositions.forEach((c) => {\n        c.composition.off('add', c.addCallback);\n        c.composition.off('remove', c.removeCallback);\n      });\n    }\n  },\n  methods: {\n    async addItem(domainObject) {\n      const item = {};\n      item.domainObject = domainObject;\n      item.key = this.openmct.objects.makeKeyString(domainObject.identifier);\n      item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();\n\n      const metadata = this.openmct.telemetry.getMetadata(domainObject);\n      const valueMetadatas = metadata ? metadata.valueMetadatas : [];\n      const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);\n\n      item.hasUnits = metadataWithUnits.length > 0;\n\n      this.items.push(item);\n    },\n    removeItem(identifier) {\n      const keystring = this.openmct.objects.makeKeyString(identifier);\n      const index = this.items.findIndex((item) => keystring === item.key);\n\n      this.items.splice(index, 1);\n    },\n    addLadTable(domainObject) {\n      let ladTable = {};\n      ladTable.domainObject = domainObject;\n      ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n      if (!this.ladTelemetryObjects) {\n        this.ladTelemetryObjects = {};\n      }\n      this.ladTelemetryObjects[ladTable.key] = [];\n      this.ladTableObjects.push(ladTable);\n\n      const composition = this.openmct.composition.get(ladTable.domainObject);\n      composition.on('add', this.addItem);\n      composition.on('remove', this.removeItem);\n      composition.load();\n\n      this.compositions.push({\n        composition,\n        addCallback: this.addItem,\n        removeCallback: this.removeItem\n      });\n    },\n    removeLadTable(identifier) {\n      const index = this.ladTableObjects.findIndex(\n        (ladTable) => this.openmct.objects.makeKeyString(identifier) === ladTable.key\n      );\n      const ladTable = this.ladTableObjects[index];\n\n      delete this.ladTelemetryObjects[ladTable.key];\n      this.ladTableObjects.splice(index, 1);\n    },\n    combineKeys(ladKey, telemetryObjectKey) {\n      return `${ladKey}-${telemetryObjectKey}`;\n    },\n    toggleColumn(key) {\n      const isHidden = this.configuration.hiddenColumns[key] === true;\n\n      this.configuration.hiddenColumns[key] = !isHidden;\n      this.ladTableConfiguration.updateConfiguration(this.configuration);\n    },\n    toggleEdit(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/LADTable/components/LadTableSet.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-lad-table-wrapper u-style-receiver js-style-receiver\" :class=\"staleClass\">\n    <table class=\"c-table c-lad-table\">\n      <thead>\n        <tr>\n          <th>Name</th>\n          <th v-if=\"showTimestamp\">Timestamp</th>\n          <th>Value</th>\n          <th v-if=\"showType\">Type</th>\n          <th v-if=\"hasUnits\">Units</th>\n        </tr>\n      </thead>\n      <tbody>\n        <template v-for=\"ladTable in ladTableObjects\" :key=\"ladTable.key\">\n          <tr class=\"c-table__group-header js-lad-table-set__table-headers\">\n            <td colspan=\"10\">\n              {{ ladTable.domainObject.name }}\n            </td>\n          </tr>\n          <LadRow\n            v-for=\"ladRow in ladTelemetryObjects[ladTable.key]\"\n            :key=\"combineKeys(ladTable.key, ladRow.key)\"\n            :domain-object=\"ladRow.domainObject\"\n            :path-to-table=\"ladTable.objectPath\"\n            :has-units=\"hasUnits\"\n            :is-stale=\"staleObjects.includes(ladRow.key)\"\n            :configuration=\"configuration\"\n            @row-context-click=\"updateViewContext\"\n          />\n        </template>\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script>\nimport { toRaw } from 'vue';\n\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport LadRow from './LadRow.vue';\n\nexport default {\n  components: {\n    LadRow\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'objectPath', 'currentView', 'ladTableConfiguration'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      ladTableObjects: [],\n      ladTelemetryObjects: {},\n      viewContext: {},\n      configuration: this.ladTableConfiguration.getConfiguration(),\n      subscribedObjects: {}\n    };\n  },\n  computed: {\n    hasUnits() {\n      const ladTables = Object.values(this.ladTelemetryObjects);\n      let showUnits = false;\n\n      for (let ladTable of ladTables) {\n        for (let telemetryObject of ladTable) {\n          let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject);\n\n          if (metadata) {\n            for (let metadatum of metadata.valueMetadatas) {\n              if (metadatum.unit) {\n                showUnits = true;\n              }\n            }\n          }\n        }\n      }\n\n      return showUnits && !this.configuration?.hiddenColumns?.units;\n    },\n    showTimestamp() {\n      return !this.configuration?.hiddenColumns?.timestamp;\n    },\n    showType() {\n      return !this.configuration?.hiddenColumns?.type;\n    },\n    staleClass() {\n      return this.isStale ? 'is-stale' : '';\n    }\n  },\n  created() {\n    this.compositions = [];\n  },\n  mounted() {\n    this.ladTableConfiguration.on('change', this.handleConfigurationChange);\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('add', this.addLadTable);\n    this.composition.on('remove', this.removeLadTable);\n    this.composition.on('reorder', this.reorderLadTables);\n    this.composition.load();\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  unmounted() {\n    this.ladTableConfiguration.off('change', this.handleConfigurationChange);\n    this.composition.off('add', this.addLadTable);\n    this.composition.off('remove', this.removeLadTable);\n    this.composition.off('reorder', this.reorderLadTables);\n    this.compositions.forEach((c) => {\n      c.composition.off('add', c.addCallback);\n      c.composition.off('remove', c.removeCallback);\n    });\n  },\n  methods: {\n    addLadTable(domainObject) {\n      let ladTable = {};\n      ladTable.domainObject = domainObject;\n      ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);\n      ladTable.objectPath = [domainObject, ...this.objectPath];\n\n      this.ladTelemetryObjects[ladTable.key] = [];\n      this.ladTableObjects.push(ladTable);\n\n      let composition = this.openmct.composition.get(ladTable.domainObject);\n      let addCallback = this.addTelemetryObject(ladTable);\n      let removeCallback = this.removeTelemetryObject(ladTable);\n\n      composition.on('add', addCallback);\n      composition.on('remove', removeCallback);\n      composition.load();\n\n      this.compositions.push({\n        composition,\n        addCallback,\n        removeCallback\n      });\n    },\n    combineKeys(ladKey, telemetryObjectKey) {\n      return `${ladKey}-${telemetryObjectKey}`;\n    },\n    removeLadTable(identifier) {\n      let index = this.ladTableObjects.findIndex(\n        (ladTable) => this.openmct.objects.makeKeyString(identifier) === ladTable.key\n      );\n      let ladTable = this.ladTableObjects[index];\n\n      ladTable?.domainObject?.composition.forEach((telemetryObject) => {\n        const telemetryKey = this.openmct.objects.makeKeyString(telemetryObject);\n        if (!this.subscribedObjects?.[telemetryKey]) {\n          return;\n        }\n        let subscribedObject = toRaw(this.subscribedObjects[telemetryKey]);\n        if (subscribedObject?.count > 1) {\n          subscribedObject.count -= 1;\n        } else if (subscribedObject?.count === 1) {\n          this.triggerUnsubscribeFromStaleness(subscribedObject.domainObject);\n          delete this.subscribedObjects[telemetryKey];\n        }\n      });\n\n      delete this.ladTelemetryObjects[ladTable.key];\n      this.ladTableObjects.splice(index, 1);\n    },\n    reorderLadTables(reorderPlan) {\n      let oldComposition = this.ladTableObjects.slice();\n      reorderPlan.forEach((reorderEvent) => {\n        this.ladTableObjects[reorderEvent.newIndex] = oldComposition[reorderEvent.oldIndex];\n      });\n    },\n    addTelemetryObject(ladTable) {\n      return (domainObject) => {\n        let telemetryObject = {};\n        telemetryObject.key = this.openmct.objects.makeKeyString(domainObject.identifier);\n        telemetryObject.domainObject = domainObject;\n\n        const telemetryObjects = this.ladTelemetryObjects[ladTable.key];\n        telemetryObjects.push(telemetryObject);\n\n        this.ladTelemetryObjects[ladTable.key] = telemetryObjects;\n\n        if (!this.subscribedObjects[telemetryObject?.key]) {\n          this.subscribeToStaleness(domainObject);\n          this.subscribedObjects[telemetryObject?.key] = { count: 1, domainObject };\n        } else if (this.subscribedObjects?.[telemetryObject?.key]?.count) {\n          this.subscribedObjects[telemetryObject?.key].count += 1;\n        }\n      };\n    },\n    removeTelemetryObject(ladTable) {\n      return (identifier) => {\n        const keystring = this.openmct.objects.makeKeyString(identifier);\n        const telemetryObjects = this.ladTelemetryObjects[ladTable.key];\n        let index = telemetryObjects.findIndex(\n          (telemetryObject) => keystring === telemetryObject.key\n        );\n\n        telemetryObjects.splice(index, 1);\n        this.ladTelemetryObjects[ladTable.key] = telemetryObjects;\n      };\n    },\n    handleConfigurationChange(configuration) {\n      this.configuration = configuration;\n    },\n    updateViewContext(rowContext) {\n      this.viewContext.row = rowContext;\n    },\n    getViewContext() {\n      return this.viewContext;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/LADTable/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ladTableCompositionPolicy from './LADTableCompositionPolicy.js';\nimport LADTableConfigurationViewProvider from './LADTableConfigurationViewProvider.js';\nimport LADTableSetViewProvider from './LADTableSetViewProvider.js';\nimport LADTableViewProvider from './LADTableViewProvider.js';\nimport LADTableViewActions from './ViewActions.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new LADTableViewProvider(openmct));\n    openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new LADTableConfigurationViewProvider(openmct));\n\n    openmct.types.addType('LadTable', {\n      name: 'LAD Table',\n      creatable: true,\n      description:\n        'Display the current value for one or more telemetry end points in a fixed table. Each row is a telemetry end point.',\n      cssClass: 'icon-tabular-lad',\n      initialize(domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          objectStyles: {}\n        };\n      }\n    });\n\n    openmct.types.addType('LadTableSet', {\n      name: 'LAD Table Set',\n      creatable: true,\n      description: 'Group LAD Tables together into a single view with sub-headers.',\n      cssClass: 'icon-tabular-lad-set',\n      initialize(domainObject) {\n        domainObject.composition = [];\n      }\n    });\n\n    openmct.composition.addPolicy(ladTableCompositionPolicy(openmct));\n\n    LADTableViewActions.forEach((action) => {\n      openmct.actions.register(action);\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/LADTable/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport {\n  createOpenMct,\n  getLatestTelemetry,\n  getMockObjects,\n  getMockTelemetry,\n  renderWhenVisible,\n  resetApplicationState,\n  spyOnBuiltins\n} from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport LadPlugin from './plugin.js';\n\nconst TABLE_BODY_ROWS = '.js-lad-table__body__row';\nconst TABLE_BODY_FIRST_ROW = TABLE_BODY_ROWS + ':first-child';\nconst TABLE_BODY_FIRST_ROW_FIRST_DATA = TABLE_BODY_FIRST_ROW + ' .js-first-data';\nconst TABLE_BODY_FIRST_ROW_SECOND_DATA = TABLE_BODY_FIRST_ROW + ' .js-second-data';\nconst TABLE_BODY_FIRST_ROW_THIRD_DATA = TABLE_BODY_FIRST_ROW + ' .js-third-data';\nconst LAD_SET_TABLE_HEADERS = '.js-lad-table-set__table-headers';\n\nfunction utcTimeFormat(value) {\n  return new Date(value).toISOString().replace('T', ' ');\n}\n\ndescribe('The LAD Table', () => {\n  const ladTableKey = 'LadTable';\n\n  let openmct;\n  let ladPlugin;\n  let historicalProvider;\n  let parent;\n  let child;\n  let telemetryCount = 3;\n  let timeFormat = 'utc';\n  let mockTelemetry = getMockTelemetry({\n    count: telemetryCount,\n    format: timeFormat\n  });\n  let mockObj = getMockObjects({\n    objectKeyStrings: ['ladTable', 'telemetry'],\n    format: timeFormat\n  });\n  let bounds = {\n    start: 0,\n    end: 4\n  };\n\n  // add telemetry object as composition in lad table\n  mockObj.ladTable.composition.push(mockObj.telemetry.identifier);\n\n  // this setups up the app\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    parent = document.createElement('div');\n    child = document.createElement('div');\n    parent.appendChild(child);\n\n    openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);\n    spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));\n\n    ladPlugin = new LadPlugin();\n    openmct.install(ladPlugin);\n\n    spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));\n\n    historicalProvider = {\n      request: () => {\n        return Promise.resolve([]);\n      }\n    };\n    spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);\n\n    openmct.time.bounds({\n      start: bounds.start,\n      end: bounds.end\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should provide a table view only for lad table objects', () => {\n    let applicableViews = openmct.objectViews.get(mockObj.ladTable, []);\n\n    let ladTableView = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);\n\n    expect(applicableViews.length).toEqual(1);\n    expect(ladTableView).toBeDefined();\n  });\n\n  describe('composition', () => {\n    let ladTableCompositionCollection;\n\n    beforeEach(() => {\n      ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);\n\n      return ladTableCompositionCollection.load();\n    });\n\n    it('should accept telemetry producing objects', () => {\n      expect(() => {\n        ladTableCompositionCollection.add(mockObj.telemetry);\n      }).not.toThrow();\n    });\n\n    it('should reject non-telemetry producing objects', () => {\n      expect(() => {\n        ladTableCompositionCollection.add(mockObj.ladTable);\n      }).toThrow();\n    });\n  });\n\n  describe('table view', () => {\n    let applicableViews;\n    let ladTableViewProvider;\n    let ladTableView;\n    let anotherTelemetryObj = getMockObjects({\n      objectKeyStrings: ['telemetry'],\n      overwrite: {\n        telemetry: {\n          name: 'New Telemetry Object',\n          identifier: {\n            namespace: '',\n            key: 'another-telemetry-object'\n          }\n        }\n      }\n    }).telemetry;\n\n    const aggregateTelemetryObj = getMockObjects({\n      objectKeyStrings: ['telemetry'],\n      overwrite: {\n        telemetry: {\n          name: 'Aggregate Telemetry Object',\n          identifier: {\n            namespace: '',\n            key: 'aggregate-telemetry-object'\n          }\n        }\n      }\n    }).telemetry;\n\n    // add another aggregate telemetry object as composition in lad table to test multi rows\n    aggregateTelemetryObj.composition = [anotherTelemetryObj.identifier];\n    mockObj.ladTable.composition.push(aggregateTelemetryObj.identifier);\n\n    beforeEach(async () => {\n      let telemetryRequestResolve;\n      let telemetryObjectResolve;\n      let anotherTelemetryObjectResolve;\n      let aggregateTelemetryObjectResolve;\n      const telemetryRequestPromise = new Promise((resolve) => {\n        telemetryRequestResolve = resolve;\n      });\n      const telemetryObjectPromise = new Promise((resolve) => {\n        telemetryObjectResolve = resolve;\n      });\n      const anotherTelemetryObjectPromise = new Promise((resolve) => {\n        anotherTelemetryObjectResolve = resolve;\n      });\n\n      const aggregateTelemetryObjectPromise = new Promise((resolve) => {\n        aggregateTelemetryObjectResolve = resolve;\n      });\n\n      spyOnBuiltins(['requestAnimationFrame']);\n      window.requestAnimationFrame.and.callFake((callBack) => {\n        callBack();\n      });\n\n      historicalProvider.request = () => {\n        telemetryRequestResolve(mockTelemetry);\n\n        return telemetryRequestPromise;\n      };\n\n      openmct.objects.get.and.callFake((obj) => {\n        if (obj.key === 'telemetry-object') {\n          telemetryObjectResolve(mockObj.telemetry);\n\n          return telemetryObjectPromise;\n        } else if (obj.key === 'another-telemetry-object') {\n          anotherTelemetryObjectResolve(anotherTelemetryObj);\n\n          return anotherTelemetryObjectPromise;\n        } else {\n          aggregateTelemetryObjectResolve(aggregateTelemetryObj);\n\n          return aggregateTelemetryObjectPromise;\n        }\n      });\n\n      openmct.time.bounds({\n        start: bounds.start,\n        end: bounds.end\n      });\n\n      applicableViews = openmct.objectViews.get(mockObj.ladTable, []);\n      ladTableViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === ladTableKey\n      );\n      ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);\n      ladTableView.show(child, true, { renderWhenVisible });\n\n      await Promise.all([\n        telemetryRequestPromise,\n        telemetryObjectPromise,\n        anotherTelemetryObjectPromise,\n        aggregateTelemetryObjectResolve\n      ]);\n      await nextTick();\n    });\n\n    it('should show one row per object in the composition', () => {\n      const rowCount = parent.querySelectorAll(TABLE_BODY_ROWS).length;\n      expect(rowCount).toBe(mockObj.ladTable.composition.length);\n    });\n\n    it('should show the most recent datum from the telemetry producing object', async () => {\n      const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat });\n      const expectedDate = utcTimeFormat(latestDatum[timeFormat]);\n      await nextTick();\n      const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;\n      expect(latestDate).toBe(expectedDate);\n      const dataType = parent\n        .querySelector(TABLE_BODY_ROWS)\n        .querySelector('.js-type-data').innerText;\n      expect(dataType).toBe('Telemetry');\n    });\n\n    it('should show aggregate telemetry type with blank data', async () => {\n      await nextTick();\n      const latestData = parent\n        .querySelectorAll(TABLE_BODY_ROWS)[1]\n        .querySelectorAll('td')[2].innerText;\n      expect(latestData).toBe('---');\n      const dataType = parent\n        .querySelectorAll(TABLE_BODY_ROWS)[1]\n        .querySelector('.js-type-data').innerText;\n      expect(dataType).toBe('Aggregate');\n    });\n\n    it('should show the name provided for the the telemetry producing object', () => {\n      const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText.trim();\n\n      const expectedName = mockObj.telemetry.name;\n      expect(rowName).toBe(expectedName);\n    });\n\n    it('should show the correct values for the datum based on domain and range hints', async () => {\n      const range = mockObj.telemetry.telemetry.values.find((val) => {\n        return val.hints && val.hints.range !== undefined;\n      }).key;\n      const domain = mockObj.telemetry.telemetry.values.find((val) => {\n        return val.hints && val.hints.domain !== undefined;\n      }).key;\n      const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat });\n      const rangeValue = mostRecentTelemetry[range];\n      const domainValue = utcTimeFormat(mostRecentTelemetry[domain]);\n      await nextTick();\n      const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;\n      const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText;\n      expect(actualRangeValue).toBe(rangeValue);\n      expect(actualDomainValue).toBe(domainValue);\n    });\n  });\n});\n\ndescribe('The LAD Table Set', () => {\n  const ladTableSetKey = 'LadTableSet';\n\n  let openmct;\n  let ladPlugin;\n  let parent;\n  let child;\n\n  let mockObj = getMockObjects({\n    objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']\n  });\n\n  let bounds = {\n    start: 0,\n    end: 4\n  };\n\n  // add mock telemetry to lad table and lad table to lad table set (composition)\n  mockObj.ladTable.composition.push(mockObj.telemetry.identifier);\n  mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    parent = document.createElement('div');\n    child = document.createElement('div');\n    parent.appendChild(child);\n\n    ladPlugin = new LadPlugin();\n    openmct.install(ladPlugin);\n\n    openmct.time.bounds({\n      start: bounds.start,\n      end: bounds.end\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n\n    return resetApplicationState(openmct);\n  });\n\n  it('should provide a lad table set view only for lad table set objects', () => {\n    spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));\n\n    let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);\n\n    let ladTableSetView = applicableViews.find(\n      (viewProvider) => viewProvider.key === ladTableSetKey\n    );\n\n    expect(applicableViews.length).toEqual(1);\n    expect(ladTableSetView).toBeDefined();\n  });\n\n  describe('composition', () => {\n    let ladTableSetCompositionCollection;\n\n    beforeEach(() => {\n      spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));\n\n      ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);\n\n      return ladTableSetCompositionCollection.load();\n    });\n\n    it('should accept lad table objects', () => {\n      expect(() => {\n        ladTableSetCompositionCollection.add(mockObj.ladTable);\n      }).not.toThrow();\n    });\n\n    it('should reject non lad table objects', () => {\n      expect(() => {\n        ladTableSetCompositionCollection.add(mockObj.telemetry);\n      }).toThrow();\n    });\n  });\n\n  describe('table view', () => {\n    let applicableViews;\n    let ladTableSetViewProvider;\n    let ladTableSetView;\n\n    let otherObj = getMockObjects({\n      objectKeyStrings: ['ladTable'],\n      overwrite: {\n        ladTable: {\n          name: 'New LAD Table Object',\n          identifier: {\n            namespace: '',\n            key: 'another-lad-object'\n          }\n        }\n      }\n    });\n\n    // add another lad table (with telemetry object) object to the lad table set for multi row test\n    otherObj.ladTable.composition.push(mockObj.telemetry.identifier);\n    mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);\n\n    beforeEach(() => {\n      spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));\n\n      spyOn(openmct.objects, 'get').and.callFake((obj) => {\n        if (obj.key === 'lad-object') {\n          return Promise.resolve(mockObj.ladTable);\n        } else if (obj.key === 'another-lad-object') {\n          return Promise.resolve(otherObj.ladTable);\n        } else if (obj.key === 'telemetry-object') {\n          return Promise.resolve(mockObj.telemetry);\n        }\n      });\n\n      openmct.time.bounds({\n        start: bounds.start,\n        end: bounds.end\n      });\n\n      applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);\n      ladTableSetViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === ladTableSetKey\n      );\n      ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);\n      ladTableSetView.show(child, false, { renderWhenVisible });\n\n      return nextTick();\n    });\n\n    it('should show one row per lad table object in the composition', () => {\n      const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);\n\n      return ladTableSetCompositionCollection.load().then(() => {\n        const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;\n\n        expect(rowCount).toBe(mockObj.ladTableSet.composition.length);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/URLIndicatorPlugin/README.md",
    "content": "# URL Indicator\nAdds an indicator which shows the availability of a URL, with success based on receipt of a 200 HTTP code. Can be used \nfor monitoring the availability of web services.\n\n## Installation\n```js\nopenmct.install(openmct.plugins.URLIndicator({\n  url: 'http://localhost:8080',\n    iconClass: 'check',\n    interval: 10000,\n    label: 'Localhost'\n })\n);\n```\n\n## Options\n* __url__: URL to indicate the status of\n* __iconClass__: Icon to show in the status bar, defaults to icon-database. See the [Style Guide](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home/glyphs?view=styleguide.glyphs) for more icon options.\n* __interval__: Interval between checking the connection, defaults to 10000\n* __label__: Name showing up as text in the status bar, defaults to url\n\n"
  },
  {
    "path": "src/plugins/URLIndicatorPlugin/URLIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// Set of connection states; changing among these states will be\n// reflected in the indicator's appearance.\n// CONNECTED: Everything nominal, expect to be able to read/write.\n// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.\n// PENDING: Still trying to connect, and haven't failed yet.\nconst CONNECTED = {\n  statusClass: 's-status-on'\n};\nconst PENDING = {\n  statusClass: 's-status-warning-lo'\n};\nconst DISCONNECTED = {\n  statusClass: 's-status-warning-hi'\n};\nexport default function URLIndicator(options, simpleIndicator) {\n  this.bindMethods();\n  this.count = 0;\n\n  this.indicator = simpleIndicator;\n  this.setDefaultsFromOptions(options);\n  this.setIndicatorToState(PENDING);\n\n  this.fetchUrl();\n  setInterval(this.fetchUrl, this.interval);\n}\n\nURLIndicator.prototype.setIndicatorToState = function (state) {\n  switch (state) {\n    case CONNECTED: {\n      this.indicator.text(this.label + ' is connected');\n      this.indicator.description(\n        this.label + ' is online, checking status every ' + this.interval + ' milliseconds.'\n      );\n      break;\n    }\n\n    case PENDING: {\n      this.indicator.text('Checking status of ' + this.label + ' please stand by...');\n      this.indicator.description('Checking status of ' + this.label + ' please stand by...');\n      break;\n    }\n\n    case DISCONNECTED: {\n      this.indicator.text(this.label + ' is offline');\n      this.indicator.description(\n        this.label + ' is offline, checking status every ' + this.interval + ' milliseconds'\n      );\n      break;\n    }\n  }\n\n  this.indicator.statusClass(state.statusClass);\n};\n\nURLIndicator.prototype.fetchUrl = function () {\n  fetch(this.URLpath)\n    .then((response) => {\n      if (response.ok) {\n        this.handleSuccess();\n      } else {\n        this.handleError();\n      }\n    })\n    .catch((error) => {\n      this.handleError();\n    });\n};\n\nURLIndicator.prototype.handleError = function (e) {\n  this.setIndicatorToState(DISCONNECTED);\n};\n\nURLIndicator.prototype.handleSuccess = function () {\n  this.setIndicatorToState(CONNECTED);\n};\n\nURLIndicator.prototype.setDefaultsFromOptions = function (options) {\n  this.URLpath = options.url;\n  this.label = options.label || options.url;\n  this.interval = options.interval || 10000;\n  this.indicator.iconClass(options.iconClass || 'icon-chain-links');\n};\n\nURLIndicator.prototype.bindMethods = function () {\n  this.fetchUrl = this.fetchUrl.bind(this);\n  this.handleSuccess = this.handleSuccess.bind(this);\n  this.handleError = this.handleError.bind(this);\n  this.setIndicatorToState = this.setIndicatorToState.bind(this);\n};\n"
  },
  {
    "path": "src/plugins/URLIndicatorPlugin/URLIndicatorPlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport URLIndicator from './URLIndicator.js';\n\nexport default function URLIndicatorPlugin(opts) {\n  return function install(openmct) {\n    const simpleIndicator = openmct.indicators.simpleIndicator();\n    const urlIndicator = new URLIndicator(opts, simpleIndicator);\n\n    openmct.indicators.add(simpleIndicator);\n\n    return urlIndicator;\n  };\n}\n"
  },
  {
    "path": "src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport * as testingUtils from 'utils/testing';\n\nimport URLIndicatorPlugin from './URLIndicatorPlugin.js';\n\ndescribe('The URLIndicator', function () {\n  let openmct;\n  let indicatorElement;\n  let pluginOptions;\n  let urlIndicator;\n  let fetchSpy;\n\n  beforeEach(function () {\n    jasmine.clock().install();\n    openmct = new testingUtils.createOpenMct();\n    spyOn(openmct.indicators, 'add');\n    fetchSpy = spyOn(window, 'fetch').and.callFake(() =>\n      Promise.resolve({\n        ok: true\n      })\n    );\n  });\n\n  afterEach(function () {\n    if (window.fetch.restore) {\n      window.fetch.restore();\n    }\n\n    jasmine.clock().uninstall();\n\n    return testingUtils.resetApplicationState(openmct);\n  });\n\n  describe('on initialization', function () {\n    describe('with default options', function () {\n      beforeEach(function () {\n        pluginOptions = {\n          url: 'someURL'\n        };\n        urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);\n        indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;\n      });\n\n      it('has a default icon class if none supplied', function () {\n        expect(indicatorElement.classList.contains('icon-chain-links')).toBe(true);\n      });\n\n      it('defaults to the URL if no label supplied', function () {\n        expect(indicatorElement.textContent.indexOf(pluginOptions.url) >= 0).toBe(true);\n      });\n    });\n\n    describe('with custom options', function () {\n      beforeEach(function () {\n        pluginOptions = {\n          url: 'customURL',\n          interval: 1814,\n          iconClass: 'iconClass-checked',\n          label: 'custom label'\n        };\n        urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);\n        indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;\n      });\n\n      it('uses the custom iconClass', function () {\n        expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);\n      });\n      it('uses custom interval', function () {\n        expect(window.fetch).toHaveBeenCalledTimes(1);\n        jasmine.clock().tick(1);\n        expect(window.fetch).toHaveBeenCalledTimes(1);\n        jasmine.clock().tick(pluginOptions.interval + 1);\n        expect(window.fetch).toHaveBeenCalledTimes(2);\n      });\n      it('uses custom label if supplied in initialization', function () {\n        expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);\n      });\n    });\n  });\n\n  describe('when running', function () {\n    beforeEach(function () {\n      pluginOptions = {\n        url: 'someURL',\n        interval: 100\n      };\n      urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);\n      indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;\n    });\n\n    it('requests the provided URL', function () {\n      jasmine.clock().tick(pluginOptions.interval + 1);\n      expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url);\n    });\n\n    it('indicates success if connection is nominal', async function () {\n      jasmine.clock().tick(pluginOptions.interval + 1);\n      await urlIndicator.fetchUrl();\n      expect(indicatorElement.classList.contains('s-status-on')).toBe(true);\n    });\n\n    it('indicates an error when the server cannot be reached', async function () {\n      fetchSpy.and.callFake(() =>\n        Promise.resolve({\n          ok: false\n        })\n      );\n      jasmine.clock().tick(pluginOptions.interval + 1);\n      await urlIndicator.fetchUrl();\n      expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  FIXED_MODE_KEY,\n  REALTIME_MODE_KEY,\n  TIME_CONTEXT_EVENTS\n} from '../../api/time/constants.js';\n\nconst SEARCH_MODE = 'tc.mode';\nconst SEARCH_TIME_SYSTEM = 'tc.timeSystem';\nconst SEARCH_START_BOUND = 'tc.startBound';\nconst SEARCH_END_BOUND = 'tc.endBound';\nconst SEARCH_START_DELTA = 'tc.startDelta';\nconst SEARCH_END_DELTA = 'tc.endDelta';\nconst TIME_EVENTS = [\n  TIME_CONTEXT_EVENTS.timeSystemChanged,\n  TIME_CONTEXT_EVENTS.modeChanged,\n  TIME_CONTEXT_EVENTS.clockChanged,\n  TIME_CONTEXT_EVENTS.clockOffsetsChanged\n];\n\nexport default class URLTimeSettingsSynchronizer {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.isUrlUpdateInProgress = false;\n\n    this.initialize = this.initialize.bind(this);\n    this.destroy = this.destroy.bind(this);\n    this.updateTimeSettings = this.updateTimeSettings.bind(this);\n    this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this);\n    this.updateBounds = this.updateBounds.bind(this);\n\n    openmct.on('start', this.initialize);\n    openmct.on('destroy', this.destroy);\n  }\n\n  initialize() {\n    this.updateTimeSettings();\n    this.openmct.router.on('change:params', this.updateTimeSettings);\n\n    TIME_EVENTS.forEach((event) => {\n      this.openmct.time.on(event, this.setUrlFromTimeApi);\n    });\n    this.openmct.time.on('boundsChanged', this.updateBounds);\n  }\n\n  destroy() {\n    this.openmct.router.off('change:params', this.updateTimeSettings);\n\n    this.openmct.off('start', this.initialize);\n    this.openmct.off('destroy', this.destroy);\n\n    TIME_EVENTS.forEach((event) => {\n      this.openmct.time.off(event, this.setUrlFromTimeApi);\n    });\n    this.openmct.time.off('boundsChanged', this.updateBounds);\n  }\n\n  updateTimeSettings() {\n    const timeParameters = this.parseParametersFromUrl();\n\n    if (this.areTimeParametersValid(timeParameters)) {\n      this.setTimeApiFromUrl(timeParameters);\n      this.openmct.router.setLocationFromUrl();\n    } else {\n      this.setUrlFromTimeApi();\n    }\n  }\n\n  parseParametersFromUrl() {\n    const searchParams = this.openmct.router.getAllSearchParams();\n    const mode = searchParams.get(SEARCH_MODE);\n    const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);\n    const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);\n    const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);\n    const bounds = {\n      start: startBound,\n      end: endBound\n    };\n    const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);\n    const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);\n    const clockOffsets = {\n      start: 0 - startOffset,\n      end: endOffset\n    };\n\n    return {\n      mode,\n      timeSystem,\n      bounds,\n      clockOffsets\n    };\n  }\n\n  setTimeApiFromUrl(timeParameters) {\n    const timeSystem = this.openmct.time.getTimeSystem();\n\n    if (timeParameters.mode === FIXED_MODE_KEY) {\n      // should update timesystem\n      if (timeSystem.key !== timeParameters.timeSystem) {\n        this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);\n      }\n      if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {\n        this.openmct.time.setMode(FIXED_MODE_KEY, timeParameters.bounds);\n      } else {\n        this.openmct.time.setMode(FIXED_MODE_KEY);\n      }\n    } else {\n      const clock = this.openmct.time.getClock();\n\n      if (clock?.key !== timeParameters.mode) {\n        this.openmct.time.setClock(timeParameters.mode);\n      }\n\n      if (\n        !this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)\n      ) {\n        this.openmct.time.setMode(REALTIME_MODE_KEY, timeParameters.clockOffsets);\n      } else {\n        this.openmct.time.setMode(REALTIME_MODE_KEY);\n      }\n\n      if (timeSystem?.key !== timeParameters.timeSystem) {\n        this.openmct.time.setTimeSystem(timeParameters.timeSystem);\n      }\n    }\n  }\n\n  updateBounds(bounds, isTick) {\n    if (!isTick) {\n      this.setUrlFromTimeApi();\n    }\n  }\n\n  setUrlFromTimeApi() {\n    const searchParams = this.openmct.router.getAllSearchParams();\n    const clock = this.openmct.time.getClock();\n    const mode = this.openmct.time.getMode();\n    const bounds = this.openmct.time.getBounds();\n    const clockOffsets = this.openmct.time.getClockOffsets();\n\n    if (mode === FIXED_MODE_KEY) {\n      searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);\n      searchParams.set(SEARCH_START_BOUND, bounds.start);\n      searchParams.set(SEARCH_END_BOUND, bounds.end);\n\n      searchParams.delete(SEARCH_START_DELTA);\n      searchParams.delete(SEARCH_END_DELTA);\n    } else {\n      searchParams.set(SEARCH_MODE, clock.key);\n\n      if (clockOffsets !== undefined) {\n        searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start);\n        searchParams.set(SEARCH_END_DELTA, clockOffsets.end);\n      } else {\n        searchParams.delete(SEARCH_START_DELTA);\n        searchParams.delete(SEARCH_END_DELTA);\n      }\n\n      searchParams.delete(SEARCH_START_BOUND);\n      searchParams.delete(SEARCH_END_BOUND);\n    }\n\n    searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);\n    this.openmct.router.updateParams(searchParams);\n  }\n\n  areTimeParametersValid(timeParameters) {\n    let isValid = false;\n\n    if (\n      this.isModeValid(timeParameters.mode) &&\n      this.isTimeSystemValid(timeParameters.timeSystem)\n    ) {\n      if (timeParameters.mode === FIXED_MODE_KEY) {\n        isValid = this.areStartAndEndValid(timeParameters.bounds);\n      } else {\n        isValid = this.areStartAndEndValid(timeParameters.clockOffsets);\n      }\n    }\n\n    return isValid;\n  }\n\n  areStartAndEndValid(bounds) {\n    return (\n      bounds !== undefined &&\n      bounds.start !== undefined &&\n      bounds.start !== null &&\n      bounds.end !== undefined &&\n      bounds.start !== null &&\n      !isNaN(bounds.start) &&\n      !isNaN(bounds.end)\n    );\n  }\n\n  isTimeSystemValid(timeSystem) {\n    let isValid = timeSystem !== undefined;\n\n    if (isValid) {\n      const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);\n      isValid = timeSystemObject !== undefined;\n    }\n\n    return isValid;\n  }\n\n  isModeValid(mode) {\n    let isValid = false;\n\n    if (mode !== undefined && mode !== null) {\n      isValid = true;\n    }\n\n    if (\n      isValid &&\n      (mode.toLowerCase() === FIXED_MODE_KEY || this.openmct.time.clocks.get(mode) !== undefined)\n    ) {\n      isValid = true;\n    }\n\n    return isValid;\n  }\n\n  areStartAndEndEqual(firstBounds, secondBounds) {\n    return firstBounds?.start === secondBounds.start && firstBounds?.end === secondBounds.end;\n  }\n}\n"
  },
  {
    "path": "src/plugins/URLTimeSettingsSynchronizer/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport URLTimeSettingsSynchronizer from './URLTimeSettingsSynchronizer.js';\n\nexport default function () {\n  return function install(openmct) {\n    return new URLTimeSettingsSynchronizer(openmct);\n  };\n}\n"
  },
  {
    "path": "src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nxdescribe('The URLTimeSettingsSynchronizer', () => {\n  let appHolder;\n  let openmct;\n  let resolveFunction;\n  let oldHash;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(openmct.plugins.MyItems());\n    openmct.install(openmct.plugins.LocalTimeSystem());\n    openmct.install(openmct.plugins.UTCTimeSystem());\n\n    openmct.on('start', done);\n\n    appHolder = document.createElement('div');\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    openmct.router.removeListener('change:hash', resolveFunction);\n\n    appHolder = undefined;\n    openmct = undefined;\n    resolveFunction = undefined;\n\n    return resetApplicationState(openmct);\n  });\n\n  it('initial clock is set to fixed is reflected in URL', (done) => {\n    resolveFunction = () => {\n      oldHash = window.location.hash;\n      expect(window.location.hash).toContain('tc.mode=fixed');\n\n      openmct.router.removeListener('change:hash', resolveFunction);\n      done();\n    };\n\n    // We have a debounce set to 300ms on setHash, so if we don't flush,\n    // the above resolve function sometimes doesn't fire due to a race condition.\n    openmct.router.setHash.flush();\n    openmct.router.on('change:hash', resolveFunction);\n  });\n\n  it('when the clock is set via the time API, it is reflected in the URL', (done) => {\n    resolveFunction = () => {\n      openmct.time.clock('local', {\n        start: -2000,\n        end: 200\n      });\n      openmct.router.setHash.flush();\n      const urlHash = window.location.hash;\n      expect(urlHash).toContain('tc.startDelta=2000');\n      expect(urlHash).toContain('tc.endDelta=200');\n      expect(urlHash).toContain('tc.mode=local');\n      openmct.router.removeListener('change:hash', resolveFunction);\n      done();\n    };\n\n    // We have a debounce set to 300ms on setHash, so if we don't flush,\n    // the above resolve function sometimes doesn't fire due to a race condition.\n    openmct.router.setHash.flush();\n    openmct.router.on('change:hash', resolveFunction);\n  });\n\n  it('when the clock mode is set to local, it is reflected in the URL', (done) => {\n    resolveFunction = () => {\n      let hash = window.location.hash;\n      hash = hash.replace('tc.mode=fixed', 'tc.mode=local');\n      window.location.hash = hash;\n\n      expect(window.location.hash).toContain('tc.mode=local');\n      done();\n    };\n\n    // We have a debounce set to 300ms on setHash, so if we don't flush,\n    // the above resolve function sometimes doesn't fire due to a race condition.\n    openmct.router.setHash.flush();\n    openmct.router.on('change:hash', resolveFunction);\n  });\n\n  it('when the clock mode is set to local, it is reflected in the URL', (done) => {\n    resolveFunction = () => {\n      let hash = window.location.hash;\n\n      hash = hash.replace('tc.mode=fixed', 'tc.mode=local');\n      window.location.hash = hash;\n      expect(window.location.hash).toContain('tc.mode=local');\n      done();\n    };\n\n    // We have a debounce set to 300ms on setHash, so if we don't flush,\n    // the above resolve function sometimes doesn't fire due to a race condition.\n    openmct.router.setHash.flush();\n    openmct.router.on('change:hash', resolveFunction);\n  });\n\n  // disabling due to test flakiness\n  xit('reset hash', (done) => {\n    window.location.hash = oldHash;\n    resolveFunction = () => {\n      expect(window.location.hash).toBe(oldHash);\n      done();\n    };\n\n    openmct.router.on('change:hash', resolveFunction);\n  });\n});\n"
  },
  {
    "path": "src/plugins/activityStates/activityStatesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { ACTIVITY_STATES_KEY } from './createActivityStatesIdentifier.js';\n\n/**\n * @typedef {Object} ActivityStatesInterceptorOptions\n * @property {import('openmct').Identifier} identifier the {namespace, key} to use for the activity states object.\n * @property {string} name The name of the activity states model.\n * @property {number} priority the priority of the interceptor. By default, it is low.\n */\n\n/**\n * Creates an activity states object in the persistence store. This is used to save plan activity states.\n * This will only get invoked when an attempt is made to save the state for an activity and no activity states object exists in the store.\n * @param {import('../../../openmct').OpenMCT} openmct\n * @param {ActivityStatesInterceptorOptions} options\n * @returns {Object}\n */\nconst ACTIVITY_STATES_TYPE = 'activity-states';\n\nfunction activityStatesInterceptor(openmct, options) {\n  const { identifier, name, priority = openmct.priority.LOW } = options;\n  const activityStatesModel = {\n    identifier,\n    name,\n    type: ACTIVITY_STATES_TYPE,\n    activities: {},\n    location: null\n  };\n\n  return {\n    appliesTo: (identifierObject) => {\n      return identifierObject.key === ACTIVITY_STATES_KEY;\n    },\n    invoke: (identifierObject, object) => {\n      if (!object || openmct.objects.isMissing(object)) {\n        openmct.objects.save(activityStatesModel);\n\n        return activityStatesModel;\n      }\n\n      return object;\n    },\n    priority\n  };\n}\n\nexport default activityStatesInterceptor;\n"
  },
  {
    "path": "src/plugins/activityStates/createActivityStatesIdentifier.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const ACTIVITY_STATES_KEY = 'activity-states';\n\nexport function createActivityStatesIdentifier(namespace = '') {\n  return {\n    key: ACTIVITY_STATES_KEY,\n    namespace\n  };\n}\n"
  },
  {
    "path": "src/plugins/activityStates/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport {\n  ACTIVITY_STATES_KEY,\n  createActivityStatesIdentifier\n} from './createActivityStatesIdentifier.js';\n\nconst MISSING_NAME = `Missing: ${ACTIVITY_STATES_KEY}`;\nconst DEFAULT_NAME = 'Activity States';\nconst activityStatesIdentifier = createActivityStatesIdentifier();\n\ndescribe('the plugin', () => {\n  let openmct;\n  let missingObj = {\n    identifier: activityStatesIdentifier,\n    type: 'unknown',\n    name: MISSING_NAME\n  };\n\n  describe('with no arguments passed in', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n      openmct.install(openmct.plugins.PlanLayout());\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('when installed, adds \"Activity States\"', async () => {\n      const activityStatesObject = await openmct.objects.get(activityStatesIdentifier);\n      expect(activityStatesObject.name).toBe(DEFAULT_NAME);\n      expect(activityStatesObject).toBeDefined();\n    });\n\n    describe('adds an interceptor that returns a \"Activity States\" model for', () => {\n      let activityStatesObject;\n      let mockNotFoundProvider;\n      let activeProvider;\n\n      beforeEach(async () => {\n        mockNotFoundProvider = {\n          get: () => Promise.reject(new Error('Not found')),\n          create: () => Promise.resolve(missingObj),\n          update: () => Promise.resolve(missingObj)\n        };\n\n        activeProvider = mockNotFoundProvider;\n        spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);\n        activityStatesObject = await openmct.objects.get(activityStatesIdentifier);\n      });\n\n      it('missing objects', () => {\n        let idsMatch = openmct.objects.areIdsEqual(\n          activityStatesObject.identifier,\n          activityStatesIdentifier\n        );\n\n        expect(activityStatesObject).toBeDefined();\n        expect(idsMatch).toBeTrue();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularConstants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Constant values used by the Autoflow Tabular View.\n */\nexport default {\n  ROW_HEIGHT: 16,\n  SLIDER_HEIGHT: 10,\n  INITIAL_COLUMN_WIDTH: 225,\n  MAX_COLUMN_WIDTH: 525,\n  COLUMN_WIDTH_STEP: 25\n};\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularController.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport AutoflowTabularRowController from './AutoflowTabularRowController.js';\n\n/**\n * Controller for an Autoflow Tabular View. Subscribes to telemetry\n * associated with children of the domain object and passes that\n * information on to the view.\n *\n * @param {DomainObject} domainObject the object being viewed\n * @param {*} data the view data\n * @param openmct a reference to the openmct application\n */\nexport default function AutoflowTabularController(domainObject, data, openmct) {\n  this.composition = openmct.composition.get(domainObject);\n  this.data = data;\n  this.openmct = openmct;\n\n  this.rows = {};\n  this.controllers = {};\n\n  this.addRow = this.addRow.bind(this);\n  this.removeRow = this.removeRow.bind(this);\n}\n\n/**\n * Set the \"Last Updated\" value to be displayed.\n * @param {string} value the value to display\n * @private\n */\nAutoflowTabularController.prototype.trackLastUpdated = function (value) {\n  this.data.updated = value;\n};\n\n/**\n * Respond to an `add` event from composition by adding a new row.\n * @private\n */\nAutoflowTabularController.prototype.addRow = function (childObject) {\n  const identifier = childObject.identifier;\n  const id = [identifier.namespace, identifier.key].join(':');\n\n  if (!this.rows[id]) {\n    this.rows[id] = {\n      classes: '',\n      name: childObject.name,\n      value: undefined\n    };\n    this.controllers[id] = new AutoflowTabularRowController(\n      childObject,\n      this.rows[id],\n      this.openmct,\n      this.trackLastUpdated.bind(this)\n    );\n    this.controllers[id].activate();\n    this.data.items.push(this.rows[id]);\n  }\n};\n\n/**\n * Respond to an `remove` event from composition by removing any\n * related row.\n * @private\n */\nAutoflowTabularController.prototype.removeRow = function (identifier) {\n  const id = [identifier.namespace, identifier.key].join(':');\n\n  if (this.rows[id]) {\n    this.data.items = this.data.items.filter(\n      function (item) {\n        return item !== this.rows[id];\n      }.bind(this)\n    );\n    this.controllers[id].destroy();\n    delete this.controllers[id];\n    delete this.rows[id];\n  }\n};\n\n/**\n * Activate this controller; begin listening for changes.\n */\nAutoflowTabularController.prototype.activate = function () {\n  this.composition.on('add', this.addRow);\n  this.composition.on('remove', this.removeRow);\n  this.composition.load();\n};\n\n/**\n * Destroy this controller; detach any associated resources.\n */\nAutoflowTabularController.prototype.destroy = function () {\n  Object.keys(this.controllers).forEach(\n    function (id) {\n      this.controllers[id].destroy();\n    }.bind(this)\n  );\n  this.controllers = {};\n  this.composition.off('add', this.addRow);\n  this.composition.off('remove', this.removeRow);\n};\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularPlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport AutoflowTabularView from './AutoflowTabularView.js';\n\nexport default function (options) {\n  return function (openmct) {\n    const views = openmct.mainViews || openmct.objectViews;\n\n    views.addProvider({\n      name: 'Autoflow Tabular',\n      key: 'autoflow',\n      cssClass: 'icon-packet',\n      description: 'A tabular view of packet contents.',\n      canView: function (d) {\n        return !options || options.type === d.type;\n      },\n      view: function (domainObject) {\n        return new AutoflowTabularView(domainObject, openmct);\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularPluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport AutoflowTabularConstants from './AutoflowTabularConstants.js';\nimport AutoflowTabularPlugin from './AutoflowTabularPlugin.js';\nimport DOMObserver from './dom-observer.js';\n\n// TODO lots of its without expects\nxdescribe('AutoflowTabularPlugin', () => {\n  let testType;\n  let testObject;\n  let mockmct;\n\n  beforeEach(() => {\n    testType = 'some-type';\n    testObject = { type: testType };\n    mockmct = createOpenMct();\n    spyOn(mockmct.composition, 'get');\n    spyOn(mockmct.objectViews, 'addProvider');\n    spyOn(mockmct.telemetry, 'getMetadata');\n    spyOn(mockmct.telemetry, 'getValueFormatter');\n    spyOn(mockmct.telemetry, 'limitEvaluator');\n    spyOn(mockmct.telemetry, 'request');\n    spyOn(mockmct.telemetry, 'subscribe');\n\n    const plugin = new AutoflowTabularPlugin({ type: testType });\n    plugin(mockmct);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(mockmct);\n  });\n\n  it('installs a view provider', () => {\n    expect(mockmct.objectViews.addProvider).toHaveBeenCalled();\n  });\n\n  describe('installs a view provider which', () => {\n    let provider;\n\n    beforeEach(() => {\n      provider = mockmct.objectViews.addProvider.calls.mostRecent().args[0];\n    });\n\n    it('applies its view to the type from options', () => {\n      expect(provider.canView(testObject, [])).toBe(true);\n    });\n\n    it('does not apply to other types', () => {\n      expect(provider.canView({ type: 'foo' }, [])).toBe(false);\n    });\n\n    describe('provides a view which', () => {\n      let testKeys;\n      let testChildren;\n      let testContainer;\n      let testHistories;\n      let mockComposition;\n      let mockMetadata;\n      let mockEvaluator;\n      let mockUnsubscribes;\n      let callbacks;\n      let view;\n      let domObserver;\n\n      function waitsForChange() {\n        return new Promise(function (resolve) {\n          window.requestAnimationFrame(resolve);\n        });\n      }\n\n      function emitEvent(mockEmitter, type, event) {\n        mockEmitter.on.calls.all().forEach((call) => {\n          if (call.args[0] === type) {\n            call.args[1](event);\n          }\n        });\n      }\n\n      beforeEach(() => {\n        callbacks = {};\n\n        spyOnBuiltins(['requestAnimationFrame']);\n        window.requestAnimationFrame.and.callFake((callBack) => {\n          callBack();\n        });\n\n        testObject = { type: 'some-type' };\n        testKeys = ['abc', 'def', 'xyz'];\n        testChildren = testKeys.map((key) => {\n          return {\n            identifier: {\n              namespace: 'test',\n              key: key\n            },\n            name: 'Object ' + key\n          };\n        });\n        testContainer = document.createElement('div');\n        domObserver = new DOMObserver(testContainer);\n\n        testHistories = testKeys.reduce((histories, key, index) => {\n          histories[key] = {\n            key: key,\n            range: index + 10,\n            domain: key + index\n          };\n\n          return histories;\n        }, {});\n\n        mockComposition = jasmine.createSpyObj('composition', ['load', 'on', 'off']);\n        mockMetadata = jasmine.createSpyObj('metadata', ['valuesForHints']);\n\n        mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);\n        mockUnsubscribes = testKeys.reduce((map, key) => {\n          map[key] = jasmine.createSpy('unsubscribe-' + key);\n\n          return map;\n        }, {});\n\n        mockmct.composition.get.and.returnValue(mockComposition);\n        mockComposition.load.and.callFake(() => {\n          testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));\n\n          return Promise.resolve(testChildren);\n        });\n\n        mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);\n        mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {\n          const mockFormatter = jasmine.createSpyObj('formatter', ['format']);\n          mockFormatter.format.and.callFake((datum) => {\n            return datum[metadatum.hint];\n          });\n\n          return mockFormatter;\n        });\n        mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);\n        mockmct.telemetry.subscribe.and.callFake((obj, callback) => {\n          const key = obj.identifier.key;\n          callbacks[key] = callback;\n\n          return mockUnsubscribes[key];\n        });\n        mockmct.telemetry.request.and.callFake((obj, request) => {\n          const key = obj.identifier.key;\n\n          return Promise.resolve([testHistories[key]]);\n        });\n        mockMetadata.valuesForHints.and.callFake((hints) => {\n          return [{ hint: hints[0] }];\n        });\n\n        view = provider.view(testObject, [testObject]);\n        view.show(testContainer);\n\n        return nextTick();\n      });\n\n      afterEach(() => {\n        domObserver.destroy();\n      });\n\n      it('populates its container', () => {\n        expect(testContainer.children.length > 0).toBe(true);\n      });\n\n      describe('when rows have been populated', () => {\n        function rowsMatch() {\n          const rows = testContainer.querySelectorAll('.l-autoflow-row').length;\n\n          return rows === testChildren.length;\n        }\n\n        it('shows one row per child object', () => {\n          return domObserver.when(rowsMatch);\n        });\n\n        // it(\"adds rows on composition change\", () => {\n        //     const child = {\n        //         identifier: {\n        //             namespace: \"test\",\n        //             key: \"123\"\n        //         },\n        //         name: \"Object 123\"\n        //     };\n        //     testChildren.push(child);\n        //     emitEvent(mockComposition, 'add', child);\n\n        //     return domObserver.when(rowsMatch);\n        // });\n\n        it('removes rows on composition change', () => {\n          const child = testChildren.pop();\n          emitEvent(mockComposition, 'remove', child.identifier);\n\n          return domObserver.when(rowsMatch);\n        });\n      });\n\n      it('removes subscriptions when destroyed', () => {\n        testKeys.forEach((key) => {\n          expect(mockUnsubscribes[key]).not.toHaveBeenCalled();\n        });\n        view.destroy();\n        testKeys.forEach((key) => {\n          expect(mockUnsubscribes[key]).toHaveBeenCalled();\n        });\n      });\n\n      it('provides a button to change column width', () => {\n        const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;\n        const nextWidth = initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;\n\n        expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual(\n          initialWidth + 'px'\n        );\n\n        testContainer.querySelector('.change-column-width').click();\n\n        function widthHasChanged() {\n          const width = testContainer.querySelector('.l-autoflow-col').css('width');\n\n          return width !== initialWidth + 'px';\n        }\n\n        return domObserver.when(widthHasChanged).then(() => {\n          expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual(\n            nextWidth + 'px'\n          );\n        });\n      });\n\n      it('subscribes to all child objects', () => {\n        testKeys.forEach((key) => {\n          expect(callbacks[key]).toEqual(jasmine.any(Function));\n        });\n      });\n\n      it('displays historical telemetry', () => {\n        function rowTextDefined() {\n          return testContainer.querySelector('.l-autoflow-item').filter('.r').text() !== '';\n        }\n\n        return domObserver.when(rowTextDefined).then(() => {\n          testKeys.forEach((key, index) => {\n            const datum = testHistories[key];\n            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');\n            expect($cell.text()).toEqual(String(datum.range));\n          });\n        });\n      });\n\n      it('displays incoming telemetry', () => {\n        const testData = testKeys.map((key, index) => {\n          return {\n            key: key,\n            range: index * 100,\n            domain: key + index\n          };\n        });\n\n        testData.forEach((datum) => {\n          callbacks[datum.key](datum);\n        });\n\n        return waitsForChange().then(() => {\n          testData.forEach((datum, index) => {\n            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');\n            expect($cell.text()).toEqual(String(datum.range));\n          });\n        });\n      });\n\n      it('updates classes for limit violations', () => {\n        const testClass = 'some-limit-violation';\n        mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });\n        testKeys.forEach((key) => {\n          callbacks[key]({\n            range: 'foo',\n            domain: 'bar'\n          });\n        });\n\n        return waitsForChange().then(() => {\n          testKeys.forEach((datum, index) => {\n            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');\n            expect($cell.hasClass(testClass)).toBe(true);\n          });\n        });\n      });\n\n      it('automatically flows to new columns', () => {\n        const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;\n        const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;\n        const count = testKeys.length;\n        const $container = testContainer;\n        let promiseChain = Promise.resolve();\n\n        function columnsHaveAutoflowed() {\n          const itemsHeight = $container.querySelector('.l-autoflow-items').height();\n          const availableHeight = itemsHeight - sliderHeight;\n          const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);\n          const columns = Math.ceil(count / availableRows);\n\n          return $container.querySelector('.l-autoflow-col').length === columns;\n        }\n\n        $container.find('.abs').css({\n          position: 'absolute',\n          left: '0px',\n          right: '0px',\n          top: '0px',\n          bottom: '0px'\n        });\n        $container.css({ position: 'absolute' });\n\n        $container.appendTo(document.body);\n\n        function setHeight(height) {\n          $container.css('height', height + 'px');\n\n          return domObserver.when(columnsHaveAutoflowed);\n        }\n\n        for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {\n          // eslint-disable-next-line no-invalid-this\n          promiseChain = promiseChain.then(setHeight.bind(this, height));\n        }\n\n        return promiseChain.then(() => {\n          $container.remove();\n        });\n      });\n\n      it('loads composition exactly once', () => {\n        const testObj = testChildren.pop();\n        emitEvent(mockComposition, 'remove', testObj.identifier);\n        testChildren.push(testObj);\n        emitEvent(mockComposition, 'add', testObj);\n        expect(mockComposition.load.calls.count()).toEqual(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularRowController.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Controller for individual rows of an Autoflow Tabular View.\n * Subscribes to telemetry and updates row data.\n *\n * @param {DomainObject} domainObject the object being viewed\n * @param {*} data the view data\n * @param openmct a reference to the openmct application\n * @param {Function} callback a callback to invoke with \"last updated\" timestamps\n */\nexport default function AutoflowTabularRowController(domainObject, data, openmct, callback) {\n  this.domainObject = domainObject;\n  this.data = data;\n  this.openmct = openmct;\n  this.callback = callback;\n\n  this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n  this.ranges = this.metadata.valuesForHints(['range']);\n  this.domains = this.metadata.valuesForHints(['domain']);\n  this.rangeFormatter = this.openmct.telemetry.getValueFormatter(this.ranges[0]);\n  this.domainFormatter = this.openmct.telemetry.getValueFormatter(this.domains[0]);\n  this.evaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);\n\n  this.initialized = false;\n}\n\n/**\n * Update row to reflect incoming telemetry data.\n * @private\n */\nAutoflowTabularRowController.prototype.updateRowData = function (datum) {\n  const violations = this.evaluator.evaluate(datum, this.ranges[0]);\n\n  this.initialized = true;\n  this.data.classes = violations ? violations.cssClass : '';\n  this.data.value = this.rangeFormatter.format(datum);\n  this.callback(this.domainFormatter.format(datum));\n};\n\n/**\n * Activate this controller; begin listening for changes.\n */\nAutoflowTabularRowController.prototype.activate = function () {\n  this.unsubscribe = this.openmct.telemetry.subscribe(\n    this.domainObject,\n    this.updateRowData.bind(this)\n  );\n\n  const options = {\n    size: 1,\n    strategy: 'latest',\n    timeContext: this.openmct.time.getContextForView([])\n  };\n  this.openmct.telemetry.request(this.domainObject, options).then(\n    function (history) {\n      if (!this.initialized && history.length > 0) {\n        this.updateRowData(history[history.length - 1]);\n      }\n    }.bind(this)\n  );\n};\n\n/**\n * Destroy this controller; detach any associated resources.\n */\nAutoflowTabularRowController.prototype.destroy = function () {\n  if (this.unsubscribe) {\n    this.unsubscribe();\n  }\n};\n"
  },
  {
    "path": "src/plugins/autoflow/AutoflowTabularView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport autoflowTemplate from './autoflow-tabular.html';\nimport AutoflowTabularConstants from './AutoflowTabularConstants.js';\nimport AutoflowTabularController from './AutoflowTabularController.js';\nimport VueView from './VueView.js';\n\nconst ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT;\nconst SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT;\nconst INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;\nconst MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH;\nconst COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP;\n\n/**\n * Implements the Autoflow Tabular view of a domain object.\n */\nexport default function AutoflowTabularView(domainObject, openmct) {\n  const data = {\n    items: [],\n    columns: [],\n    width: INITIAL_COLUMN_WIDTH,\n    filter: '',\n    updated: 'No updates',\n    rowCount: 1\n  };\n  const controller = new AutoflowTabularController(domainObject, data, openmct);\n  let interval;\n\n  VueView.call(this, {\n    data: data,\n    methods: {\n      increaseColumnWidth: function () {\n        data.width += COLUMN_WIDTH_STEP;\n        data.width = data.width > MAX_COLUMN_WIDTH ? INITIAL_COLUMN_WIDTH : data.width;\n      },\n      reflow: function () {\n        let column = [];\n        let index = 0;\n        const filteredItems = data.items.filter(function (item) {\n          return item.name.toLowerCase().indexOf(data.filter.toLowerCase()) !== -1;\n        });\n\n        data.columns = [];\n\n        while (index < filteredItems.length) {\n          if (column.length >= data.rowCount) {\n            data.columns.push(column);\n            column = [];\n          }\n\n          column.push(filteredItems[index]);\n          index += 1;\n        }\n\n        if (column.length > 0) {\n          data.columns.push(column);\n        }\n      }\n    },\n    watch: {\n      filter: 'reflow',\n      items: 'reflow',\n      rowCount: 'reflow'\n    },\n    template: autoflowTemplate,\n    unmounted: function () {\n      controller.destroy();\n\n      if (interval) {\n        clearInterval(interval);\n        interval = undefined;\n      }\n    },\n    mounted: function () {\n      controller.activate();\n\n      const updateRowHeight = function () {\n        const tabularArea = this.$refs.autoflowItems;\n        const height = tabularArea ? tabularArea.clientHeight : 0;\n        const available = height - SLIDER_HEIGHT;\n        const rows = Math.max(1, Math.floor(available / ROW_HEIGHT));\n        data.rowCount = rows;\n      }.bind(this);\n\n      interval = setInterval(updateRowHeight, 50);\n      this.$nextTick(updateRowHeight);\n    }\n  });\n}\n\nAutoflowTabularView.prototype = Object.create(VueView.prototype);\n"
  },
  {
    "path": "src/plugins/autoflow/README.md",
    "content": "# Autoflow View\n\nThis plugin provides the Autoflow View for domain objects in Open MCT. This view allows users to visualize the latest \nvalues of a collection of telemetry points in a condensed list.\n\n## Installation\n``` js\n    openmct.install(openmct.plugins.AutoflowView({\n        type: \"telemetry.fixed\"\n    }));\n```\n\n## Options\n* `type`: The object type to add the Autoflow View to. Currently supports a single value. If not provided, will make the \nAutoflow view available for all objects (which is probably not what you want).\n"
  },
  {
    "path": "src/plugins/autoflow/VueView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nexport default function () {\n  class VueView {\n    constructor(options) {\n      const { vNode, destroy } = mount(options);\n      this.show = function (container) {\n        container.appendChild(vNode.el);\n      };\n      this.destroy = destroy;\n    }\n  }\n\n  return VueView;\n}\n"
  },
  {
    "path": "src/plugins/autoflow/autoflow-tabular.html",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<div class=\"items-holder abs contents autoflow obj-value-format\">\n  <div class=\"abs l-flex-row holder t-autoflow-header l-autoflow-header\">\n    <span class=\"t-filter l-filter\">\n      <input type=\"search\" class=\"t-filter-input\" v-model=\"filter\" />\n      <a v-if=\"filter !== ''\" v-on:click=\"filter = ''\" class=\"clear-icon icon-x-in-circle\"></a>\n    </span>\n\n    <div class=\"flex-elem grows t-last-update\" title=\"Last Update\">{{updated}}</div>\n    <a\n      title=\"Change column width\"\n      v-on:click=\"increaseColumnWidth()\"\n      class=\"s-button flex-elem icon-arrows-right-left change-column-width\"\n    ></a>\n  </div>\n  <div class=\"abs t-autoflow-items l-autoflow-items\" ref=\"autoflowItems\">\n    <ul v-for=\"column in columns\" class=\"l-autoflow-col\" :style=\"{ width: width + 'px' }\">\n      <li v-for=\"row in column\" class=\"l-autoflow-row\">\n        <span\n          :aria-label=\"row.value\"\n          :title=\"row.value\"\n          :data-value=\"row.value\"\n          :class=\"'l-autoflow-item r l-obj-val-format ' + row.classes\"\n          >{{row.value}}</span\n        >\n        <span :aria-label=\"row.name\" :title=\"row.name\" class=\"l-autoflow-item l\">{{row.name}}</span>\n      </li>\n    </ul>\n  </div>\n</div>\n"
  },
  {
    "path": "src/plugins/autoflow/dom-observer.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function DOMObserver(element) {\n  this.element = element;\n  this.observers = [];\n}\n\nDOMObserver.prototype.when = function (latchFunction) {\n  return new Promise(\n    function (resolve, reject) {\n      //Test latch function at least once\n      if (latchFunction()) {\n        resolve();\n      } else {\n        //Latch condition not true yet, create observer on DOM and test again on change.\n        const config = {\n          attributes: true,\n          childList: true,\n          subtree: true\n        };\n        const observer = new MutationObserver(function () {\n          if (latchFunction()) {\n            resolve();\n          }\n        });\n        observer.observe(this.element, config);\n        this.observers.push(observer);\n      }\n    }.bind(this)\n  );\n};\n\nDOMObserver.prototype.destroy = function () {\n  this.observers.forEach(\n    function (observer) {\n      observer.disconnect();\n    }.bind(this)\n  );\n};\n"
  },
  {
    "path": "src/plugins/charts/bar/BarGraphCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { BAR_GRAPH_KEY } from './BarGraphConstants.js';\n\nexport default function BarGraphCompositionPolicy(openmct) {\n  function hasRange(metadata) {\n    const rangeValues = metadata.valuesForHints(['range']);\n\n    return rangeValues && rangeValues.length > 0;\n  }\n\n  function hasBarGraphTelemetry(domainObject) {\n    if (!openmct.telemetry.isTelemetryObject(domainObject)) {\n      return false;\n    }\n\n    let metadata = openmct.telemetry.getMetadata(domainObject);\n\n    return metadata.values().length > 0 && hasRange(metadata);\n  }\n\n  return {\n    allow: function (parent, child) {\n      if (parent.type === BAR_GRAPH_KEY) {\n        if (child.type === 'conditionSet' || !hasBarGraphTelemetry(child)) {\n          return false;\n        }\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/bar/BarGraphConstants.js",
    "content": "export const BAR_GRAPH_VIEW = 'bar-graph.view';\nexport const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';\nexport const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';\n"
  },
  {
    "path": "src/plugins/charts/bar/BarGraphPlot.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"plotWrapper\" class=\"has-local-controls\" :class=\"{ 's-unsynced': isZoomed }\">\n    <div v-if=\"isZoomed\" class=\"l-state-indicators\">\n      <span\n        class=\"l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle\"\n        title=\"This plot is not currently displaying the latest data. Reset pan/zoom to view latest data.\"\n      ></span>\n    </div>\n    <div ref=\"plot\" class=\"c-bar-chart\" @plotly_relayout=\"zoom\"></div>\n    <div\n      v-if=\"false\"\n      ref=\"localControl\"\n      class=\"gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover\"\n    >\n      <button\n        v-if=\"data.length\"\n        class=\"c-button icon-reset\"\n        :disabled=\"!isZoomed\"\n        title=\"Reset pan/zoom\"\n        @click=\"reset()\"\n      ></button>\n    </div>\n  </div>\n</template>\n<script>\nimport Plotly from 'plotly-basic';\n\nconst MULTI_AXES_X_PADDING_PERCENT = {\n  LEFT: 8,\n  RIGHT: 94\n};\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    data: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    plotAxisTitle: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['subscribe', 'unsubscribe'],\n  data() {\n    return {\n      isZoomed: false,\n      primaryYAxisRange: {\n        min: '',\n        max: ''\n      },\n      xAxisRange: {\n        min: '',\n        max: ''\n      }\n    };\n  },\n  watch: {\n    data: {\n      immediate: false,\n      handler: 'updateData'\n    }\n  },\n  created() {\n    this.registerListeners();\n  },\n  mounted() {\n    this.plotResizeObserver.observe(this.$refs.plotWrapper);\n    Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {\n      responsive: true,\n      displayModeBar: false\n    });\n  },\n  beforeUnmount() {\n    if (this.plotResizeObserver) {\n      this.plotResizeObserver.unobserve(this.$refs.plotWrapper);\n      this.plotResizeObserver.disconnect();\n      clearTimeout(this.resizeTimer);\n    }\n\n    if (this.removeBarColorListener) {\n      this.removeBarColorListener();\n    }\n\n    Plotly.purge(this.$refs.plot);\n  },\n  methods: {\n    getAxisMinMax(axis) {\n      const min = axis.autoSize ? '' : axis.min;\n      const max = axis.autoSize ? '' : axis.max;\n\n      return {\n        min,\n        max\n      };\n    },\n    getLayout() {\n      const yAxesMeta = this.getYAxisMeta();\n      const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);\n      const xAxisDomain = this.getXAxisDomain(yAxesMeta);\n\n      return {\n        autosize: true,\n        showlegend: false,\n        textposition: 'auto',\n        font: {\n          family: 'Helvetica Neue, Helvetica, Arial, sans-serif',\n          size: '12px',\n          color: '#666'\n        },\n        xaxis: {\n          domain: xAxisDomain,\n          range: [this.xAxisRange.min, this.xAxisRange.max],\n          title: this.plotAxisTitle.xAxisTitle,\n          automargin: true,\n          fixedrange: true\n        },\n        yaxis: primaryYaxis,\n        margin: {\n          l: 5,\n          r: 5,\n          t: 5,\n          b: 0\n        },\n        paper_bgcolor: 'transparent',\n        plot_bgcolor: 'transparent'\n      };\n    },\n    getYAxisMeta() {\n      const yAxisMeta = {};\n\n      this.data.forEach((datum) => {\n        const yAxisMetadata = datum.yAxisMetadata;\n        const range = '1';\n        const side = 'left';\n        const name = '';\n        const unit = yAxisMetadata.units;\n\n        yAxisMeta[range] = {\n          range,\n          side,\n          name,\n          unit\n        };\n      });\n\n      return yAxisMeta;\n    },\n    getXAxisDomain(yAxisMeta) {\n      let leftPaddingPerc = 0;\n      let rightPaddingPerc = 100;\n      let rightSide =\n        yAxisMeta && Object.values(yAxisMeta).filter((axisMeta) => axisMeta.side === 'right');\n      let leftSide =\n        yAxisMeta && Object.values(yAxisMeta).filter((axisMeta) => axisMeta.side === 'left');\n      if (yAxisMeta && rightSide.length > 1) {\n        rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;\n      }\n\n      if (yAxisMeta && leftSide.length > 1) {\n        leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;\n      }\n\n      return [leftPaddingPerc / 100, rightPaddingPerc / 100];\n    },\n    getYaxisLayout(yAxisMeta) {\n      if (!yAxisMeta) {\n        return {};\n      }\n\n      const { name, range, side = 'left', unit } = yAxisMeta;\n      const title = `${name} ${unit ? '(' + unit + ')' : ''}`;\n      const yaxis = {\n        automargin: true,\n        fixedrange: true,\n        title\n      };\n\n      let yAxistype = this.primaryYAxisRange;\n      if (range === '1') {\n        yaxis.range = [yAxistype.min, yAxistype.max];\n\n        return yaxis;\n      }\n\n      yaxis.range = [yAxistype.min, yAxistype.max];\n      yaxis.anchor = side.toLowerCase() === 'left' ? 'free' : 'x';\n      yaxis.showline = side.toLowerCase() === 'left';\n      yaxis.side = side.toLowerCase();\n      yaxis.overlaying = 'y';\n      yaxis.position = 0.01;\n\n      return yaxis;\n    },\n    registerListeners() {\n      this.removeBarColorListener = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.barStyles',\n        this.barColorChanged\n      );\n      this.resizeTimer = false;\n      if (window.ResizeObserver) {\n        this.plotResizeObserver = new ResizeObserver(() => {\n          // debounce and trigger window resize so that plotly can resize the plot\n          clearTimeout(this.resizeTimer);\n          this.resizeTimer = setTimeout(() => {\n            window.dispatchEvent(new Event('resize'));\n          }, 250);\n        });\n      }\n    },\n    reset() {\n      this.updatePlot();\n\n      this.isZoomed = false;\n      this.$emit('subscribe');\n    },\n    barColorChanged() {\n      const colors = [];\n      const indices = [];\n      this.data.forEach((item, index) => {\n        const key = item.key;\n        const colorExists =\n          this.domainObject.configuration.barStyles.series[key] &&\n          this.domainObject.configuration.barStyles.series[key].color;\n        indices.push(index);\n        if (colorExists) {\n          colors.push(this.domainObject.configuration.barStyles.series[key].color);\n        } else {\n          colors.push(item.marker.color);\n        }\n      });\n      const plotUpdate = {\n        'marker.color': colors\n      };\n      Plotly.restyle(this.$refs.plot, plotUpdate, indices);\n    },\n    updateData() {\n      this.updatePlot();\n    },\n    updateLocalControlPosition() {\n      const localControl = this.$refs.localControl;\n      localControl.style.display = 'none';\n\n      const plot = this.$refs.plot;\n      const bgLayer = this.$el.querySelector('.bglayer');\n\n      const plotBoundingRect = plot.getBoundingClientRect();\n      const bgLayerBoundingRect = bgLayer.getBoundingClientRect();\n\n      const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;\n      const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;\n\n      localControl.style.top = `${top}px`;\n      localControl.style.left = `${left}px`;\n      localControl.style.display = 'block';\n    },\n    updatePlot() {\n      if (!this.$refs || !this.$refs.plot) {\n        return;\n      }\n\n      Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout());\n    },\n    zoom(eventData) {\n      const autorange = eventData['xaxis.autorange'];\n      const { autosize } = eventData;\n\n      if (autosize || autorange) {\n        this.isZoomed = false;\n        this.reset();\n\n        return;\n      }\n\n      this.isZoomed = true;\n      this.$emit('unsubscribe');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/bar/BarGraphView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <BarGraph\n    ref=\"barGraph\"\n    class=\"c-plot c-bar-chart-view\"\n    :data=\"trace\"\n    :plot-axis-title=\"plotAxisTitle\"\n    @subscribe=\"subscribeToAll\"\n    @unsubscribe=\"removeAllSubscriptions\"\n  />\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport BarGraph from './BarGraphPlot.vue';\n\nexport default {\n  components: {\n    BarGraph\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  data() {\n    this.telemetryObjects = {};\n    this.telemetryObjectFormats = {};\n    this.subscriptions = [];\n    this.composition = {};\n\n    return {\n      trace: []\n    };\n  },\n  computed: {\n    plotAxisTitle() {\n      const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};\n      const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';\n      const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';\n\n      return {\n        xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,\n        yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`\n      };\n    }\n  },\n  mounted() {\n    this.refreshData = this.refreshData.bind(this);\n    this.setTimeContext();\n\n    this.loadComposition();\n    this.unobserveAxes = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.axes',\n      this.refreshData\n    );\n    this.unobserveInterpolation = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.useInterpolation',\n      this.refreshData\n    );\n    this.unobserveBar = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.useBar',\n      this.refreshData\n    );\n  },\n  beforeUnmount() {\n    this.stopFollowingTimeContext();\n\n    this.removeAllSubscriptions();\n\n    if (!this.composition) {\n      return;\n    }\n\n    this.composition.off('add', this.addToComposition);\n    this.composition.off('remove', this.removeTelemetryObject);\n    if (this.unobserveAxes) {\n      this.unobserveAxes();\n    }\n\n    if (this.unobserveInterpolation) {\n      this.unobserveInterpolation();\n    }\n\n    if (this.unobserveBar) {\n      this.unobserveBar();\n    }\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n\n      this.timeContext = this.openmct.time.getContextForView(this.path);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.timeContext.on('boundsChanged', this.refreshData);\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('boundsChanged', this.refreshData);\n      }\n    },\n    addToComposition(telemetryObject) {\n      if (Object.values(this.telemetryObjects).length > 0) {\n        this.confirmRemoval(telemetryObject);\n      } else {\n        this.addTelemetryObject(telemetryObject);\n      }\n    },\n    confirmRemoval(telemetryObject) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will replace the current telemetry source. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              const oldTelemetryObject = Object.values(this.telemetryObjects)[0];\n              this.removeFromComposition(oldTelemetryObject);\n              this.removeTelemetryObject(oldTelemetryObject.identifier);\n              this.addTelemetryObject(telemetryObject);\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              this.removeFromComposition(telemetryObject);\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    removeFromComposition(telemetryObject) {\n      this.composition.remove(telemetryObject);\n    },\n    addTelemetryObject(telemetryObject) {\n      // grab information we need from the added telemetry object\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      this.telemetryObjects[key] = telemetryObject;\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);\n      const telemetryObjectPath = [telemetryObject, ...this.path];\n      const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(\n        telemetryObject,\n        telemetryObjectPath\n      );\n\n      // make an update object that's a clone of the existing styles object so we preserve existing choices\n      let stylesUpdate = {};\n      if (this.domainObject.configuration.barStyles.series[key]) {\n        stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);\n      }\n\n      stylesUpdate.name = telemetryObject.name;\n      stylesUpdate.type = telemetryObject.type;\n      stylesUpdate.isAlias = telemetryIsAlias;\n\n      // if something has changed, mutate and notify listeners\n      if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {\n        this.openmct.objects.mutate(\n          this.domainObject,\n          `configuration.barStyles.series[\"${key}\"]`,\n          stylesUpdate\n        );\n      }\n\n      // ask for the current telemetry data, then subscribe for changes\n      this.requestDataFor(telemetryObject);\n      this.subscribeToObject(telemetryObject);\n    },\n    setTrace(key, name, axisMetadata, xValues, yValues) {\n      let trace = {\n        key,\n        name: name,\n        x: xValues,\n        y: yValues,\n        xAxisMetadata: {},\n        yAxisMetadata: axisMetadata.yAxisMetadata,\n        type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',\n        mode: 'lines',\n        line: {\n          shape: this.domainObject.configuration.useInterpolation\n        },\n        marker: {\n          color: this.domainObject.configuration.barStyles.series[key].color\n        },\n        hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'\n      };\n      this.addTrace(trace, key);\n    },\n    addTrace(trace, key) {\n      if (!this.trace.length) {\n        this.trace = this.trace.concat([trace]);\n\n        return;\n      }\n\n      let isInTrace = false;\n      const newTrace = this.trace.map((currentTrace, index) => {\n        if (currentTrace.key !== key) {\n          return currentTrace;\n        }\n\n        isInTrace = true;\n\n        return trace;\n      });\n\n      this.trace = isInTrace ? newTrace : newTrace.concat([trace]);\n    },\n    getAxisMetadata(telemetryObject) {\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      if (!metadata) {\n        return {};\n      }\n\n      const yAxisMetadata = metadata.valuesForHints(['range'])[0];\n      //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only\n      const xAxisMetadata = metadata.valuesForHints(['range']).map((metaDatum) => {\n        metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);\n\n        return metaDatum;\n      });\n\n      return {\n        xAxisMetadata,\n        yAxisMetadata\n      };\n    },\n    getOptions() {\n      const { start, end } = this.timeContext.getBounds();\n\n      return {\n        end,\n        start,\n        size: 1,\n        strategy: 'latest'\n      };\n    },\n    loadComposition() {\n      this.composition = this.openmct.composition.get(this.domainObject);\n\n      this.composition.on('add', this.addToComposition);\n      this.composition.on('remove', this.removeTelemetryObject);\n      this.composition.load();\n    },\n    refreshData(bounds, isTick) {\n      if (!isTick) {\n        const telemetryObjects = Object.values(this.telemetryObjects);\n        telemetryObjects.forEach((telemetryObject) => {\n          //clear existing data\n          const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n          const axisMetadata = this.getAxisMetadata(telemetryObject);\n          this.setTrace(key, telemetryObject.name, axisMetadata, [], []);\n          //request new data\n          this.requestDataFor(telemetryObject);\n          this.subscribeToObject(telemetryObject);\n        });\n      }\n    },\n    removeAllSubscriptions() {\n      this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n      this.subscriptions = [];\n    },\n    removeSubscription(key) {\n      const found = this.subscriptions.findIndex((subscription) => subscription.key === key);\n      if (found > -1) {\n        this.subscriptions[found].unsubscribe();\n        this.subscriptions.splice(found, 1);\n      }\n    },\n    removeTelemetryObject(identifier) {\n      const key = this.openmct.objects.makeKeyString(identifier);\n      if (this.telemetryObjects[key]) {\n        delete this.telemetryObjects[key];\n      }\n\n      if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {\n        delete this.telemetryObjectFormats[key];\n      }\n\n      if (this.domainObject.configuration.barStyles.series[key]) {\n        delete this.domainObject.configuration.barStyles.series[key];\n        this.openmct.objects.mutate(\n          this.domainObject,\n          `configuration.barStyles.series[\"${key}\"]`,\n          undefined\n        );\n      }\n\n      this.removeSubscription(key);\n\n      this.trace = this.trace.filter((t) => t.key !== key);\n    },\n    addDataToGraph(telemetryObject, data, axisMetadata) {\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n\n      if (data.message) {\n        this.openmct.notifications.alert(data.message);\n      }\n\n      if (!this.isDataInTimeRange(data, key, telemetryObject)) {\n        return;\n      }\n\n      if (\n        this.domainObject.configuration.axes.xKey === undefined ||\n        this.domainObject.configuration.axes.yKey === undefined\n      ) {\n        const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);\n        this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {\n          xKey,\n          yKey\n        });\n      }\n\n      let xValues = [];\n      let yValues = [];\n      let xAxisMetadata = axisMetadata.xAxisMetadata.find(\n        (metadata) => metadata.key === this.domainObject.configuration.axes.xKey\n      );\n      if (xAxisMetadata && xAxisMetadata.isArrayValue) {\n        //populate x and y values\n        let metadataKey = this.domainObject.configuration.axes.xKey;\n        if (data[metadataKey] !== undefined) {\n          xValues = this.parse(key, metadataKey, data);\n        }\n\n        metadataKey = this.domainObject.configuration.axes.yKey;\n        if (data[metadataKey] !== undefined) {\n          yValues = this.parse(key, metadataKey, data);\n        }\n      } else {\n        //populate X and Y values for plotly\n        axisMetadata.xAxisMetadata\n          .filter((metadataObj) => !metadataObj.isArrayValue)\n          .forEach((metadata) => {\n            if (!xAxisMetadata) {\n              //Assign the first metadata to use for any formatting\n              xAxisMetadata = metadata;\n            }\n\n            xValues.push(metadata.name);\n            if (data[metadata.key]) {\n              const parsedValue = this.parse(key, metadata.key, data);\n              yValues.push(parsedValue);\n            } else {\n              yValues.push(null);\n            }\n          });\n      }\n\n      this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);\n    },\n    isDataInTimeRange(datum, key, telemetryObject) {\n      const timeSystemKey = this.timeContext.getTimeSystem().key;\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };\n\n      let currentTimestamp = this.parse(key, metadataValue.key, datum);\n\n      return currentTimestamp && this.timeContext.getBounds().end >= currentTimestamp;\n    },\n    format(telemetryObjectKey, metadataKey, data) {\n      const formats = this.telemetryObjectFormats[telemetryObjectKey];\n\n      return formats[metadataKey].format(data);\n    },\n    parse(telemetryObjectKey, metadataKey, datum) {\n      if (!datum) {\n        return;\n      }\n\n      const formats = this.telemetryObjectFormats[telemetryObjectKey];\n\n      return formats[metadataKey].parse(datum);\n    },\n    requestDataFor(telemetryObject) {\n      const axisMetadata = this.getAxisMetadata(telemetryObject);\n      const options = this.getOptions();\n      this.openmct.telemetry\n        .request(telemetryObject, options)\n        .then((data) => {\n          data.forEach((datum) => {\n            this.addDataToGraph(telemetryObject, datum, axisMetadata);\n          });\n        })\n        .catch((error) => {\n          console.warn(`Error fetching data`, error);\n        });\n    },\n    subscribeToObject(telemetryObject) {\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n\n      this.removeSubscription(key);\n\n      const options = this.getOptions();\n      const axisMetadata = this.getAxisMetadata(telemetryObject);\n      const unsubscribe = this.openmct.telemetry.subscribe(\n        telemetryObject,\n        (data) => this.addDataToGraph(telemetryObject, data, axisMetadata),\n        options\n      );\n\n      this.subscriptions.push({\n        key,\n        unsubscribe\n      });\n    },\n    subscribeToAll() {\n      const telemetryObjects = Object.values(this.telemetryObjects);\n      telemetryObjects.forEach(this.subscribeToObject);\n    },\n    identifyAxesKeys(metadata) {\n      const { xAxisMetadata, yAxisMetadata } = metadata;\n\n      let xKey;\n      let yKey;\n\n      // If xAxisMetadata contains array values, use the first one for xKey\n      const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);\n      const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);\n\n      if (arrayValues.length > 0) {\n        xKey = arrayValues[0].key;\n        yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;\n      } else if (nonArrayValues.length > 0) {\n        xKey = nonArrayValues[0].key;\n        yKey = 'none';\n      } else {\n        // Fallback if no valid xKey or yKey is found\n        xKey = 'none';\n        yKey = 'none';\n      }\n\n      return { xKey, yKey };\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/bar/BarGraphViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants.js';\nimport BarGraphView from './BarGraphView.vue';\n\nexport default function BarGraphViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: BAR_GRAPH_VIEW,\n    name: 'Bar Graph',\n    cssClass: 'icon-telemetry',\n    canView(domainObject, objectPath) {\n      return domainObject && domainObject.type === BAR_GRAPH_KEY;\n    },\n\n    canEdit(domainObject, objectPath) {\n      return domainObject && domainObject.type === BAR_GRAPH_KEY;\n    },\n\n    view(domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show(element) {\n          let isCompact = isCompactView(objectPath);\n\n          const { vNode, destroy } = mount(\n            {\n              components: {\n                BarGraphView\n              },\n              provide: {\n                openmct,\n                domainObject,\n                path: objectPath\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact\n                  }\n                };\n              },\n              template: '<bar-graph-view ref=\"graphComponent\" :options=\"options\"></bar-graph-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n        destroy() {\n          _destroy();\n        },\n        onClearData() {\n          component.$refs.graphComponent.refreshData();\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js",
    "content": "import mount from 'utils/mount';\n\nimport { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants.js';\nimport BarGraphOptions from './BarGraphOptions.vue';\n\nexport default function BarGraphInspectorViewProvider(openmct) {\n  return {\n    key: BAR_GRAPH_INSPECTOR_KEY,\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      let object = selection[0][0].context.item;\n\n      return object && object.type === BAR_GRAPH_KEY;\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                BarGraphOptions\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item\n              },\n              template: '<bar-graph-options></bar-graph-options>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/bar/inspector/BarGraphOptions.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-bar-graph-options js-bar-plot-option\">\n    <ul class=\"c-tree\">\n      <h2 title=\"Display properties for this object\">Bar Graph Series</h2>\n      <li>\n        <SeriesOptions\n          v-for=\"series in plotSeries\"\n          :key=\"series.keyString\"\n          :item=\"series\"\n          :color-palette=\"colorPalette\"\n        />\n      </li>\n    </ul>\n    <div class=\"grid-properties\">\n      <ul class=\"l-inspector-part\">\n        <h2 title=\"Y axis settings for this object\">Axes</h2>\n        <li class=\"grid-row\">\n          <div class=\"grid-cell label\" title=\"X axis selection.\">X Axis</div>\n          <div v-if=\"isEditing\" class=\"grid-cell value\">\n            <select v-model=\"xKey\" @change=\"updateForm('xKey')\">\n              <option\n                v-for=\"option in xKeyOptions\"\n                :key=\"`xKey-${option.value}`\"\n                :value=\"option.value\"\n                :selected=\"option.value === xKey\"\n              >\n                {{ option.name }}\n              </option>\n            </select>\n          </div>\n          <div v-else class=\"grid-cell value\">{{ xKeyLabel }}</div>\n        </li>\n        <li v-if=\"yKey !== ''\" class=\"grid-row\">\n          <div class=\"grid-cell label\" title=\"Y axis selection.\">Y Axis</div>\n          <div v-if=\"isEditing\" class=\"grid-cell value\">\n            <select v-model=\"yKey\" @change=\"updateForm('yKey')\">\n              <option\n                v-for=\"option in yKeyOptions\"\n                :key=\"`yKey-${option.value}`\"\n                :value=\"option.value\"\n                :selected=\"option.value === yKey\"\n              >\n                {{ option.name }}\n              </option>\n            </select>\n          </div>\n          <div v-else class=\"grid-cell value\">{{ yKeyLabel }}</div>\n        </li>\n      </ul>\n    </div>\n    <div class=\"grid-properties\">\n      <ul class=\"l-inspector-part\">\n        <h2 title=\"Settings for plot\">Settings</h2>\n        <li class=\"grid-row\">\n          <div v-if=\"isEditing\" class=\"grid-cell label\" title=\"Display style for the plot\">\n            Display Style\n          </div>\n          <div v-if=\"isEditing\" class=\"grid-cell value\">\n            <select v-model=\"useBar\" @change=\"updateBar\">\n              <option :value=\"true\">Bar</option>\n              <option :value=\"false\">Line</option>\n            </select>\n          </div>\n          <div v-if=\"!isEditing\" class=\"grid-cell label\" title=\"Display style for plot\">\n            Display Style\n          </div>\n          <div v-if=\"!isEditing\" class=\"grid-cell value\">\n            {{\n              {\n                true: 'Bar',\n                false: 'Line'\n              }[useBar]\n            }}\n          </div>\n        </li>\n        <li v-if=\"!useBar\" class=\"grid-row\">\n          <div\n            v-if=\"isEditing\"\n            class=\"grid-cell label\"\n            title=\"The rendering method to join lines for this series.\"\n          >\n            Line Method\n          </div>\n          <div v-if=\"isEditing\" class=\"grid-cell value\">\n            <select v-model=\"useInterpolation\" @change=\"updateInterpolation\">\n              <option value=\"linear\">Linear interpolate</option>\n              <option value=\"hv\">Step after</option>\n            </select>\n          </div>\n          <div\n            v-if=\"!isEditing\"\n            class=\"grid-cell label\"\n            title=\"The rendering method to join lines for this series.\"\n          >\n            Line Method\n          </div>\n          <div v-if=\"!isEditing\" class=\"grid-cell value\">\n            {{\n              {\n                linear: 'Linear interpolation',\n                hv: 'Step After'\n              }[useInterpolation]\n            }}\n          </div>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ColorPalette from '@/ui/color/ColorPalette';\n\nimport SeriesOptions from './SeriesOptions.vue';\n\nexport default {\n  components: {\n    SeriesOptions\n  },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      xKey: this.domainObject.configuration.axes.xKey,\n      yKey: this.domainObject.configuration.axes.yKey,\n      xKeyLabel: '',\n      yKeyLabel: '',\n      plotSeries: [],\n      yKeyOptions: [],\n      xKeyOptions: [],\n      isEditing: this.openmct.editor.isEditing(),\n      colorPalette: this.colorPalette,\n      useInterpolation: this.domainObject.configuration.useInterpolation,\n      useBar: this.domainObject.configuration.useBar\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  beforeMount() {\n    this.colorPalette = new ColorPalette();\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditState);\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.registerListeners();\n    this.composition.load();\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n    this.stopListening();\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    },\n    registerListeners() {\n      this.composition.on('add', this.addSeries);\n      this.composition.on('remove', this.removeSeries);\n      this.unobserve = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.axes',\n        this.setKeysAndSetupOptions\n      );\n    },\n    stopListening() {\n      this.composition.off('add', this.addSeries);\n      this.composition.off('remove', this.removeSeries);\n      if (this.unobserve) {\n        this.unobserve();\n      }\n    },\n    addSeries(series, index) {\n      this.plotSeries.push(series);\n      this.setupOptions();\n    },\n    removeSeries(seriesIdentifier) {\n      const index = this.plotSeries.findIndex((plotSeries) =>\n        this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier)\n      );\n      if (index >= 0) {\n        this.plotSeries.splice(index, 1);\n        this.setupOptions();\n      }\n    },\n    setKeysAndSetupOptions() {\n      this.xKey = this.domainObject.configuration.axes.xKey;\n      this.yKey = this.domainObject.configuration.axes.yKey;\n      this.setupOptions();\n    },\n    setupOptions() {\n      this.xKeyOptions = [];\n      this.yKeyOptions = [];\n      if (this.plotSeries.length <= 0) {\n        return;\n      }\n\n      let update = false;\n      const series = this.plotSeries[0];\n      const metadata = this.openmct.telemetry.getMetadata(series);\n      const metadataRangeValues = metadata.valuesForHints(['range']).map((metaDatum) => {\n        metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);\n\n        return metaDatum;\n      });\n      const metadataArrayValues = metadataRangeValues.filter(\n        (metadataObj) => metadataObj.isArrayValue\n      );\n      const metadataValues = metadataRangeValues.filter((metadataObj) => !metadataObj.isArrayValue);\n      metadataArrayValues.forEach((metadataValue) => {\n        this.xKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.key,\n          isArrayValue: metadataValue.isArrayValue\n        });\n        this.yKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.key,\n          isArrayValue: metadataValue.isArrayValue\n        });\n      });\n\n      //Metadata values that are not array values will be grouped together as x-axis only option.\n      // Here, the y-axis is not relevant.\n      if (metadataValues.length) {\n        this.xKeyOptions.push(\n          metadataValues.reduce(\n            (previousValue, currentValue) => {\n              return {\n                name: previousValue?.name\n                  ? `${previousValue.name}, ${currentValue.name}`\n                  : `${currentValue.name}`,\n                value: currentValue.key,\n                isArrayValue: currentValue.isArrayValue\n              };\n            },\n            { name: '' }\n          )\n        );\n      }\n\n      let xKeyOptionIndex;\n      let yKeyOptionIndex;\n\n      if (this.domainObject.configuration.axes.xKey) {\n        xKeyOptionIndex = this.xKeyOptions.findIndex(\n          (option) => option.value === this.domainObject.configuration.axes.xKey\n        );\n        if (xKeyOptionIndex > -1) {\n          this.xKey = this.xKeyOptions[xKeyOptionIndex].value;\n          this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;\n        }\n      } else {\n        if (this.xKey === undefined) {\n          update = true;\n          xKeyOptionIndex = 0;\n          this.xKey = this.xKeyOptions[xKeyOptionIndex].value;\n          this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;\n        }\n      }\n\n      if (metadataRangeValues.length > 1) {\n        if (\n          this.domainObject.configuration.axes.yKey &&\n          this.domainObject.configuration.axes.yKey !== 'none'\n        ) {\n          yKeyOptionIndex = this.yKeyOptions.findIndex(\n            (option) => option.value === this.domainObject.configuration.axes.yKey\n          );\n          if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {\n            this.yKey = this.yKeyOptions[yKeyOptionIndex].value;\n            this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;\n          }\n        } else {\n          if (this.yKey === undefined) {\n            if (metadataValues.length && metadataArrayValues.length === 0) {\n              update = true;\n              this.yKey = 'none';\n            } else {\n              yKeyOptionIndex = this.yKeyOptions.findIndex(\n                (option, index) => index !== xKeyOptionIndex\n              );\n              if (yKeyOptionIndex > -1) {\n                update = true;\n                this.yKey = this.yKeyOptions[yKeyOptionIndex].value;\n                this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;\n              }\n            }\n          }\n        }\n\n        this.yKeyOptions = this.yKeyOptions.map((option, index) => {\n          if (index === xKeyOptionIndex) {\n            option.name = `${option.name} (swap)`;\n            option.swap = yKeyOptionIndex;\n          } else {\n            option.name = option.name.replace(' (swap)', '');\n            option.swap = undefined;\n          }\n\n          return option;\n        });\n      } else if (\n        this.xKey !== undefined &&\n        this.domainObject.configuration.axes.yKey === undefined\n      ) {\n        this.domainObject.configuration.axes.yKey = 'none';\n      }\n\n      this.xKeyOptions = this.xKeyOptions.map((option, index) => {\n        if (index === yKeyOptionIndex) {\n          option.name = `${option.name} (swap)`;\n          option.swap = xKeyOptionIndex;\n        } else {\n          option.name = option.name.replace(' (swap)', '');\n          option.swap = undefined;\n        }\n\n        return option;\n      });\n\n      if (update === true) {\n        this.saveConfiguration();\n      }\n    },\n    updateForm(property) {\n      if (property === 'xKey') {\n        const xKeyOption = this.xKeyOptions.find((option) => option.value === this.xKey);\n        if (xKeyOption.swap !== undefined) {\n          //swap\n          this.yKey = this.xKeyOptions[xKeyOption.swap].value;\n        } else if (!xKeyOption.isArrayValue) {\n          this.yKey = 'none';\n        } else {\n          this.yKey = undefined;\n        }\n      } else if (property === 'yKey') {\n        const yKeyOption = this.yKeyOptions.find((option) => option.value === this.yKey);\n        if (yKeyOption.swap !== undefined) {\n          //swap\n          this.xKey = this.yKeyOptions[yKeyOption.swap].value;\n        }\n      }\n\n      this.saveConfiguration();\n    },\n    saveConfiguration() {\n      this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {\n        xKey: this.xKey,\n        yKey: this.yKey\n      });\n    },\n    updateInterpolation(event) {\n      this.openmct.objects.mutate(\n        this.domainObject,\n        `configuration.useInterpolation`,\n        this.useInterpolation\n      );\n    },\n    updateBar(event) {\n      this.openmct.objects.mutate(this.domainObject, `configuration.useBar`, this.useBar);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/bar/inspector/SeriesOptions.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul>\n    <li class=\"c-tree__item menus-to-left\" :class=\"aliasCss\" role=\"treeitem\">\n      <span\n        class=\"c-disclosure-triangle is-enabled flex-elem\"\n        :class=\"expandedCssClass\"\n        @click=\"expanded = !expanded\"\n      >\n      </span>\n\n      <div class=\"c-object-label\">\n        <div :class=\"[seriesCss]\"></div>\n        <div class=\"c-object-label__name\">{{ name }}</div>\n      </div>\n    </li>\n    <ul class=\"grid-properties\">\n      <li class=\"grid-row\">\n        <ColorSwatch\n          v-if=\"expanded\"\n          :current-color=\"currentColor\"\n          title=\"Manually set the color for this bar graph series.\"\n          edit-title=\"Manually set the color for this bar graph series.\"\n          view-title=\"The color for this bar graph series.\"\n          short-label=\"Color\"\n          @color-set=\"setColor\"\n        />\n      </li>\n    </ul>\n  </ul>\n</template>\n\n<script>\nimport Color from '@/ui/color/Color';\nimport ColorSwatch from '@/ui/color/ColorSwatch.vue';\n\nexport default {\n  components: {\n    ColorSwatch\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    colorPalette: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      currentColor: undefined,\n      name: '',\n      type: '',\n      isAlias: false,\n      expanded: false\n    };\n  },\n  computed: {\n    expandedCssClass() {\n      return this.expanded ? 'c-disclosure-triangle--expanded' : '';\n    },\n    seriesCss() {\n      const type = this.openmct.types.get(this.type);\n      if (type && type.definition && type.definition.cssClass) {\n        return `c-object-label__type-icon ${type.definition.cssClass}`;\n      }\n\n      return 'c-object-label__type-icon';\n    },\n    aliasCss() {\n      let cssClass = '';\n      if (this.isAlias) {\n        cssClass = 'is-alias';\n      }\n\n      return cssClass;\n    }\n  },\n  watch: {\n    item: {\n      handler() {\n        this.initColorAndName();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.initColorAndName();\n    this.removeBarStylesListener = this.openmct.objects.observe(\n      this.domainObject,\n      `configuration.barStyles.series[\"${this.key}\"]`,\n      this.initColorAndName\n    );\n  },\n  beforeUnmount() {\n    if (this.removeBarStylesListener) {\n      this.removeBarStylesListener();\n    }\n  },\n  methods: {\n    initColorAndName() {\n      this.key = this.openmct.objects.makeKeyString(this.item.identifier);\n      // this is called before the plot is initialized\n      if (!this.domainObject.configuration.barStyles.series[this.key]) {\n        const color = this.colorPalette.getNextColor().asHexString();\n        this.domainObject.configuration.barStyles.series[this.key] = {\n          color,\n          type: '',\n          name: '',\n          isAlias: false\n        };\n      } else if (!this.domainObject.configuration.barStyles.series[this.key].color) {\n        this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette\n          .getNextColor()\n          .asHexString();\n      }\n\n      this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;\n      this.name = this.domainObject.configuration.barStyles.series[this.key].name;\n      this.type = this.domainObject.configuration.barStyles.series[this.key].type;\n      this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;\n\n      let colorHexString = this.currentColor;\n      const colorObject = Color.fromHexString(colorHexString);\n\n      this.colorPalette.remove(colorObject);\n    },\n    setColor(chosenColor) {\n      this.currentColor = chosenColor.asHexString();\n      this.openmct.objects.mutate(\n        this.domainObject,\n        `configuration.barStyles.series[\"${this.key}\"].color`,\n        this.currentColor\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/bar/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport BarGraphCompositionPolicy from './BarGraphCompositionPolicy.js';\nimport { BAR_GRAPH_KEY } from './BarGraphConstants.js';\nimport BarGraphViewProvider from './BarGraphViewProvider.js';\nimport BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.types.addType(BAR_GRAPH_KEY, {\n      key: BAR_GRAPH_KEY,\n      name: 'Graph',\n      cssClass: 'icon-bar-chart',\n      description: 'Visualize data as a bar or line graph.',\n      creatable: true,\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          barStyles: { series: {} },\n          axes: {},\n          useInterpolation: 'linear',\n          useBar: true\n        };\n      },\n      priority: 891\n    });\n\n    openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));\n\n    openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));\n\n    openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/bar/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// import BarGraph from './BarGraphPlot.vue';\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants.js';\nimport BarGraphPlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let mockObjectPath;\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'time-strip',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    const testTelemetry = [\n      {\n        utc: 1,\n        'some-key': ['1.3222'],\n        'some-other-key': [1]\n      },\n      {\n        utc: 2,\n        'some-key': ['2.555'],\n        'some-other-key': [2]\n      },\n      {\n        utc: 3,\n        'some-key': ['3.888'],\n        'some-other-key': [3]\n      }\n    ];\n\n    openmct = createOpenMct();\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(testTelemetry);\n\n      return telemetryPromise;\n    });\n\n    openmct.install(new BarGraphPlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n    document.body.appendChild(element);\n\n    spyOn(window, 'ResizeObserver').and.returnValue({\n      observe() {},\n      unobserve() {},\n      disconnect() {}\n    });\n\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 4\n    });\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach((done) => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n    resetApplicationState(openmct).then(done).catch(done);\n  });\n\n  describe('The bar graph view', () => {\n    let barGraphObject;\n\n    let mockComposition;\n\n    beforeEach(async () => {\n      barGraphObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-plot'\n        },\n        configuration: {\n          barStyles: {\n            series: {}\n          },\n          axes: {},\n          useInterpolation: 'linear',\n          useBar: true\n        },\n        type: 'telemetry.plot.bar-graph',\n        name: 'Test Bar Graph'\n      };\n\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        return [];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      await nextTick();\n    });\n\n    it('provides a bar graph view', () => {\n      const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);\n      const plotViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === BAR_GRAPH_VIEW\n      );\n      expect(plotViewProvider).toBeDefined();\n    });\n\n    xit('Renders plotly bar graph', () => {\n      let barChartElement = element.querySelectorAll('.plotly');\n      expect(barChartElement.length).toBe(1);\n    });\n\n    it('Handles dots in telemetry id', () => {\n      const dotFullTelemetryObject = {\n        identifier: {\n          namespace: 'someNamespace',\n          key: '~OpenMCT~outer.test-object.foo.bar'\n        },\n        type: 'test-dotful-object',\n        name: 'A Dotful Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key.foo.name.45',\n              name: 'Some dotful attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key.bar.344.rad',\n              name: 'Another dotful attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);\n      const plotViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === BAR_GRAPH_VIEW\n      );\n      const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);\n      barGraphView.show(child, true);\n      mockComposition.emit('add', dotFullTelemetryObject);\n      expect(\n        barGraphObject.configuration.barStyles.series[\n          'someNamespace:~OpenMCT~outer.test-object.foo.bar'\n        ].name\n      ).toEqual('A Dotful Object');\n    });\n  });\n\n  describe('The spectral plot view for telemetry objects with array values', () => {\n    let barGraphObject;\n\n    let mockComposition;\n\n    beforeEach(async () => {\n      barGraphObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-plot'\n        },\n        configuration: {\n          barStyles: {\n            series: {}\n          },\n          axes: {\n            xKey: 'some-key',\n            yKey: 'some-other-key'\n          },\n          useInterpolation: 'linear',\n          useBar: false\n        },\n        type: 'telemetry.plot.bar-graph',\n        name: 'Test Bar Graph'\n      };\n\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        return [];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      await nextTick();\n    });\n\n    it('Renders spectral plots', async () => {\n      const dotFullTelemetryObject = {\n        identifier: {\n          namespace: 'someNamespace',\n          key: '~OpenMCT~outer.test-object.foo.bar'\n        },\n        type: 'test-dotful-object',\n        name: 'A Dotful Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              formatString: '%0.2f[]',\n              hints: {\n                range: 1\n              },\n              source: 'some-key'\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              format: 'number[]',\n              hints: {\n                range: 2\n              },\n              source: 'some-other-key'\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);\n      const plotViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === BAR_GRAPH_VIEW\n      );\n      const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);\n      barGraphView.show(child, true);\n      mockComposition.emit('add', dotFullTelemetryObject);\n\n      await nextTick();\n      await nextTick();\n\n      const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');\n      expect(plotElement).not.toBeNull();\n    });\n  });\n\n  describe('the bar graph objects', () => {\n    const mockObject = {\n      name: 'A very nice bar graph',\n      key: BAR_GRAPH_KEY,\n      creatable: true\n    };\n\n    it('defines a bar graph object type with the correct key', () => {\n      const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;\n      expect(objectDef.key).toEqual(mockObject.key);\n    });\n\n    it('is creatable', () => {\n      const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;\n      expect(objectDef.creatable).toEqual(mockObject.creatable);\n    });\n  });\n\n  describe('The bar graph composition policy', () => {\n    it('allows composition for telemetry that contain at least one range', () => {\n      const parent = {\n        composition: [],\n        configuration: {},\n        name: 'Some Bar Graph',\n        type: 'telemetry.plot.bar-graph',\n        location: 'mine',\n        modified: 1631005183584,\n        persisted: 1631005183502,\n        identifier: {\n          namespace: '',\n          key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n        }\n      };\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              source: 'some-key',\n              name: 'Some attribute',\n              format: 'enum',\n              enumerations: [\n                {\n                  value: 0,\n                  string: 'OFF'\n                },\n                {\n                  value: 1,\n                  string: 'ON'\n                }\n              ],\n              hints: {\n                range: 1\n              }\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(parent);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).not.toThrow();\n      expect(parent.composition.length).toBe(1);\n    });\n\n    it(\"disallows composition for telemetry that don't contain any range hints\", () => {\n      const parent = {\n        composition: [],\n        configuration: {},\n        name: 'Some Bar Graph',\n        type: 'telemetry.plot.bar-graph',\n        location: 'mine',\n        modified: 1631005183584,\n        persisted: 1631005183502,\n        identifier: {\n          namespace: '',\n          key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n        }\n      };\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              name: 'Some attribute'\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute'\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(parent);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).toThrow();\n      expect(parent.composition.length).toBe(0);\n    });\n    it('disallows composition for condition sets', () => {\n      const parent = {\n        composition: [],\n        configuration: {},\n        name: 'Some Bar Graph',\n        type: 'telemetry.plot.bar-graph',\n        location: 'mine',\n        modified: 1631005183584,\n        persisted: 1631005183502,\n        identifier: {\n          namespace: '',\n          key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n        }\n      };\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'conditionSet',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              format: 'enum',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 1\n              }\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(parent);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).toThrow();\n      expect(parent.composition.length).toBe(0);\n    });\n  });\n  describe('the inspector view', () => {\n    let mockComposition;\n    let testDomainObject;\n    let selection;\n    let plotInspectorView;\n    let viewContainer;\n    let optionsElement;\n    beforeEach(async () => {\n      testDomainObject = {\n        identifier: {\n          namespace: '',\n          key: '~Some~foo.bar'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      selection = [\n        [\n          {\n            context: {\n              item: {\n                id: 'test-object',\n                identifier: {\n                  key: 'test-object',\n                  namespace: ''\n                },\n                type: 'telemetry.plot.bar-graph',\n                configuration: {\n                  barStyles: {\n                    series: {\n                      '~Some~foo.bar': {\n                        name: 'A telemetry object',\n                        type: 'some-type',\n                        isAlias: true\n                      }\n                    }\n                  },\n                  axes: {},\n                  useInterpolation: 'linear',\n                  useBar: true\n                },\n                composition: [\n                  {\n                    identifier: {\n                      key: '~Some~foo.bar'\n                    }\n                  }\n                ]\n              }\n            }\n          },\n          {\n            context: {\n              item: {\n                type: 'time-strip',\n                identifier: {\n                  key: 'some-other-key',\n                  namespace: ''\n                }\n              }\n            }\n          }\n        ]\n      ];\n\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testDomainObject);\n\n        return [testDomainObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      viewContainer = document.createElement('div');\n      child.append(viewContainer);\n\n      const applicableViews = openmct.inspectorViews.get(selection);\n      plotInspectorView = applicableViews.filter((view) => view.key === BAR_GRAPH_INSPECTOR_KEY)[0];\n      plotInspectorView.show(viewContainer);\n\n      await nextTick();\n      optionsElement = element.querySelector('.c-bar-graph-options');\n    });\n\n    afterEach(() => {\n      plotInspectorView.destroy();\n    });\n\n    it('it renders the options', () => {\n      expect(optionsElement).toBeDefined();\n    });\n\n    it('shows the name', () => {\n      const seriesEl = optionsElement.querySelector('.c-object-label__name');\n      expect(seriesEl.innerHTML).toEqual('A telemetry object');\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';\n\nexport default function ScatterPlotCompositionPolicy(openmct) {\n  function hasRange(metadata) {\n    const rangeValues = metadata.valuesForHints(['range']).map((value) => {\n      return value.source;\n    });\n\n    const uniqueRangeValues = new Set(rangeValues);\n\n    return uniqueRangeValues && uniqueRangeValues.size > 1;\n  }\n\n  function hasScatterPlotTelemetry(domainObject) {\n    if (!openmct.telemetry.isTelemetryObject(domainObject)) {\n      return false;\n    }\n\n    let metadata = openmct.telemetry.getMetadata(domainObject);\n\n    return metadata.values().length > 0 && hasRange(metadata);\n  }\n\n  return {\n    allow: function (parent, child) {\n      if (parent.type === SCATTER_PLOT_KEY) {\n        if (child.type === 'conditionSet' || !hasScatterPlotTelemetry(child)) {\n          return false;\n        }\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/scatter/ScatterPlotForm.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <div class=\"c-form--sub-grid\">\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\" :class=\"{ req: isRequired }\"> </span>\n          <label>Minimum X axis value</label>\n          <input\n            ref=\"domainMin\"\n            v-model.number=\"domainMin\"\n            data-field-name=\"domainMin\"\n            type=\"number\"\n            @input=\"onChange('domainMin')\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\" :class=\"{ req: isRequired }\"> </span>\n          <label>Maximum X axis value</label>\n          <input\n            ref=\"domainMax\"\n            v-model.number=\"domainMax\"\n            data-field-name=\"domainMax\"\n            type=\"number\"\n            @input=\"onChange('domainMax')\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\" :class=\"{ req: isRequired }\"> </span>\n          <label>Minimum Y axis value</label>\n          <input\n            ref=\"rangeMin\"\n            v-model.number=\"rangeMin\"\n            data-field-name=\"rangeMin\"\n            type=\"number\"\n            @input=\"onChange('rangeMin')\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\" :class=\"{ req: isRequired }\"> </span>\n          <label>Maximum Y axis value</label>\n          <input\n            ref=\"rangeMax\"\n            v-model.number=\"rangeMax\"\n            data-field-name=\"rangeMax\"\n            type=\"number\"\n            @input=\"onChange('rangeMax')\"\n          />\n        </div>\n      </div>\n    </span>\n  </span>\n</template>\n\n<script>\nexport default {\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    return {\n      rangeMax: this.model.value.rangeMax,\n      rangeMin: this.model.value.rangeMin,\n      domainMax: this.model.value.domainMax,\n      domainMin: this.model.value.domainMin\n    };\n  },\n  computed: {\n    isRequired() {\n      return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(\n        (value) => value !== undefined && value !== ''\n      );\n    }\n  },\n  methods: {\n    onChange(property) {\n      if (this[property] === '') {\n        this[property] = undefined;\n      }\n\n      const data = {\n        model: this.model,\n        value: {\n          rangeMax: this.rangeMax,\n          rangeMin: this.rangeMin,\n          domainMax: this.domainMax,\n          domainMin: this.domainMin\n        }\n      };\n\n      if (property) {\n        this.model.validate(data);\n      }\n\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/ScatterPlotView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <ScatterPlotWithUnderlay\n    class=\"c-plot c-scatter-chart-view\"\n    :data=\"trace\"\n    :plot-axis-title=\"plotAxisTitle\"\n    @subscribe=\"subscribeToAll\"\n    @unsubscribe=\"removeAllSubscriptions\"\n  />\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue';\n\nexport default {\n  components: {\n    ScatterPlotWithUnderlay\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  data() {\n    this.telemetryObjects = {};\n    this.telemetryObjectFormats = {};\n    this.valuesByTimestamp = {};\n    this.subscriptions = [];\n\n    return {\n      trace: []\n    };\n  },\n  computed: {\n    plotAxisTitle() {\n      const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};\n      const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';\n      const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';\n\n      return {\n        xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,\n        yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`\n      };\n    }\n  },\n  mounted() {\n    this.setTimeContext();\n    this.loadComposition();\n    this.reloadTelemetry = this.reloadTelemetry.bind(this);\n    this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500);\n    this.unobserve = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.axes',\n      this.reloadTelemetry\n    );\n    this.unobserveUnderlayRanges = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.ranges',\n      this.reloadTelemetry\n    );\n  },\n  beforeUnmount() {\n    this.stopFollowingTimeContext();\n\n    if (!this.composition) {\n      return;\n    }\n\n    this.removeAllSubscriptions();\n\n    this.composition.off('add', this.addToComposition);\n    this.composition.off('remove', this.removeTelemetryObject);\n    if (this.unobserve) {\n      this.unobserve();\n    }\n\n    if (this.unobserveUnderlayRanges) {\n      this.unobserveUnderlayRanges();\n    }\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n\n      this.timeContext = this.openmct.time.getContextForView(this.path);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.timeContext.on('boundsChanged', this.reloadTelemetryOnBoundsChange);\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('boundsChanged', this.reloadTelemetryOnBoundsChange);\n      }\n    },\n    addToComposition(telemetryObject) {\n      if (Object.values(this.telemetryObjects).length > 0) {\n        this.confirmRemoval(telemetryObject);\n      } else {\n        this.addTelemetryObject(telemetryObject);\n      }\n    },\n    removeFromComposition(telemetryObject) {\n      this.composition.remove(telemetryObject);\n    },\n    addTelemetryObject(telemetryObject) {\n      // grab information we need from the added telemetry object\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      this.telemetryObjects[key] = telemetryObject;\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);\n      this.getDataForTelemetry(key);\n    },\n    confirmRemoval(telemetryObject) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will replace the current telemetry source. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              const oldTelemetryObject = Object.values(this.telemetryObjects)[0];\n              this.removeFromComposition(oldTelemetryObject);\n              this.removeTelemetryObject(oldTelemetryObject.identifier);\n              this.valuesByTimestamp = {};\n              this.addTelemetryObject(telemetryObject);\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              this.removeFromComposition(telemetryObject);\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    getTelemetryProcessor(keyString) {\n      return (telemetry) => {\n        //Check that telemetry object has not been removed since telemetry was requested.\n        const telemetryObject = this.telemetryObjects[keyString];\n        if (!telemetryObject) {\n          return;\n        }\n\n        telemetry.forEach((datum) => {\n          this.addDataToGraph(telemetryObject, datum);\n        });\n        this.updateTrace(telemetryObject);\n      };\n    },\n    getAxisMetadata(telemetryObject) {\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      if (!metadata) {\n        return {};\n      }\n\n      return metadata.valuesForHints(['range']);\n    },\n    loadComposition() {\n      this.composition = this.openmct.composition.get(this.domainObject);\n      this.composition.on('add', this.addToComposition);\n      this.composition.on('remove', this.removeTelemetryObject);\n      this.composition.load();\n    },\n    reloadTelemetryOnBoundsChange(bounds, isTick) {\n      if (!isTick) {\n        this.reloadTelemetry();\n      }\n    },\n    reloadTelemetry() {\n      this.valuesByTimestamp = {};\n\n      Object.keys(this.telemetryObjects).forEach((key) => {\n        this.getDataForTelemetry(key);\n      });\n    },\n    getDataForTelemetry(key) {\n      const telemetryObject = this.telemetryObjects[key];\n      if (!telemetryObject) {\n        return;\n      }\n\n      const telemetryProcessor = this.getTelemetryProcessor(key);\n      const options = this.getOptions();\n      this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor);\n      this.subscribeToObject(telemetryObject);\n    },\n    removeTelemetryObject(identifier) {\n      const key = this.openmct.objects.makeKeyString(identifier);\n      if (this.telemetryObjects[key]) {\n        delete this.telemetryObjects[key];\n      }\n\n      if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {\n        delete this.telemetryObjectFormats[key];\n      }\n\n      this.removeSubscription(key);\n    },\n    addDataToGraph(telemetryObject, data) {\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n\n      if (data.message) {\n        this.openmct.notifications.alert(data.message);\n      }\n\n      if (\n        !this.domainObject.configuration.axes.xKey ||\n        !this.domainObject.configuration.axes.yKey\n      ) {\n        return;\n      }\n\n      const timestamp = this.getTimestampForDatum(data, key, telemetryObject);\n      let valueForTimestamp = this.valuesByTimestamp[timestamp] || {};\n\n      //populate x values\n      let metadataKey = this.domainObject.configuration.axes.xKey;\n      if (data[metadataKey] !== undefined) {\n        valueForTimestamp.x = this.format(key, metadataKey, data);\n      }\n\n      metadataKey = this.domainObject.configuration.axes.yKey;\n      if (data[metadataKey] !== undefined) {\n        valueForTimestamp.y = this.format(key, metadataKey, data);\n      }\n\n      this.valuesByTimestamp[timestamp] = valueForTimestamp;\n    },\n    updateTrace(telemetryObject) {\n      const xAndyValues = Object.values(this.valuesByTimestamp);\n      const xValues = xAndyValues.map((value) => value.x);\n      const yValues = xAndyValues.map((value) => value.y);\n      const axisMetadata = this.getAxisMetadata(telemetryObject);\n      const xAxisMetadata = axisMetadata.find(\n        (metadata) => metadata.source === this.domainObject.configuration.axes.xKey\n      );\n      let yAxisMetadata = {};\n      if (this.domainObject.configuration.axes.yKey) {\n        yAxisMetadata = axisMetadata.find(\n          (metadata) => metadata.source === this.domainObject.configuration.axes.yKey\n        );\n      }\n\n      let trace = {\n        key: this.openmct.objects.makeKeyString(this.domainObject.identifier),\n        name: this.domainObject.name,\n        x: xValues,\n        y: yValues,\n        text: yValues.map(String),\n        xAxisMetadata: xAxisMetadata,\n        yAxisMetadata: yAxisMetadata,\n        type: 'scatter',\n        mode: 'markers',\n        marker: {\n          color: this.domainObject.configuration.styles.color\n        },\n        hoverinfo: 'x+y'\n      };\n\n      if (\n        this.domainObject.configuration.ranges !== undefined &&\n        this.domainObject.configuration.ranges.domainMin !== undefined &&\n        this.domainObject.configuration.ranges.domainMax !== undefined\n      ) {\n        trace.xaxis = {\n          min: this.domainObject.configuration.ranges.domainMin,\n          max: this.domainObject.configuration.ranges.domainMax\n        };\n      }\n\n      if (\n        this.domainObject.configuration.ranges !== undefined &&\n        this.domainObject.configuration.ranges.rangeMin !== undefined &&\n        this.domainObject.configuration.ranges.rangeMax !== undefined\n      ) {\n        trace.yaxis = {\n          min: this.domainObject.configuration.ranges.rangeMin,\n          max: this.domainObject.configuration.ranges.rangeMax\n        };\n      }\n\n      this.trace = [trace];\n    },\n    getTimestampForDatum(datum, key, telemetryObject) {\n      const timeSystemKey = this.timeContext.getTimeSystem().key;\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };\n\n      return this.parse(key, metadataValue.source, datum);\n    },\n    format(telemetryObjectKey, metadataKey, data) {\n      const formats = this.telemetryObjectFormats[telemetryObjectKey];\n\n      return formats[metadataKey].format(data);\n    },\n    parse(telemetryObjectKey, metadataKey, datum) {\n      if (!datum) {\n        return;\n      }\n\n      const formats = this.telemetryObjectFormats[telemetryObjectKey];\n\n      return formats[metadataKey].parse(datum);\n    },\n    getOptions() {\n      const { start, end } = this.timeContext.getBounds();\n\n      return {\n        end,\n        start\n      };\n    },\n    subscribeToObject(telemetryObject) {\n      const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n\n      this.removeSubscription(key);\n\n      const options = this.getOptions();\n      const unsubscribe = this.openmct.telemetry.subscribe(\n        telemetryObject,\n        (data) => this.addDataToGraph(telemetryObject, data),\n        options\n      );\n\n      this.subscriptions.push({\n        key,\n        unsubscribe\n      });\n    },\n    subscribeToAll() {\n      const telemetryObjects = Object.values(this.telemetryObjects);\n      telemetryObjects.forEach(this.subscribeToObject);\n    },\n    removeAllSubscriptions() {\n      this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n      this.subscriptions = [];\n    },\n    removeSubscription(key) {\n      const found = this.subscriptions.findIndex((subscription) => subscription.key === key);\n      if (found > -1) {\n        this.subscriptions[found].unsubscribe();\n        this.subscriptions.splice(found, 1);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/ScatterPlotViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';\nimport ScatterPlotView from './ScatterPlotView.vue';\n\nexport default function ScatterPlotViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === TIME_STRIP_KEY);\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: SCATTER_PLOT_VIEW,\n    name: 'Scatter Plot',\n    cssClass: 'icon-telemetry',\n    canView(domainObject, objectPath) {\n      return domainObject && domainObject.type === SCATTER_PLOT_KEY;\n    },\n\n    canEdit(domainObject, objectPath) {\n      return domainObject && domainObject.type === SCATTER_PLOT_KEY;\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const isCompact = isCompactView(objectPath);\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                ScatterPlotView\n              },\n              provide: {\n                openmct,\n                domainObject,\n                path: objectPath\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact\n                  }\n                };\n              },\n              template: '<scatter-plot-view :options=\"options\"></scatter-plot-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"plotWrapper\" class=\"has-local-controls\" :class=\"{ 's-unsynced': isZoomed }\">\n    <div v-if=\"isZoomed\" class=\"l-state-indicators\">\n      <span\n        class=\"l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle\"\n        title=\"This plot is not currently displaying the latest data. Reset pan/zoom to view latest data.\"\n      ></span>\n    </div>\n    <div ref=\"plot\" class=\"c-scatter-chart\"></div>\n    <div\n      ref=\"localControl\"\n      class=\"gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover\"\n    >\n      <button\n        v-if=\"data.length\"\n        class=\"c-button icon-reset\"\n        :disabled=\"!isZoomed\"\n        title=\"Reset pan/zoom\"\n        @click=\"reset()\"\n      ></button>\n    </div>\n  </div>\n</template>\n<script>\nimport Plotly from 'plotly-basic';\n\nconst MULTI_AXES_X_PADDING_PERCENT = {\n  LEFT: 8,\n  RIGHT: 94\n};\n\nimport { getValidatedData } from '@/plugins/plan/util';\n\nconst PATH_COLORS = ['blue', 'red', 'green'];\nconst MARKER_COLOR = 'white';\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    data: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    plotAxisTitle: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['subscribe', 'unsubscribe'],\n  data() {\n    return {\n      isZoomed: false,\n      yAxisRange: {\n        min: '',\n        max: ''\n      },\n      xAxisRange: {\n        min: '',\n        max: ''\n      }\n    };\n  },\n  watch: {\n    data: {\n      immediate: false,\n      handler() {\n        this.updateData();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.getUnderlayPlotData();\n\n    Plotly.newPlot(\n      this.$refs.plot,\n      Array.from(this.data.concat(this.getShapes(this.shapesData))),\n      this.getLayout(),\n      {\n        responsive: true,\n        displayModeBar: false\n      }\n    );\n    this.registerListeners();\n\n    this.$refs.plot.on('plotly_relayout', this.zoom);\n  },\n  beforeUnmount() {\n    if (this.$refs.plot && this.$refs.plot.off) {\n      this.$refs.plot.off('plotly_relayout', this.zoom);\n    }\n\n    if (this.plotResizeObserver) {\n      this.plotResizeObserver.disconnect();\n      clearTimeout(this.resizeTimer);\n    }\n\n    if (this.unlistenUnderlay) {\n      this.unlistenUnderlay();\n    }\n\n    if (this.unlistenUnderlayRanges) {\n      this.unlistenUnderlayRanges();\n    }\n\n    if (this.unobserveColorChanges) {\n      this.unobserveColorChanges();\n    }\n\n    Plotly.purge(this.$refs.plot);\n  },\n  methods: {\n    getUnderlayPlotData() {\n      if (this.domainObject.selectFile) {\n        this.shapesData = getValidatedData(this.domainObject);\n      } else {\n        this.shapesData = [];\n      }\n    },\n    observeForUnderlayPlotChanges() {\n      this.getUnderlayPlotData();\n      this.updateData();\n    },\n    getAxisMinMax() {\n      if (!this.data.length) {\n        return;\n      }\n\n      // For now, use x and y axes min, max values only if an underlay is available\n      if (this.shapesData.length && this.data[0].xaxis) {\n        this.xAxisRange = this.data[0].xaxis;\n      }\n\n      if (this.shapesData.length && this.data[0].yaxis) {\n        this.yAxisRange = this.data[0].yaxis;\n      }\n    },\n    getLayout() {\n      this.getAxisMinMax();\n\n      const yAxesMeta = this.getYAxisMeta();\n      const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);\n      const xAxisDomain = this.getXAxisDomain(yAxesMeta);\n\n      const shapes = this.shapesData.map((shapeData, index) => {\n        if (\n          !shapeData.x ||\n          !shapeData.y ||\n          !shapeData.x.length ||\n          !shapeData.y.length ||\n          shapeData.x.length !== shapeData.y.length\n        ) {\n          return '';\n        }\n\n        let path = `M ${shapeData.x[0]},${shapeData.y[0]}`;\n        shapeData.x.forEach((point, shapeIndex) => {\n          if (shapeIndex > 0) {\n            path = `${path} L${point},${shapeData.y[shapeIndex]}`;\n          }\n        });\n\n        return {\n          path,\n          type: 'path',\n          line: {\n            color: PATH_COLORS[index]\n          },\n          opacity: 0.5\n        };\n      });\n\n      return {\n        autosize: true,\n        showlegend: false,\n        textposition: 'auto',\n        font: {\n          family: 'Helvetica Neue, Helvetica, Arial, sans-serif',\n          size: '12px',\n          color: '#666'\n        },\n        xaxis: {\n          domain: xAxisDomain,\n          range: [this.xAxisRange.min, this.xAxisRange.max],\n          title: this.plotAxisTitle.xAxisTitle,\n          automargin: true\n        },\n        yaxis: primaryYaxis,\n        margin: {\n          l: 5,\n          r: 5,\n          t: 5,\n          b: 0\n        },\n        paper_bgcolor: 'transparent',\n        plot_bgcolor: 'transparent',\n        shapes,\n        layer: 'below'\n      };\n    },\n    getYAxisMeta() {\n      const yAxisMeta = {};\n\n      this.data.forEach((datum) => {\n        const yAxisMetadata = datum.yAxisMetadata;\n        const range = '1';\n        const side = 'left';\n        const name = yAxisMetadata.name;\n        const unit = yAxisMetadata.units;\n\n        yAxisMeta[range] = {\n          range,\n          side,\n          name,\n          unit\n        };\n      });\n\n      return yAxisMeta;\n    },\n    getXAxisDomain(yAxisMeta) {\n      let leftPaddingPerc = 0;\n      let rightPaddingPerc = 100;\n      let rightSide =\n        yAxisMeta && Object.values(yAxisMeta).filter((axisMeta) => axisMeta.side === 'right');\n      let leftSide =\n        yAxisMeta && Object.values(yAxisMeta).filter((axisMeta) => axisMeta.side === 'left');\n      if (yAxisMeta && rightSide.length > 1) {\n        rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;\n      }\n\n      if (yAxisMeta && leftSide.length > 1) {\n        leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;\n      }\n\n      return [leftPaddingPerc / 100, rightPaddingPerc / 100];\n    },\n    getYaxisLayout(yAxisMeta) {\n      if (!yAxisMeta) {\n        return {};\n      }\n\n      const { name, range, side = 'left', unit } = yAxisMeta;\n      const title = `${name} ${unit ? '(' + unit + ')' : ''}`;\n      const yaxis = {\n        automargin: true,\n        title\n      };\n\n      let yRange = this.yAxisRange;\n      if (range === '1') {\n        yaxis.range = [yRange.min, yRange.max];\n\n        return yaxis;\n      }\n\n      yaxis.range = [yRange.min, yRange.max];\n      yaxis.anchor = side.toLowerCase() === 'left' ? 'free' : 'x';\n      yaxis.showline = side.toLowerCase() === 'left';\n      yaxis.side = side.toLowerCase();\n      yaxis.overlaying = 'y';\n      yaxis.position = 0.01;\n\n      return yaxis;\n    },\n    registerListeners() {\n      this.unobserveColorChanges = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.styles.color',\n        this.updateColors\n      );\n      this.unlistenUnderlay = this.openmct.objects.observe(\n        this.domainObject,\n        'selectFile',\n        this.observeForUnderlayPlotChanges\n      );\n      this.unlistenUnderlayRanges = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.ranges',\n        this.updateData\n      );\n      this.resizeTimer = false;\n      if (window.ResizeObserver) {\n        this.plotResizeObserver = new ResizeObserver(() => {\n          // debounce and trigger window resize so that plotly can resize the plot\n          clearTimeout(this.resizeTimer);\n          this.resizeTimer = setTimeout(() => {\n            window.dispatchEvent(new Event('resize'));\n          }, 250);\n        });\n        this.plotResizeObserver.observe(this.$refs.plotWrapper);\n      }\n    },\n    updateColors() {\n      const colors = [];\n      const indices = [];\n      this.data.forEach((item, index) => {\n        const colorExists = this.domainObject.configuration.styles.color;\n        indices.push(index);\n        if (colorExists) {\n          colors.push(this.domainObject.configuration.styles.color);\n        } else {\n          colors.push(item.marker.color);\n        }\n      });\n      const plotUpdate = {\n        'marker.color': colors\n      };\n\n      Plotly.restyle(this.$refs.plot, plotUpdate, indices);\n    },\n    reset() {\n      this.isZoomed = false;\n\n      this.updatePlot();\n      this.$emit('subscribe');\n    },\n    updateData() {\n      this.updatePlot();\n    },\n    updateLocalControlPosition() {\n      const localControl = this.$refs.localControl;\n      localControl.style.display = 'none';\n\n      const plot = this.$refs.plot;\n      const bgLayer = this.$el.querySelector('.bglayer');\n\n      const plotBoundingRect = plot.getBoundingClientRect();\n      const bgLayerBoundingRect = bgLayer.getBoundingClientRect();\n\n      const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;\n      const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;\n\n      localControl.style.top = `${top}px`;\n      localControl.style.left = `${left}px`;\n      localControl.style.display = 'block';\n    },\n    updatePlot() {\n      if (!this.$refs || !this.$refs.plot || this.isZoomed) {\n        return;\n      }\n\n      Plotly.react(\n        this.$refs.plot,\n        Array.from(this.data.concat(this.getShapes(this.shapesData))),\n        this.getLayout()\n      );\n    },\n    zoom(eventData) {\n      const autorange = eventData['xaxis.autorange'];\n      const { autosize } = eventData;\n\n      if (autosize || autorange) {\n        return;\n      }\n\n      this.isZoomed = true;\n      this.$emit('unsubscribe');\n    },\n    getShapes() {\n      let markerData = {\n        x: [],\n        y: []\n      };\n      const shapes = this.shapesData.map((shapeData, index) => {\n        if (\n          !shapeData.x ||\n          !shapeData.y ||\n          !shapeData.x.length ||\n          !shapeData.y.length ||\n          shapeData.x.length !== shapeData.y.length\n        ) {\n          return '';\n        }\n\n        let text = [];\n        shapeData.x.forEach((point) => {\n          text.push(`${parseFloat(point).toPrecision(2)}`);\n        });\n\n        markerData.x = markerData.x.concat(shapeData.x);\n        markerData.y = markerData.y.concat(shapeData.y);\n\n        return {\n          x: shapeData.x,\n          y: shapeData.y,\n          mode: 'text',\n          text,\n          textfont: {\n            family: 'Helvetica Neue, Helvetica, Arial, sans-serif',\n            size: '12px',\n            color: PATH_COLORS[index]\n          },\n          opacity: 0.5\n        };\n      });\n\n      shapes.push({\n        x: markerData.x,\n        y: markerData.y,\n        mode: 'markers',\n        marker: {\n          size: 6,\n          color: MARKER_COLOR\n        }\n      });\n\n      return shapes;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/inspector/PlotOptions.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div>\n    <div v-if=\"canEdit\">\n      <PlotOptionsEdit />\n    </div>\n    <div v-else>\n      <PlotOptionsBrowse />\n    </div>\n  </div>\n</template>\n\n<script>\nimport PlotOptionsBrowse from './PlotOptionsBrowse.vue';\nimport PlotOptionsEdit from './PlotOptionsEdit.vue';\nexport default {\n  components: {\n    PlotOptionsBrowse,\n    PlotOptionsEdit\n  },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"js-plot-options-browse grid-properties\">\n    <ul class=\"l-inspector-part\">\n      <h2 title=\"Object view settings\">Settings</h2>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"X axis selection\">X Axis</div>\n        <div class=\"grid-cell value\">{{ xKeyLabel }}</div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Y axis selection\">Y Axis</div>\n        <div class=\"grid-cell value\">{{ yKeyLabel }}</div>\n      </li>\n      <ColorSwatch\n        :current-color=\"currentColor\"\n        edit-title=\"Manually set the color for this plot\"\n        view-title=\"The marker color for this plot\"\n        short-label=\"Color\"\n      />\n    </ul>\n  </div>\n</template>\n\n<script>\nimport Color from '../../../../ui/color/Color.js';\nimport ColorPalette from '../../../../ui/color/ColorPalette.js';\nimport ColorSwatch from '../../../../ui/color/ColorSwatch.vue';\n\nexport default {\n  components: { ColorSwatch },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      xKeyLabel: '',\n      yKeyLabel: '',\n      currentColor: undefined\n    };\n  },\n  mounted() {\n    this.plotSeries = [];\n    this.colorPalette = new ColorPalette();\n    this.initColor();\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.registerListeners();\n    this.composition.load();\n  },\n  beforeUnmount() {\n    this.stopListening();\n  },\n  methods: {\n    initColor() {\n      // this is called before the plot is initialized\n      if (\n        !this.domainObject.configuration.styles ||\n        !this.domainObject.configuration.styles.color\n      ) {\n        const color = this.colorPalette.getNextColor().asHexString();\n        this.domainObject.configuration.styles = {\n          color\n        };\n      }\n\n      this.currentColor = this.domainObject.configuration.styles.color;\n      const colorObject = Color.fromHexString(this.currentColor);\n\n      this.colorPalette.remove(colorObject);\n    },\n    registerListeners() {\n      this.composition.on('add', this.addSeries);\n      this.composition.on('remove', this.removeSeries);\n      this.unobserve = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.axes',\n        this.setAxesLabels\n      );\n    },\n    stopListening() {\n      this.composition.off('add', this.addSeries);\n      this.composition.off('remove', this.removeSeries);\n      if (this.unobserve) {\n        this.unobserve();\n      }\n    },\n    addSeries(series, index) {\n      this.plotSeries.push(series);\n      this.setAxesLabels();\n    },\n    removeSeries(seriesKey) {\n      const seriesIndex = this.plotSeries.findIndex((plotSeries) =>\n        this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier)\n      );\n\n      const foundSeries = seriesIndex > -1;\n      if (foundSeries) {\n        this.plotSeries.splice(seriesIndex, 1);\n        this.setAxesLabels();\n      }\n    },\n    setAxesLabels() {\n      let xKeyOptions = [];\n      let yKeyOptions = [];\n      if (this.plotSeries.length <= 0) {\n        return;\n      }\n\n      const series = this.plotSeries[0];\n      const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);\n\n      metadataValues.forEach((metadataValue) => {\n        xKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.source || metadataValue.key\n        });\n        yKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.source || metadataValue.key\n        });\n      });\n      let xKeyOptionIndex;\n      let yKeyOptionIndex;\n\n      if (this.domainObject.configuration.axes.xKey) {\n        xKeyOptionIndex = xKeyOptions.findIndex(\n          (option) => option.value === this.domainObject.configuration.axes.xKey\n        );\n        if (xKeyOptionIndex > -1) {\n          this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name;\n        }\n      }\n\n      if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) {\n        yKeyOptionIndex = yKeyOptions.findIndex(\n          (option) => option.value === this.domainObject.configuration.axes.yKey\n        );\n        if (yKeyOptionIndex > -1) {\n          this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name;\n        }\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"js-plot-options-edit grid-properties\">\n    <ul class=\"l-inspector-part\">\n      <h2 title=\"Object view settings\">Settings</h2>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"X axis selection.\">X Axis</div>\n        <div class=\"grid-cell value\">\n          <select v-model=\"xKey\" @change=\"updateForm('xKey')\">\n            <option\n              v-for=\"option in xKeyOptions\"\n              :key=\"`xKey-${option.value}`\"\n              :value=\"option.value\"\n              :selected=\"option.value == xKey\"\n            >\n              {{ option.name }}\n            </option>\n          </select>\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Y axis selection.\">Y Axis</div>\n        <div class=\"grid-cell value\">\n          <select v-model=\"yKey\" @change=\"updateForm('yKey')\">\n            <option\n              v-for=\"option in yKeyOptions\"\n              :key=\"`yKey-${option.value}`\"\n              :value=\"option.value\"\n              :selected=\"option.value == yKey\"\n            >\n              {{ option.name }}\n            </option>\n          </select>\n        </div>\n      </li>\n      <ColorSwatch\n        :current-color=\"currentColor\"\n        title=\"Manually set the line and marker color for this plot.\"\n        edit-title=\"Manually set the line and marker color for this plot.\"\n        view-title=\"The line and marker color for this plot.\"\n        short-label=\"Color\"\n        @color-set=\"setColor\"\n      />\n    </ul>\n  </div>\n</template>\n<script>\nimport Color from '../../../../ui/color/Color.js';\nimport ColorPalette from '../../../../ui/color/ColorPalette.js';\nimport ColorSwatch from '../../../../ui/color/ColorSwatch.vue';\n\nexport default {\n  components: { ColorSwatch },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      xKey: undefined,\n      yKey: undefined,\n      xKeyOptions: [],\n      yKeyOptions: [],\n      currentColor: undefined\n    };\n  },\n  mounted() {\n    this.plotSeries = [];\n    this.colorPalette = new ColorPalette();\n    this.initColor();\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.registerListeners();\n    this.composition.load();\n  },\n  beforeUnmount() {\n    this.stopListening();\n  },\n  methods: {\n    initColor() {\n      // this is called before the plot is initialized\n      if (\n        !this.domainObject.configuration.styles ||\n        !this.domainObject.configuration.styles.color\n      ) {\n        const color = this.colorPalette.getNextColor().asHexString();\n        this.domainObject.configuration.styles = {\n          color\n        };\n      }\n\n      this.currentColor = this.domainObject.configuration.styles.color;\n      const colorObject = Color.fromHexString(this.currentColor);\n\n      this.colorPalette.remove(colorObject);\n    },\n    setColor(chosenColor) {\n      this.currentColor = chosenColor.asHexString();\n      this.openmct.objects.mutate(\n        this.domainObject,\n        `configuration.styles.color`,\n        this.currentColor\n      );\n    },\n    registerListeners() {\n      this.composition.on('add', this.addSeries);\n      this.composition.on('remove', this.removeSeries);\n      this.unobserve = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.axes',\n        this.setupOptions\n      );\n    },\n    stopListening() {\n      this.composition.off('add', this.addSeries);\n      this.composition.off('remove', this.removeSeries);\n      if (this.unobserve) {\n        this.unobserve();\n      }\n    },\n    addSeries(series, index) {\n      this.plotSeries.push(series);\n      this.setupOptions();\n    },\n    removeSeries(seriesIdentifier) {\n      const index = this.plotSeries.findIndex((plotSeries) =>\n        this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier)\n      );\n      if (index >= 0) {\n        this.plotSeries.splice(index, 1);\n        this.setupOptions();\n      }\n    },\n    setupOptions() {\n      this.xKeyOptions = [];\n      this.yKeyOptions = [];\n      if (this.plotSeries.length <= 0) {\n        return;\n      }\n\n      let update = false;\n      const series = this.plotSeries[0];\n      const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);\n      metadataValues.forEach((metadataValue) => {\n        this.xKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.source || metadataValue.key\n        });\n        this.yKeyOptions.push({\n          name: metadataValue.name || metadataValue.key,\n          value: metadataValue.source || metadataValue.key\n        });\n      });\n\n      let xKeyOptionIndex;\n      let yKeyOptionIndex;\n\n      if (this.domainObject.configuration.axes.xKey) {\n        xKeyOptionIndex = this.xKeyOptions.findIndex(\n          (option) => option.value === this.domainObject.configuration.axes.xKey\n        );\n        if (xKeyOptionIndex > -1) {\n          this.xKey = this.xKeyOptions[xKeyOptionIndex].value;\n        } else {\n          this.xKey = undefined;\n        }\n      }\n\n      if (this.xKey === undefined) {\n        update = true;\n        xKeyOptionIndex = 0;\n        this.xKey = this.xKeyOptions[xKeyOptionIndex].value;\n      }\n\n      if (metadataValues.length > 1) {\n        if (this.domainObject.configuration.axes.yKey) {\n          yKeyOptionIndex = this.yKeyOptions.findIndex(\n            (option) => option.value === this.domainObject.configuration.axes.yKey\n          );\n          if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {\n            this.yKey = this.yKeyOptions[yKeyOptionIndex].value;\n          } else {\n            this.yKey = undefined;\n          }\n        }\n\n        if (this.yKey === undefined) {\n          update = true;\n          yKeyOptionIndex = this.yKeyOptions.findIndex(\n            (option, index) => index !== xKeyOptionIndex\n          );\n          this.yKey = this.yKeyOptions[yKeyOptionIndex].value;\n        }\n\n        this.yKeyOptions = this.yKeyOptions.map((option, index) => {\n          if (index === xKeyOptionIndex) {\n            option.name = `${option.name} (swap)`;\n            option.swap = yKeyOptionIndex;\n          } else {\n            option.name = option.name.replace(' (swap)', '');\n            option.swap = undefined;\n          }\n\n          return option;\n        });\n      }\n\n      this.xKeyOptions = this.xKeyOptions.map((option, index) => {\n        if (index === yKeyOptionIndex) {\n          option.name = `${option.name} (swap)`;\n          option.swap = xKeyOptionIndex;\n        } else {\n          option.name = option.name.replace(' (swap)', '');\n          option.swap = undefined;\n        }\n\n        return option;\n      });\n\n      if (update === true) {\n        this.saveConfiguration();\n      }\n    },\n    updateForm(property) {\n      if (property === 'xKey') {\n        const xKeyOption = this.xKeyOptions.find((option) => option.value === this.xKey);\n        if (xKeyOption.swap !== undefined) {\n          //swap\n          this.yKey = this.xKeyOptions[xKeyOption.swap].value;\n        }\n      } else if (property === 'yKey') {\n        const yKeyOption = this.yKeyOptions.find((option) => option.value === this.yKey);\n        if (yKeyOption.swap !== undefined) {\n          //swap\n          this.xKey = this.yKeyOptions[yKeyOption.swap].value;\n        }\n      }\n\n      this.saveConfiguration();\n    },\n    saveConfiguration() {\n      this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {\n        xKey: this.xKey,\n        yKey: this.yKey\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js",
    "content": "import mount from 'utils/mount';\n\nimport { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants.js';\nimport PlotOptions from './PlotOptions.vue';\n\nexport default function ScatterPlotInspectorViewProvider(openmct) {\n  return {\n    key: SCATTER_PLOT_INSPECTOR_KEY,\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      let object = selection[0][0].context.item;\n\n      return object && object.type === SCATTER_PLOT_KEY;\n    },\n    view: function (selection) {\n      let _destroy = null;\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlotOptions\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item\n              },\n              template: '<plot-options></plot-options>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/charts/scatter/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider.js';\nimport ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy.js';\nimport { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';\nimport ScatterPlotForm from './ScatterPlotForm.vue';\nimport ScatterPlotViewProvider from './ScatterPlotViewProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.forms.addNewFormControl(\n      'scatter-plot-form-control',\n      getScatterPlotFormControl(openmct)\n    );\n\n    openmct.types.addType(SCATTER_PLOT_KEY, {\n      key: SCATTER_PLOT_KEY,\n      name: 'Scatter Plot',\n      cssClass: 'icon-plot-scatter',\n      description: 'View data as a scatter plot.',\n      creatable: true,\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          styles: {},\n          axes: {},\n          ranges: {}\n        };\n      },\n      form: [\n        {\n          name: 'Underlay data (JSON file)',\n          key: 'selectFile',\n          control: 'file-input',\n          text: 'Select File...',\n          type: 'application/json',\n          removable: true,\n          hideFromInspector: true,\n          property: ['selectFile']\n        },\n        {\n          name: 'Underlay ranges',\n          control: 'scatter-plot-form-control',\n          cssClass: 'l-input',\n          key: 'scatterPlotForm',\n          required: false,\n          hideFromInspector: false,\n          property: ['configuration', 'ranges'],\n          validate: ({ value }, callback) => {\n            const { rangeMin, rangeMax, domainMin, domainMax } = value;\n            const valid = {\n              rangeMin,\n              rangeMax,\n              domainMin,\n              domainMax\n            };\n\n            if (callback) {\n              callback(valid);\n            }\n\n            const values = Object.values(valid);\n            const hasAllValues = values.every((rangeValue) => rangeValue !== undefined);\n            const hasNoValues = values.every((rangeValue) => rangeValue === undefined);\n\n            return hasAllValues || hasNoValues;\n          }\n        }\n      ],\n      priority: 891\n    });\n\n    openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));\n\n    openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));\n\n    openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);\n  };\n\n  function getScatterPlotFormControl(openmct) {\n    let destroyComponent;\n\n    return {\n      show(element, model, onChange) {\n        const { vNode, destroy } = mount(\n          {\n            el: element,\n            components: {\n              ScatterPlotForm\n            },\n            provide: {\n              openmct\n            },\n            data() {\n              return {\n                model,\n                onChange\n              };\n            },\n            template: `<scatter-plot-form :model=\"model\" @on-change=\"onChange\"></scatter-plot-form>`\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        destroyComponent = destroy;\n\n        return vNode;\n      },\n      destroy() {\n        destroyComponent();\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/charts/scatter/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport ScatterPlotPlugin from './plugin.js';\nimport { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW } from './scatterPlotConstants.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let mockObjectPath;\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      }\n    ];\n    const testTelemetry = [\n      {\n        utc: 1,\n        'some-key': 'some-value 1',\n        'some-other-key': 'some-other-value 1'\n      },\n      {\n        utc: 2,\n        'some-key': 'some-value 2',\n        'some-other-key': 'some-other-value 2'\n      },\n      {\n        utc: 3,\n        'some-key': 'some-value 3',\n        'some-other-key': 'some-other-value 3'\n      }\n    ];\n\n    openmct = createOpenMct();\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(testTelemetry);\n\n      return telemetryPromise;\n    });\n\n    openmct.install(new ScatterPlotPlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n    document.body.appendChild(element);\n\n    spyOn(window, 'ResizeObserver').and.returnValue({\n      observe() {},\n      unobserve() {},\n      disconnect() {}\n    });\n\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 4\n    });\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach((done) => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n    resetApplicationState(openmct).then(done).catch(done);\n  });\n\n  describe('The scatter plot view', () => {\n    let testDomainObject;\n    let scatterPlotObject;\n\n    let mockComposition;\n\n    beforeEach(async () => {\n      scatterPlotObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-plot'\n        },\n        type: 'telemetry.plot.scatter-plot',\n        name: 'Test Scatter Plot',\n        configuration: {\n          axes: {},\n          styles: {}\n        }\n      };\n\n      testDomainObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testDomainObject);\n\n        return [testDomainObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      await nextTick();\n    });\n\n    it('provides a scatter plot view', () => {\n      const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);\n      const plotViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW\n      );\n      expect(plotViewProvider).toBeDefined();\n    });\n\n    xit('Renders plotly scatter plot', () => {\n      let scatterPlotElement = element.querySelectorAll('.plotly');\n      expect(scatterPlotElement.length).toBe(1);\n    });\n  });\n\n  describe('the scatter plot objects', () => {\n    const mockObject = {\n      name: 'A very nice scatter plot',\n      key: SCATTER_PLOT_KEY,\n      creatable: true\n    };\n\n    it('defines a scatter plot object type with the correct key', () => {\n      const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;\n      expect(objectDef.key).toEqual(mockObject.key);\n    });\n\n    it('is creatable', () => {\n      const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;\n      expect(objectDef.creatable).toEqual(mockObject.creatable);\n    });\n  });\n\n  describe('The scatter plot composition policy', () => {\n    it('allows composition for telemetry that contain at least 2 ranges', () => {\n      const parent = {\n        composition: [],\n        configuration: {\n          axes: {},\n          styles: {}\n        },\n        name: 'Some Scatter Plot',\n        type: 'telemetry.plot.scatter-plot',\n        location: 'mine',\n        modified: 1631005183584,\n        persisted: 1631005183502,\n        identifier: {\n          namespace: '',\n          key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n        }\n      };\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key2',\n              name: 'Another attribute2',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(parent);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).not.toThrow();\n      expect(parent.composition.length).toBe(1);\n    });\n\n    it(\"disallows composition for telemetry that don't contain at least 2 range hints\", () => {\n      const parent = {\n        composition: [],\n        configuration: {\n          axes: {},\n          styles: {}\n        },\n        name: 'Some Scatter Plot',\n        type: 'telemetry.plot.scatter-plot',\n        location: 'mine',\n        modified: 1631005183584,\n        persisted: 1631005183502,\n        identifier: {\n          namespace: '',\n          key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n        }\n      };\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 1\n              }\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(parent);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).toThrow();\n      expect(parent.composition.length).toBe(0);\n    });\n  });\n  describe('the inspector view', () => {\n    let mockComposition;\n    let testDomainObject;\n    let selection;\n    let plotInspectorView;\n    let viewContainer;\n    let optionsElement;\n    beforeEach(async () => {\n      testDomainObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      selection = [\n        [\n          {\n            context: {\n              item: {\n                id: 'test-object',\n                identifier: {\n                  key: 'test-object',\n                  namespace: ''\n                },\n                type: 'telemetry.plot.scatter-plot',\n                configuration: {\n                  axes: {},\n                  styles: {}\n                },\n                composition: [\n                  {\n                    key: '~Some~foo.scatter'\n                  }\n                ]\n              }\n            }\n          }\n        ]\n      ];\n\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testDomainObject);\n\n        return [testDomainObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      viewContainer = document.createElement('div');\n      child.append(viewContainer);\n\n      const applicableViews = openmct.inspectorViews.get(selection);\n      plotInspectorView = applicableViews[0];\n      plotInspectorView.show(viewContainer);\n\n      await nextTick();\n      optionsElement = element.querySelector('.c-scatter-plot-options');\n    });\n\n    afterEach(() => {\n      plotInspectorView.destroy();\n    });\n\n    it('it renders the options', () => {\n      expect(optionsElement).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/charts/scatter/scatterPlotConstants.js",
    "content": "export const SCATTER_PLOT_VIEW = 'scatter-plot.view';\nexport const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';\nexport const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';\nexport const TIME_STRIP_KEY = 'time-strip';\n"
  },
  {
    "path": "src/plugins/clearData/ClearDataAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction inSelectionPath(openmct, domainObject) {\n  const domainObjectIdentifier = domainObject.identifier;\n\n  return openmct.selection.get().some((selectionPath) => {\n    return selectionPath.some((objectInPath) => {\n      const objectInPathIdentifier = objectInPath.context.item.identifier;\n\n      return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier);\n    });\n  });\n}\n\nconst CLEAR_DATA_ACTION_KEY = 'clear-data-action';\nclass ClearDataAction {\n  constructor(openmct, appliesToObjects) {\n    this.name = 'Clear Data for Object';\n    this.key = CLEAR_DATA_ACTION_KEY;\n    this.description = 'Clears current data for object, unsubscribes and resubscribes to data';\n    this.cssClass = 'icon-clear-data';\n\n    this._openmct = openmct;\n    this._appliesToObjects = appliesToObjects;\n  }\n\n  invoke(objectPath) {\n    let domainObject = null;\n    if (objectPath) {\n      domainObject = objectPath[0];\n    }\n\n    this._openmct.objectViews.emit('clearData', domainObject);\n  }\n  appliesTo(objectPath) {\n    if (!objectPath) {\n      return false;\n    }\n\n    const contextualDomainObject = objectPath[0];\n    // first check to see if this action applies to this sort of object at all\n    const appliesToThisObject = this._appliesToObjects.some((type) => {\n      return contextualDomainObject.type === type;\n    });\n    if (!appliesToThisObject) {\n      // we've selected something not applicable\n      return false;\n    }\n\n    const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject);\n    if (objectInSelectionPath) {\n      return true;\n    } else {\n      // if this it doesn't match up, check to see if we're in a composition (i.e., layout)\n      const routerPath = this._openmct.router.path[0];\n\n      return routerPath.type === 'layout';\n    }\n  }\n}\n\nexport { CLEAR_DATA_ACTION_KEY };\n\nexport default ClearDataAction;\n"
  },
  {
    "path": "src/plugins/clearData/components/GlobalClearIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    aria-label=\"Global Clear Indicator\"\n    class=\"c-indicator c-indicator--clickable icon-clear-data s-status-caution\"\n  >\n    <span class=\"label c-indicator__label\">\n      <button @click=\"globalClearEmit\">Clear Data</button>\n    </span>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  methods: {\n    globalClearEmit() {\n      this.openmct.objectViews.emit('clearData');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/clearData/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ClearDataAction from './ClearDataAction.js';\nimport GlobalClearIndicator from './components/GlobalClearIndicator.vue';\n\nexport default function plugin(appliesToObjects, options = { indicator: true }) {\n  let installIndicator = options.indicator;\n\n  appliesToObjects = appliesToObjects || [];\n\n  return function install(openmct) {\n    if (installIndicator) {\n      let indicator = {\n        vueComponent: GlobalClearIndicator,\n        key: 'global-clear-indicator',\n        priority: openmct.priority.DEFAULT\n      };\n\n      openmct.indicators.add(indicator);\n    }\n\n    openmct.actions.register(new ClearDataAction(openmct, appliesToObjects));\n  };\n}\n"
  },
  {
    "path": "src/plugins/clearData/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport ClearDataPlugin from './plugin.js';\n\ndescribe('The Clear Data Plugin:', () => {\n  let clearDataPlugin;\n\n  describe('The clear data action:', () => {\n    let openmct;\n    let selection;\n    let mockObjectPath;\n    let clearDataAction;\n    let testViewObject;\n    beforeEach((done) => {\n      openmct = createOpenMct();\n\n      clearDataPlugin = new ClearDataPlugin(\n        ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],\n        { indicator: true }\n      );\n      openmct.install(clearDataPlugin);\n\n      clearDataAction = openmct.actions.getAction('clear-data-action');\n      testViewObject = [\n        {\n          identifier: {\n            key: 'foo-table',\n            namespace: ''\n          },\n          type: 'table'\n        }\n      ];\n      openmct.router.path = testViewObject;\n      mockObjectPath = [\n        {\n          name: 'Mock Table',\n          type: 'table',\n          identifier: {\n            key: 'foo-table',\n            namespace: ''\n          }\n        }\n      ];\n      selection = [\n        {\n          context: {\n            item: mockObjectPath[0]\n          }\n        }\n      ];\n\n      openmct.selection.select(selection);\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      openmct.router.path = null;\n\n      return resetApplicationState(openmct);\n    });\n    it('is installed', () => {\n      expect(clearDataAction).toBeDefined();\n    });\n\n    it('is applicable on applicable objects', () => {\n      const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);\n      expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined();\n    });\n\n    it('is not applicable on inapplicable objects', () => {\n      testViewObject = [\n        {\n          identifier: {\n            key: 'foo-widget',\n            namespace: ''\n          },\n          type: 'widget'\n        }\n      ];\n      mockObjectPath = [\n        {\n          name: 'Mock Widget',\n          type: 'widget',\n          identifier: {\n            key: 'foo-widget',\n            namespace: ''\n          }\n        }\n      ];\n      selection = [\n        {\n          context: {\n            item: mockObjectPath[0]\n          }\n        }\n      ];\n      openmct.selection.select(selection);\n      const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);\n      expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined();\n    });\n\n    it('is not applicable if object not in the selection path and not a layout', () => {\n      selection = [\n        {\n          context: {\n            item: {\n              name: 'Some Random Widget',\n              type: 'not-in-path-widget',\n              identifier: {\n                key: 'something-else-widget',\n                namespace: ''\n              }\n            }\n          }\n        }\n      ];\n      openmct.selection.select(selection);\n      const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);\n      expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined();\n    });\n\n    it('is applicable if object not in the selection path and is a layout', () => {\n      selection = [\n        {\n          context: {\n            item: {\n              name: 'Some Random Widget',\n              type: 'not-in-path-widget',\n              identifier: {\n                key: 'something-else-widget',\n                namespace: ''\n              }\n            }\n          }\n        }\n      ];\n\n      openmct.selection.select(selection);\n\n      testViewObject = [\n        {\n          identifier: {\n            key: 'foo-layout',\n            namespace: ''\n          },\n          type: 'layout'\n        }\n      ];\n      openmct.router.path = testViewObject;\n      const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);\n      expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined();\n    });\n\n    it('fires an event upon invocation', (done) => {\n      openmct.objectViews.on('clearData', (domainObject) => {\n        expect(domainObject).toEqual(testViewObject[0]);\n        done();\n      });\n      clearDataAction.invoke(testViewObject);\n    });\n  });\n\n  describe('The clear data indicator:', () => {\n    let openmct;\n    let appHolder;\n\n    beforeEach((done) => {\n      openmct = createOpenMct();\n\n      clearDataPlugin = new ClearDataPlugin(\n        ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],\n        {\n          indicator: true\n        }\n      );\n      openmct.install(clearDataPlugin);\n      appHolder = document.createElement('div');\n      document.body.appendChild(appHolder);\n      openmct.on('start', done);\n      openmct.start(appHolder);\n    });\n\n    it('installs', () => {\n      const globalClearIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'global-clear-indicator'\n      ).vueComponent;\n      expect(globalClearIndicator).toBeDefined();\n    });\n\n    it('renders its major elements', () => {\n      const indicatorClass = appHolder.querySelector('.c-indicator');\n      const iconClass = appHolder.querySelector('.icon-clear-data');\n      const indicatorLabel = appHolder.querySelector('.c-indicator__label');\n      const buttonElement = indicatorLabel.querySelector('button');\n      const hasMajorElements = Boolean(indicatorClass && iconClass && buttonElement);\n\n      expect(hasMajorElements).toBe(true);\n      expect(buttonElement.innerText).toEqual('Clear Data');\n    });\n\n    it('clicking the button fires the global clear', (done) => {\n      const indicatorLabel = appHolder.querySelector('.c-indicator__label');\n      const buttonElement = indicatorLabel.querySelector('button');\n      const clickEvent = createMouseEvent('click');\n      openmct.objectViews.on('clearData', done);\n      buttonElement.dispatchEvent(clickEvent);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/clock/ClockViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Clock from './components/ClockComponent.vue';\n\nexport default function ClockViewProvider(openmct) {\n  return {\n    key: 'clock.view',\n    name: 'Clock',\n    cssClass: 'icon-clock',\n    canView(domainObject) {\n      return domainObject.type === 'clock';\n    },\n\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Clock\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: '<clock />'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/clock/components/ClockComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"u-contents\">\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      aria-atomic=\"true\"\n      aria-label=\"Clock\"\n      class=\"c-clock l-time-display u-style-receiver js-style-receiver\"\n    >\n      <div class=\"c-clock__timezone\">\n        {{ timeZoneAbbr }}\n      </div>\n      <div class=\"c-clock__value\">\n        {{ timeTextValue }}\n      </div>\n      <div class=\"c-clock__ampm\">\n        {{ timeAmPm }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport moment from 'moment';\nimport momentTimezone from 'moment-timezone';\nimport raf from 'utils/raf';\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      configuration: this.domainObject.configuration,\n      lastTimestamp: this.openmct.time.now()\n    };\n  },\n  computed: {\n    baseFormat() {\n      return this.configuration.baseFormat;\n    },\n    use24() {\n      return this.configuration.use24 === 'clock24';\n    },\n    timezone() {\n      return this.configuration.timezone;\n    },\n    timeFormat() {\n      return this.use24 ? this.baseFormat.replace('hh', 'HH') : this.baseFormat;\n    },\n    zoneName() {\n      return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : 'UTC';\n    },\n    momentTime() {\n      return this.zoneName\n        ? moment.utc(this.lastTimestamp).tz(this.zoneName)\n        : moment.utc(this.lastTimestamp);\n    },\n    timeZoneAbbr() {\n      return this.momentTime.zoneAbbr();\n    },\n    timeTextValue() {\n      return this.timeFormat && this.momentTime.format(this.timeFormat);\n    },\n    timeAmPm() {\n      return this.use24 ? '' : this.momentTime.format('A');\n    }\n  },\n  mounted() {\n    this.unobserve = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration',\n      (configuration) => {\n        this.configuration = configuration;\n      }\n    );\n    this.tick = raf(this.tick);\n    this.openmct.time.on('tick', this.tick);\n  },\n  beforeUnmount() {\n    if (this.unobserve) {\n      this.unobserve();\n    }\n\n    this.openmct.time.off('tick', this.tick);\n  },\n  methods: {\n    tick(timestamp) {\n      this.lastTimestamp = timestamp;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/clock/components/ClockIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    aria-label=\"Clock Indicator\"\n    class=\"c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable\"\n    role=\"complementary\"\n    aria-live=\"off\"\n  >\n    <span class=\"label c-indicator__label\">\n      {{ timeTextValue }}\n    </span>\n  </div>\n</template>\n\n<script>\nimport moment from 'moment';\nimport raf from 'utils/raf';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    indicatorFormat: {\n      type: String,\n      default: 'YYYY/MM/DD HH:mm:ss'\n    }\n  },\n  data() {\n    return {\n      timestamp: this.openmct.time.getClock() ? this.openmct.time.now() : undefined\n    };\n  },\n  computed: {\n    timeTextValue() {\n      return `${moment.utc(this.timestamp).format(this.indicatorFormat)} ${\n        this.openmct.time.getTimeSystem().name\n      }`;\n    }\n  },\n  mounted() {\n    this.tick = raf(this.tick);\n    this.openmct.time.on('tick', this.tick);\n    this.tick(this.timestamp);\n  },\n  beforeUnmount() {\n    this.openmct.time.off('tick', this.tick);\n  },\n  methods: {\n    tick(timestamp) {\n      this.timestamp = timestamp;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/clock/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport momentTimezone from 'moment-timezone';\n\nimport ClockViewProvider from './ClockViewProvider.js';\nimport ClockIndicator from './components/ClockIndicator.vue';\n\nexport default function ClockPlugin(options) {\n  return function install(openmct) {\n    openmct.types.addType('clock', {\n      name: 'Clock',\n      description:\n        'A digital clock that uses system time and supports a variety of display formats and timezones.',\n      creatable: true,\n      cssClass: 'icon-clock',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          baseFormat: 'YYYY/MM/DD hh:mm:ss',\n          use24: 'clock12',\n          timezone: 'UTC'\n        };\n      },\n      form: [\n        {\n          key: 'displayFormat',\n          name: 'Display Format',\n          control: 'select',\n          options: [\n            {\n              value: 'YYYY/MM/DD hh:mm:ss',\n              name: 'YYYY/MM/DD hh:mm:ss'\n            },\n            {\n              value: 'YYYY/DDD hh:mm:ss',\n              name: 'YYYY/DDD hh:mm:ss'\n            },\n            {\n              value: 'hh:mm:ss',\n              name: 'hh:mm:ss'\n            }\n          ],\n          cssClass: 'l-inline',\n          property: ['configuration', 'baseFormat']\n        },\n        {\n          ariaLabel: '12 or 24 hour clock',\n          control: 'select',\n          options: [\n            {\n              value: 'clock12',\n              name: '12hr'\n            },\n            {\n              value: 'clock24',\n              name: '24hr'\n            }\n          ],\n          cssClass: 'l-inline',\n          property: ['configuration', 'use24']\n        },\n        {\n          key: 'timezone',\n          name: 'Timezone',\n          control: 'autocomplete',\n          cssClass: 'c-clock__timezone-selection c-menu--no-icon',\n          options: momentTimezone.tz.names(),\n          property: ['configuration', 'timezone']\n        }\n      ]\n    });\n    openmct.objectViews.addProvider(new ClockViewProvider(openmct));\n\n    if (options?.enableClockIndicator === true) {\n      const indicator = {\n        vueComponent: ClockIndicator,\n        key: 'clock-indicator',\n        priority: openmct.priority.LOW\n      };\n      openmct.indicators.add(indicator);\n    }\n\n    openmct.objects.addGetInterceptor({\n      appliesTo: (identifier, domainObject) => {\n        return domainObject && domainObject.type === 'clock';\n      },\n      invoke: (identifier, domainObject) => {\n        if (domainObject.configuration) {\n          return domainObject;\n        }\n\n        if (domainObject.clockFormat && domainObject.timezone) {\n          const baseFormat = domainObject.clockFormat[0];\n          const use24 = domainObject.clockFormat[1];\n          const timezone = domainObject.timezone;\n\n          domainObject.configuration = {\n            baseFormat,\n            use24,\n            timezone\n          };\n\n          openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);\n        }\n\n        return domainObject;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/clock/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport clockPlugin from './plugin.js';\n\ndescribe('Clock plugin:', () => {\n  let openmct;\n  let clockDefinition;\n  let element;\n  let child;\n  let appHolder;\n\n  let clockDomainObject;\n\n  function setupClock(enableClockIndicator) {\n    return new Promise((resolve, reject) => {\n      clockDomainObject = {\n        identifier: {\n          key: 'clock',\n          namespace: 'test-namespace'\n        },\n        type: 'clock'\n      };\n\n      appHolder = document.createElement('div');\n      appHolder.style.width = '640px';\n      appHolder.style.height = '480px';\n      document.body.appendChild(appHolder);\n\n      openmct = createOpenMct();\n\n      element = document.createElement('div');\n      child = document.createElement('div');\n      element.appendChild(child);\n\n      openmct.install(clockPlugin({ enableClockIndicator }));\n\n      clockDefinition = openmct.types.get('clock').definition;\n      clockDefinition.initialize(clockDomainObject);\n\n      openmct.on('start', resolve);\n      openmct.start(appHolder);\n    });\n  }\n\n  describe('Clock view:', () => {\n    let clockViewProvider;\n    let clockView;\n    let clockViewObject;\n    let mutableClockObject;\n    let mockComposition;\n\n    beforeEach(async () => {\n      await setupClock(true);\n\n      clockViewObject = {\n        ...clockDomainObject,\n        id: 'test-object',\n        name: 'Clock',\n        configuration: {\n          baseFormat: 'YYYY/MM/DD hh:mm:ss',\n          use24: 'clock12',\n          timezone: 'UTC'\n        }\n      };\n\n      mockComposition = new EventEmitter();\n      // eslint-disable-next-line require-await\n      mockComposition.load = async () => {\n        return [];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n      spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));\n      spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));\n      spyOn(openmct.objects, 'supportsMutation').and.returnValue(true);\n\n      const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);\n      clockViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'clock.view');\n\n      mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier);\n\n      clockView = clockViewProvider.view(mutableClockObject);\n      clockView.show(child);\n\n      await nextTick();\n      await new Promise((resolve) => requestAnimationFrame(resolve));\n    });\n\n    afterEach(() => {\n      clockView.destroy();\n      openmct.objects.destroyMutable(mutableClockObject);\n      if (appHolder) {\n        appHolder.remove();\n      }\n\n      return resetApplicationState(openmct);\n    });\n\n    it('has name as Clock', () => {\n      expect(clockDefinition.name).toEqual('Clock');\n    });\n\n    it('is creatable', () => {\n      expect(clockDefinition.creatable).toEqual(true);\n    });\n\n    it('provides clock view', () => {\n      expect(clockViewProvider).toBeDefined();\n    });\n\n    it('renders clock element', () => {\n      const clockElement = element.querySelectorAll('.c-clock');\n      expect(clockElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const clockElement = element.querySelector('.c-clock');\n      const timezone = clockElement.querySelector('.c-clock__timezone');\n      const time = clockElement.querySelector('.c-clock__value');\n      const amPm = clockElement.querySelector('.c-clock__ampm');\n      const hasMajorElements = Boolean(timezone && time && amPm);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders time in UTC', () => {\n      const clockElement = element.querySelector('.c-clock');\n      const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim();\n\n      expect(timezone).toBe('UTC');\n    });\n\n    it('updates the 24 hour option in the configuration', (done) => {\n      expect(clockDomainObject.configuration.use24).toBe('clock12');\n      const new24Option = 'clock24';\n\n      openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {\n        expect(changedDomainObject.use24).toBe(new24Option);\n        done();\n      });\n\n      openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option);\n    });\n\n    it('updates the timezone option in the configuration', (done) => {\n      expect(clockDomainObject.configuration.timezone).toBe('UTC');\n      const newZone = 'CST6CDT';\n\n      openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {\n        expect(changedDomainObject.timezone).toBe(newZone);\n        done();\n      });\n\n      openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone);\n    });\n\n    it('updates the time format option in the configuration', (done) => {\n      expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss');\n      const newFormat = 'hh:mm:ss';\n\n      openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {\n        expect(changedDomainObject.baseFormat).toBe(newFormat);\n        done();\n      });\n\n      openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat);\n    });\n  });\n\n  describe('Clock Indicator view:', () => {\n    let clockIndicator;\n\n    afterEach(() => {\n      clockIndicator = undefined;\n      if (appHolder) {\n        appHolder.remove();\n      }\n\n      return resetApplicationState(openmct);\n    });\n\n    it(\"doesn't exist\", async () => {\n      await setupClock(false);\n\n      clockIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'clock-indicator'\n      );\n\n      const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined;\n      expect(clockIndicatorMissing).toBe(true);\n    });\n\n    it('exists', async () => {\n      await setupClock(true);\n\n      clockIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'clock-indicator'\n      ).vueComponent;\n\n      const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;\n      expect(hasClockIndicator).toBe(true);\n    });\n\n    it('contains text', async () => {\n      await setupClock(true);\n      await new Promise((resolve) => requestAnimationFrame(resolve));\n\n      clockIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'clock-indicator'\n      ).vueComponent;\n      const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;\n      expect(hasClockIndicator).toBe(true);\n      const clockIndicatorText = appHolder\n        .querySelector('.t-indicator-clock .c-indicator__label')\n        .textContent.trim();\n      const textIncludesUTC = clockIndicatorText.includes('UTC');\n\n      expect(textIncludesUTC).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/comps/CompsCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function CompsCompositionPolicy(openmct) {\n  return {\n    allow: function (parent, child) {\n      if (parent.type === 'comps' && !openmct.telemetry.isTelemetryObject(child)) {\n        return false;\n      }\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport CompsInspectorView from './components/CompsInspectorView.vue';\n\nexport default class CompsInspectorViewProvider {\n  constructor(openmct, compsManagerPool) {\n    this.openmct = openmct;\n    this.name = 'Config';\n    this.key = 'comps-configuration';\n    this.compsManagerPool = compsManagerPool;\n  }\n\n  canView(selection) {\n    if (selection.length !== 1 || selection[0].length === 0) {\n      return false;\n    }\n\n    let object = selection[0][0].context.item;\n    return object && object.type === 'comps';\n  }\n\n  view(selection) {\n    let _destroy = null;\n    const domainObject = selection[0][0].context.item;\n    const openmct = this.openmct;\n    const compsManagerPool = this.compsManagerPool;\n\n    return {\n      show: function (element) {\n        const { destroy } = mount(\n          {\n            el: element,\n            components: {\n              CompsInspectorView: CompsInspectorView\n            },\n            provide: {\n              openmct,\n              domainObject,\n              compsManagerPool\n            },\n            template: '<comps-inspector-view></comps-inspector-view>'\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        _destroy = destroy;\n      },\n      showTab: function (isEditing) {\n        return isEditing;\n      },\n      priority: function () {\n        return 1;\n      },\n      destroy: function () {\n        if (_destroy) {\n          _destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsManager.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nexport default class CompsManager extends EventEmitter {\n  #openmct;\n  #domainObject;\n  #composition;\n  #telemetryObjects = {};\n  #telemetryCollections = {};\n  #telemetryLoadedPromises = [];\n  #telemetryOptions = {};\n  #loaded = false;\n  #compositionLoaded = false;\n  #telemetryProcessors = {};\n  #loadVersion = 0;\n  #currentLoadPromise = null;\n\n  constructor(openmct, domainObject) {\n    super();\n    this.#openmct = openmct;\n    this.#domainObject = domainObject;\n    this.clearData = this.clearData.bind(this);\n  }\n\n  #getNextAlphabeticalParameterName() {\n    const parameters = this.#domainObject.configuration.comps.parameters;\n    const existingNames = new Set(parameters.map((p) => p.name));\n    const alphabet = 'abcdefghijklmnopqrstuvwxyz';\n    let suffix = '';\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      for (let letter of alphabet) {\n        const proposedName = letter + suffix;\n        if (!existingNames.has(proposedName)) {\n          return proposedName;\n        }\n      }\n      // Increment suffix after exhausting the alphabet\n      suffix = (parseInt(suffix, 10) || 0) + 1;\n    }\n  }\n\n  addParameter(telemetryObject) {\n    const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);\n    const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);\n    const timeSystem = this.#openmct.time.getTimeSystem();\n    const domains = metaData?.valuesForHints(['domain']);\n    const timeMetaData = domains.find((d) => d.key === timeSystem.key);\n    // in the valuesMetadata, find the first numeric data type\n    const rangeItems = metaData.valueMetadatas.filter(\n      (metaDatum) => metaDatum.hints && metaDatum.hints.range\n    );\n    rangeItems.sort((a, b) => a.hints.range - b.hints.range);\n    let valueToUse = rangeItems[0]?.key;\n    if (!valueToUse) {\n      // if no numeric data type, just use the first one\n      valueToUse = metaData.valueMetadatas[0]?.key;\n    }\n    this.#domainObject.configuration.comps.parameters.push({\n      keyString,\n      name: `${this.#getNextAlphabeticalParameterName()}`,\n      valueToUse,\n      testValue: 0,\n      timeMetaData,\n      accumulateValues: false,\n      sampleSize: 10\n    });\n    this.emit('parameterAdded', this.#domainObject);\n  }\n\n  getParameters() {\n    const parameters = this.#domainObject.configuration.comps.parameters;\n    const parametersWithTimeKey = parameters.map((parameter) => {\n      return {\n        ...parameter,\n        timeKey: this.#telemetryCollections[parameter.keyString]?.timeKey\n      };\n    });\n    return parametersWithTimeKey;\n  }\n\n  getTelemetryObjectForParameter(keyString) {\n    return this.#telemetryObjects[keyString];\n  }\n\n  getMetaDataValuesForParameter(keyString) {\n    const telemetryObject = this.getTelemetryObjectForParameter(keyString);\n    if (!telemetryObject) {\n      return [];\n    }\n    const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);\n    return metaData.valueMetadatas;\n  }\n\n  deleteParameter(keyString) {\n    this.#domainObject.configuration.comps.parameters =\n      this.#domainObject.configuration.comps.parameters.filter(\n        (parameter) => parameter.keyString !== keyString\n      );\n    // if there are no parameters referencing this parameter keyString, remove the telemetry object too\n    const parameterExists = this.#domainObject.configuration.comps.parameters.some(\n      (parameter) => parameter.keyString === keyString\n    );\n    if (!parameterExists) {\n      this.emit('parameterRemoved', this.#domainObject);\n    }\n  }\n\n  setDomainObject(passedDomainObject) {\n    this.#domainObject = passedDomainObject;\n  }\n\n  isReady() {\n    return this.#loaded;\n  }\n\n  async load(telemetryOptions) {\n    // Increment the load version to mark a new load operation\n    const loadVersion = ++this.#loadVersion;\n\n    if (!_.isEqual(this.#telemetryOptions, telemetryOptions)) {\n      this.#destroy();\n    }\n\n    this.#telemetryOptions = telemetryOptions;\n\n    // Start the load process and store the promise\n    this.#currentLoadPromise = (async () => {\n      // Load composition if not already loaded\n      if (!this.#compositionLoaded) {\n        await this.#loadComposition();\n        // Check if a newer load has been initiated\n        if (loadVersion !== this.#loadVersion) {\n          await this.#currentLoadPromise;\n          return;\n        }\n        this.#compositionLoaded = true;\n      }\n\n      // Start listening to telemetry if not already done\n      if (!this.#loaded) {\n        await this.#startListeningToUnderlyingTelemetry();\n        // Check again for newer load\n        if (loadVersion !== this.#loadVersion) {\n          await this.#currentLoadPromise;\n          return;\n        }\n        this.#loaded = true;\n      }\n    })();\n\n    // Await the load process\n    await this.#currentLoadPromise;\n  }\n\n  async #startListeningToUnderlyingTelemetry() {\n    let outstandingRequests = 0;\n\n    const allRequestsComplete = new Promise((resolve) => {\n      Object.keys(this.#telemetryCollections).forEach((keyString) => {\n        if (!this.#telemetryCollections[keyString].loaded) {\n          const telemetryCollection = this.#telemetryCollections[keyString];\n\n          telemetryCollection.on('add', this.#getTelemetryProcessor(keyString));\n          telemetryCollection.on('clear', this.clearData);\n\n          telemetryCollection.on('requestStarted', () => {\n            outstandingRequests++;\n          });\n\n          telemetryCollection.on('requestEnded', () => {\n            outstandingRequests--;\n            if (outstandingRequests === 0) {\n              resolve();\n            }\n          });\n\n          // Trigger the load process\n          telemetryCollection.load();\n        }\n      });\n\n      // If no requests were started, resolve immediately\n      if (outstandingRequests === 0) {\n        resolve();\n      }\n    });\n\n    // Wait for all requests to complete\n    await allRequestsComplete;\n  }\n\n  #destroy() {\n    this.stopListeningToUnderlyingTelemetry();\n    this.#composition = null;\n    this.#telemetryCollections = {};\n    this.#compositionLoaded = false;\n    this.#loaded = false;\n    this.#telemetryObjects = {};\n  }\n\n  stopListeningToUnderlyingTelemetry() {\n    this.#loaded = false;\n    Object.keys(this.#telemetryCollections).forEach((keyString) => {\n      const specificTelemetryProcessor = this.#telemetryProcessors[keyString];\n      delete this.#telemetryProcessors[keyString];\n      this.#telemetryCollections[keyString].off('add', specificTelemetryProcessor);\n      this.#telemetryCollections[keyString].off('clear', this.clearData);\n      this.#telemetryCollections[keyString].destroy();\n    });\n  }\n\n  getTelemetryObjects() {\n    return this.#telemetryObjects;\n  }\n\n  async #loadComposition() {\n    this.#composition = this.#openmct.composition.get(this.#domainObject);\n    if (this.#composition) {\n      this.#composition.on('add', this.#addTelemetryObject);\n      this.#composition.on('remove', this.#removeTelemetryObject);\n      await this.#composition.load();\n    }\n  }\n\n  #getParameterForKeyString(keyString) {\n    return this.#domainObject.configuration.comps.parameters.find(\n      (parameter) => parameter.keyString === keyString\n    );\n  }\n\n  #getImputedDataUsingLOCF(datum, telemetryCollection) {\n    const telemetryCollectionData = telemetryCollection.getAll();\n    let insertionPointForNewData = telemetryCollection._sortedIndex(datum);\n    if (insertionPointForNewData && insertionPointForNewData >= telemetryCollectionData.length) {\n      insertionPointForNewData = telemetryCollectionData.length - 1;\n    }\n    // get the closest datum to the new datum\n    const closestDatum = telemetryCollectionData[insertionPointForNewData];\n    // clone the closest datum and replace the time key with the new time\n    const imputedData = {\n      ...closestDatum,\n      [telemetryCollection.timeKey]: datum[telemetryCollection.timeKey]\n    };\n    return imputedData;\n  }\n\n  getDataFrameForRequest() {\n    // Step 1: Collect all unique timestamps from all telemetry collections\n    const allTimestampsSet = new Set();\n\n    Object.values(this.#telemetryCollections).forEach((collection) => {\n      const timeSystemKey = collection.timeKey;\n      collection.getAll().forEach((dataPoint) => {\n        allTimestampsSet.add(dataPoint[timeSystemKey]);\n      });\n    });\n\n    // Convert the set to a sorted array\n    const allTimestamps = Array.from(allTimestampsSet).sort((a, b) => a - b);\n\n    // Step 2: Initialize the result object\n    const telemetryForComps = {};\n\n    // Step 3: Iterate through each telemetry collection to align data\n    Object.keys(this.#telemetryCollections).forEach((keyString) => {\n      const telemetryCollection = this.#telemetryCollections[keyString];\n      const alignedValues = [];\n\n      // Iterate through each common timestamp\n      allTimestamps.forEach((timestamp) => {\n        const timeKey = telemetryCollection.timeKey;\n        const fakeData = { [timeKey]: timestamp };\n        const imputedDatum = this.#getImputedDataUsingLOCF(fakeData, telemetryCollection);\n        if (imputedDatum) {\n          alignedValues.push(imputedDatum);\n        }\n      });\n\n      telemetryForComps[keyString] = alignedValues;\n    });\n\n    return telemetryForComps;\n  }\n\n  getDataFrameForSubscription(newTelemetry) {\n    const telemetryForComps = {};\n    const newTelemetryKey = Object.keys(newTelemetry)[0];\n    const newTelemetryParameter = this.#getParameterForKeyString(newTelemetryKey);\n    const newTelemetryData = newTelemetry[newTelemetryKey];\n    const otherTelemetryKeys = Object.keys(this.#telemetryCollections).slice(0);\n    if (newTelemetryParameter.accumulateValues) {\n      telemetryForComps[newTelemetryKey] = this.#telemetryCollections[newTelemetryKey].getAll();\n    } else {\n      telemetryForComps[newTelemetryKey] = newTelemetryData;\n    }\n    otherTelemetryKeys.forEach((keyString) => {\n      telemetryForComps[keyString] = [];\n    });\n\n    const otherTelemetryKeysNotAccumulating = otherTelemetryKeys.filter(\n      (keyString) => !this.#getParameterForKeyString(keyString).accumulateValues\n    );\n    const otherTelemetryKeysAccumulating = otherTelemetryKeys.filter(\n      (keyString) => this.#getParameterForKeyString(keyString).accumulateValues\n    );\n\n    // if we're accumulating, just add all the data\n    otherTelemetryKeysAccumulating.forEach((keyString) => {\n      telemetryForComps[keyString] = this.#telemetryCollections[keyString].getAll();\n    });\n\n    // for the others, march through the new telemetry data and add data to the frame from the other telemetry objects\n    // using LOCF\n    newTelemetryData.forEach((newDatum) => {\n      otherTelemetryKeysNotAccumulating.forEach((otherKeyString) => {\n        const otherCollection = this.#telemetryCollections[otherKeyString];\n        const imputedDatum = this.#getImputedDataUsingLOCF(newDatum, otherCollection);\n        if (imputedDatum) {\n          telemetryForComps[otherKeyString].push(imputedDatum);\n        }\n      });\n    });\n    return telemetryForComps;\n  }\n\n  #removeTelemetryObject = (telemetryObjectIdentifier) => {\n    const keyString = this.#openmct.objects.makeKeyString(telemetryObjectIdentifier);\n    delete this.#telemetryObjects[keyString];\n    this.#telemetryCollections[keyString]?.destroy();\n    delete this.#telemetryCollections[keyString];\n    // remove all parameters that reference this telemetry object\n    this.deleteParameter(keyString);\n  };\n\n  #requestUnderlyingTelemetry() {\n    const underlyingTelemetry = {};\n    Object.keys(this.#telemetryCollections).forEach((collectionKey) => {\n      const collection = this.#telemetryCollections[collectionKey];\n      underlyingTelemetry[collectionKey] = collection.getAll();\n    });\n    return underlyingTelemetry;\n  }\n\n  #getTelemetryProcessor(keyString) {\n    if (this.#telemetryProcessors[keyString]) {\n      return this.#telemetryProcessors[keyString];\n    }\n\n    const telemetryProcessor = (newTelemetry) => {\n      this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });\n    };\n    this.#telemetryProcessors[keyString] = telemetryProcessor;\n    return telemetryProcessor;\n  }\n\n  #telemetryProcessor = (newTelemetry, keyString) => {\n    this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });\n  };\n\n  clearData(telemetryLoadedPromise) {\n    this.#loaded = false;\n  }\n\n  setOutputFormat(outputFormat) {\n    this.#domainObject.configuration.comps.outputFormat = outputFormat;\n    this.emit('outputFormatChanged', outputFormat);\n  }\n\n  getOutputFormat() {\n    return this.#domainObject.configuration.comps.outputFormat;\n  }\n\n  getExpression() {\n    return this.#domainObject.configuration.comps.expression;\n  }\n\n  #addTelemetryObject = (telemetryObject) => {\n    const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);\n    this.#telemetryObjects[keyString] = telemetryObject;\n    this.#telemetryCollections[keyString] = this.#openmct.telemetry.requestCollection(\n      telemetryObject,\n      this.#telemetryOptions\n    );\n\n    // check to see if we have a corresponding parameter\n    // if not, add one\n    const parameterExists = this.#domainObject.configuration.comps.parameters.some(\n      (parameter) => parameter.keyString === keyString\n    );\n    if (!parameterExists) {\n      this.addParameter(telemetryObject);\n    }\n  };\n\n  static getCompsManager(domainObject, openmct, compsManagerPool) {\n    const id = openmct.objects.makeKeyString(domainObject.identifier);\n\n    if (!compsManagerPool[id]) {\n      compsManagerPool[id] = new CompsManager(openmct, domainObject);\n    }\n\n    return compsManagerPool[id];\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsMathWorker.js",
    "content": "import { evaluate } from 'mathjs';\n\n// eslint-disable-next-line no-undef\nonconnect = function (e) {\n  const port = e.ports[0];\n\n  port.onmessage = function (event) {\n    const { type, callbackID, telemetryForComps, expression, parameters, newTelemetry } =\n      event.data;\n    let responseType = 'unknown';\n    let error = null;\n    let result = [];\n    try {\n      if (type === 'calculateRequest') {\n        responseType = 'calculationRequestResult';\n        console.debug(`📫 Received new calculation request with callback ID ${callbackID}`);\n        result = calculateRequest(telemetryForComps, parameters, expression);\n      } else if (type === 'calculateSubscription') {\n        responseType = 'calculationSubscriptionResult';\n        result = calculateSubscription(telemetryForComps, newTelemetry, parameters, expression);\n      } else if (type === 'init') {\n        port.postMessage({ type: 'ready' });\n        return;\n      } else {\n        throw new Error('Invalid message type');\n      }\n    } catch (errorInCalculation) {\n      error = errorInCalculation;\n    }\n    console.debug(`📭 Sending response for callback ID ${callbackID}`, result);\n    port.postMessage({ type: responseType, callbackID, result, error });\n  };\n};\n\nfunction getFullDataFrame(telemetryForComps, parameters) {\n  const dataFrame = {};\n  Object.keys(telemetryForComps)?.forEach((key) => {\n    const parameter = parameters.find((p) => p.keyString === key);\n    const dataSet = telemetryForComps[key];\n    const telemetryMap = new Map(dataSet.map((item) => [item[parameter.timeKey], item]));\n    dataFrame[key] = telemetryMap;\n  });\n  return dataFrame;\n}\n\nfunction calculateSubscription(telemetryForComps, newTelemetry, parameters, expression) {\n  const dataFrame = getFullDataFrame(telemetryForComps, parameters);\n  const calculation = calculate(dataFrame, parameters, expression);\n  const newTelemetryKey = Object.keys(newTelemetry)[0];\n  const newTelemetrySize = newTelemetry[newTelemetryKey].length;\n  let trimmedCalculation = calculation;\n  if (calculation.length > newTelemetrySize) {\n    trimmedCalculation = calculation.slice(calculation.length - newTelemetrySize);\n  }\n  return trimmedCalculation;\n}\n\nfunction calculateRequest(telemetryForComps, parameters, expression) {\n  const dataFrame = getFullDataFrame(telemetryForComps, parameters);\n  return calculate(dataFrame, parameters, expression);\n}\n\nfunction calculate(dataFrame, parameters, expression) {\n  const sumResults = [];\n  // ensure all parameter keyStrings have corresponding telemetry data\n  if (!expression) {\n    return sumResults;\n  }\n  // set up accumulated data structure\n  const accumulatedData = {};\n  parameters.forEach((parameter) => {\n    if (parameter.accumulateValues) {\n      accumulatedData[parameter.name] = [];\n    }\n  });\n\n  // take the first parameter keyString as the reference\n  const referenceParameter = parameters[0];\n  const otherParameters = parameters.slice(1);\n  // iterate over the reference telemetry data\n  const referenceTelemetry = dataFrame[referenceParameter.keyString];\n  referenceTelemetry?.forEach((referenceTelemetryItem) => {\n    let referenceValue = referenceTelemetryItem[referenceParameter.valueToUse];\n    if (referenceParameter.accumulateValues) {\n      accumulatedData[referenceParameter.name].push(referenceValue);\n      referenceValue = accumulatedData[referenceParameter.name];\n    }\n    if (\n      referenceParameter.accumulateValues &&\n      referenceParameter.sampleSize &&\n      referenceParameter.sampleSize > 0\n    ) {\n      // enforce sample size by ensuring referenceValue has the latest n elements\n      // if we don't have at least the sample size, skip this iteration\n      if (!referenceValue.length || referenceValue.length < referenceParameter.sampleSize) {\n        return;\n      }\n      referenceValue = referenceValue.slice(-referenceParameter.sampleSize);\n    }\n\n    const scope = {\n      [referenceParameter.name]: referenceValue\n    };\n    const referenceTime = referenceTelemetryItem[referenceParameter.timeKey];\n    // iterate over the other parameters to set the scope\n    let missingData = false;\n    otherParameters.forEach((parameter) => {\n      const otherDataFrame = dataFrame[parameter.keyString];\n      const otherTelemetry = otherDataFrame.get(referenceTime);\n      if (otherTelemetry === undefined || otherTelemetry === null) {\n        missingData = true;\n        return;\n      }\n      let otherValue = otherTelemetry[parameter.valueToUse];\n      if (parameter.accumulateValues) {\n        accumulatedData[parameter.name].push(referenceValue);\n        otherValue = accumulatedData[referenceParameter.name];\n      }\n      scope[parameter.name] = otherValue;\n    });\n    if (missingData) {\n      console.debug('🤦‍♂️ Missing data for some parameters, skipping calculation');\n      return;\n    }\n    const rawComputedValue = evaluate(expression, scope);\n    let computedValue = rawComputedValue;\n    if (computedValue.entries) {\n      // if there aren't any entries, return with nothing\n      if (computedValue.entries.length === 0) {\n        return;\n      }\n      console.debug('📊 Computed value is an array of entries', computedValue.entries);\n      // make array of arrays of entries\n      computedValue = computedValue.entries?.[0];\n    }\n    sumResults.push({ [referenceParameter.timeKey]: referenceTime, value: computedValue });\n  });\n  return sumResults;\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsMetadataProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport CompsManager from './CompsManager.js';\n\nexport default class CompsMetadataProvider {\n  #openmct = null;\n  #compsManagerPool = null;\n\n  constructor(openmct, compsManagerPool) {\n    this.#openmct = openmct;\n    this.#compsManagerPool = compsManagerPool;\n  }\n\n  supportsMetadata(domainObject) {\n    return domainObject.type === 'comps';\n  }\n\n  getDefaultDomains(domainObject) {\n    return this.#openmct.time.getAllTimeSystems().map(function (ts, i) {\n      return {\n        key: ts.key,\n        name: ts.name,\n        format: ts.timeFormat,\n        hints: {\n          domain: i\n        }\n      };\n    });\n  }\n\n  getMetadata(domainObject) {\n    const specificCompsManager = CompsManager.getCompsManager(\n      domainObject,\n      this.#openmct,\n      this.#compsManagerPool\n    );\n    // if there are any parameters, grab the first one's timeMetaData\n    const timeMetaData = specificCompsManager?.getParameters()[0]?.timeMetaData;\n    const metaDataToReturn = {\n      values: [\n        {\n          key: 'value',\n          name: 'Value',\n          derived: true,\n          formatString: specificCompsManager.getOutputFormat(),\n          hints: {\n            range: 1\n          }\n        }\n      ]\n    };\n    if (timeMetaData) {\n      metaDataToReturn.values.push(timeMetaData);\n    } else {\n      const defaultDomains = this.getDefaultDomains(domainObject);\n      metaDataToReturn.values.push(...defaultDomains);\n    }\n    return metaDataToReturn;\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport CompsManager from './CompsManager.js';\n\nexport default class CompsTelemetryProvider {\n  #openmct = null;\n  #sharedWorker = null;\n  #compsManagerPool = null;\n  #lastUniqueID = 1;\n  #requestPromises = {};\n  #subscriptionCallbacks = {};\n  // id is random 4 digit number\n  #id = Math.floor(Math.random() * 9000) + 1000;\n\n  constructor(openmct, compsManagerPool) {\n    this.#openmct = openmct;\n    this.#compsManagerPool = compsManagerPool;\n    this.#openmct.on('start', this.#startSharedWorker.bind(this));\n  }\n\n  isTelemetryObject(domainObject) {\n    return domainObject.type === 'comps';\n  }\n\n  supportsRequest(domainObject) {\n    return domainObject.type === 'comps';\n  }\n\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'comps';\n  }\n\n  #getCallbackID() {\n    return this.#lastUniqueID++;\n  }\n\n  request(domainObject, options) {\n    return new Promise((resolve, reject) => {\n      const specificCompsManager = CompsManager.getCompsManager(\n        domainObject,\n        this.#openmct,\n        this.#compsManagerPool\n      );\n      specificCompsManager.load(options).then(() => {\n        const callbackID = this.#getCallbackID();\n        const telemetryForComps = JSON.parse(\n          JSON.stringify(specificCompsManager.getDataFrameForRequest())\n        );\n        const expression = specificCompsManager.getExpression();\n        const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));\n        if (!expression || !parameters) {\n          resolve([]);\n          return;\n        }\n        this.#requestPromises[callbackID] = { resolve, reject };\n        const payload = {\n          type: 'calculateRequest',\n          telemetryForComps,\n          expression,\n          parameters,\n          callbackID\n        };\n        this.#sharedWorker.port.postMessage(payload);\n      });\n    });\n  }\n\n  #computeOnNewTelemetry(specificCompsManager, callbackID, newTelemetry) {\n    if (!specificCompsManager.isReady()) {\n      return;\n    }\n    const expression = specificCompsManager.getExpression();\n    const telemetryForComps = specificCompsManager.getDataFrameForSubscription(newTelemetry);\n    const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));\n    if (!expression || !parameters) {\n      return;\n    }\n    const payload = {\n      type: 'calculateSubscription',\n      telemetryForComps,\n      newTelemetry,\n      expression,\n      parameters,\n      callbackID\n    };\n    this.#sharedWorker.port.postMessage(payload);\n  }\n\n  subscribe(domainObject, callback) {\n    const specificCompsManager = CompsManager.getCompsManager(\n      domainObject,\n      this.#openmct,\n      this.#compsManagerPool\n    );\n    const callbackID = this.#getCallbackID();\n    this.#subscriptionCallbacks[callbackID] = callback;\n    const boundComputeOnNewTelemetry = this.#computeOnNewTelemetry.bind(\n      this,\n      specificCompsManager,\n      callbackID\n    );\n    specificCompsManager.on('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);\n    const telemetryOptions = {\n      strategy: 'latest',\n      size: 1\n    };\n    specificCompsManager.load(telemetryOptions);\n    return () => {\n      delete this.#subscriptionCallbacks[callbackID];\n      specificCompsManager.stopListeningToUnderlyingTelemetry();\n      specificCompsManager.off('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);\n    };\n  }\n\n  #startSharedWorker() {\n    if (this.#sharedWorker) {\n      throw new Error('Shared worker already started');\n    }\n    const sharedWorkerURL = `${this.#openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}compsMathWorker.js`;\n\n    this.#sharedWorker = new SharedWorker(sharedWorkerURL, `Comps Math Worker`);\n    this.#sharedWorker.port.onmessage = this.onSharedWorkerMessage.bind(this);\n    this.#sharedWorker.port.onmessageerror = this.onSharedWorkerMessageError.bind(this);\n    this.#sharedWorker.port.start();\n\n    this.#sharedWorker.port.postMessage({ type: 'init' });\n\n    this.#openmct.on('destroy', () => {\n      this.#sharedWorker.port.close();\n    });\n  }\n\n  onSharedWorkerMessage(event) {\n    const { type, result, callbackID, error } = event.data;\n    if (\n      type === 'calculationSubscriptionResult' &&\n      this.#subscriptionCallbacks[callbackID] &&\n      result.length\n    ) {\n      this.#subscriptionCallbacks[callbackID](result);\n    } else if (type === 'calculationRequestResult' && this.#requestPromises[callbackID]) {\n      if (error) {\n        console.error('📝 Error calculating request:', event.data);\n        this.#requestPromises[callbackID].resolve([]);\n      } else {\n        this.#requestPromises[callbackID].resolve(result);\n      }\n      delete this.#requestPromises[callbackID];\n    }\n  }\n\n  onSharedWorkerMessageError(event) {\n    console.error('❌ Shared worker message error:', event);\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/CompsViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport CompsView from './components/CompsView.vue';\n\nconst DEFAULT_VIEW_PRIORITY = 100;\n\nexport default class CompsViewProvider {\n  constructor(openmct, compsManagerPool) {\n    this.openmct = openmct;\n    this.name = 'Comps View';\n    this.key = 'comps.view';\n    this.cssClass = 'icon-derived-telemetry';\n    this.compsManagerPool = compsManagerPool;\n  }\n\n  canView(domainObject, objectPath) {\n    return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);\n  }\n\n  canEdit(domainObject, objectPath) {\n    return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);\n  }\n\n  view(domainObject, objectPath) {\n    let _destroy = null;\n    let component = null;\n\n    return {\n      show: (container, isEditing) => {\n        const { vNode, destroy } = mount(\n          {\n            el: container,\n            components: {\n              CompsView\n            },\n            provide: {\n              openmct: this.openmct,\n              domainObject,\n              objectPath,\n              compsManagerPool: this.compsManagerPool\n            },\n            data() {\n              return {\n                isEditing\n              };\n            },\n            template: '<CompsView :isEditing=\"isEditing\"></CompsView>'\n          },\n          {\n            app: this.openmct.app,\n            element: container\n          }\n        );\n        _destroy = destroy;\n        component = vNode.componentInstance;\n      },\n      onEditModeChange: (isEditing) => {\n        component.isEditing = isEditing;\n      },\n      destroy: () => {\n        if (_destroy) {\n          _destroy();\n        }\n        component = null;\n      }\n    };\n  }\n\n  priority(domainObject) {\n    if (domainObject.type === 'comps') {\n      return Number.MAX_VALUE;\n    } else {\n      return DEFAULT_VIEW_PRIORITY;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/components/CompsInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-inspect-properties\">\n    <template v-if=\"isEditing\">\n      <ul class=\"c-inspect-properties__section\">\n        <li class=\"c-inspect-properties__row\">\n          <div class=\"c-inspect-properties__label\" title=\"Output Format\">\n            <label for=\"OutputFormatControl\">Output Format</label>\n          </div>\n          <div class=\"c-inspect-properties__value\">\n            <input\n              id=\"OutputFormatControl\"\n              v-model=\"inputFormatValue\"\n              type=\"text\"\n              class=\"c-input--flex\"\n              placeholder=\"e.g. %0.2f\"\n              @change=\"changeInputFormat()\"\n            />\n          </div>\n        </li>\n      </ul>\n    </template>\n  </div>\n</template>\n\n<script setup>\nimport { inject, onBeforeMount, onBeforeUnmount, ref } from 'vue';\n\nimport CompsManager from '../CompsManager';\n\nconst isEditing = ref(false);\nconst inputFormatValue = ref('');\n\nconst openmct = inject('openmct');\nconst domainObject = inject('domainObject');\nconst compsManagerPool = inject('compsManagerPool');\n\nonBeforeMount(() => {\n  isEditing.value = openmct.editor.isEditing();\n  openmct.editor.on('isEditing', toggleEdit);\n  inputFormatValue.value = domainObject.configuration.comps.outputFormat;\n});\n\nonBeforeUnmount(() => {\n  openmct.editor.off('isEditing', toggleEdit);\n});\n\nfunction toggleEdit(passedIsEditing) {\n  isEditing.value = passedIsEditing;\n}\n\nfunction changeInputFormat() {\n  openmct.objects.mutate(domainObject, `configuration.comps.outputFormat`, inputFormatValue.value);\n  const compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);\n  compsManager.setOutputFormat(inputFormatValue.value);\n}\n</script>\n"
  },
  {
    "path": "src/plugins/comps/components/CompsView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-comps\" aria-label=\"Derived Telemetry\">\n    <section class=\"c-section c-comps-output\">\n      <div class=\"c-output-featured\">\n        <span class=\"c-output-featured__label\">Current Output</span>\n        <span class=\"c-output-featured__value\" aria-label=\"Current Output Value\">\n          <template\n            v-if=\"testDataApplied && currentTestOutput !== undefined && currentTestOutput !== null\"\n          >\n            {{ currentTestOutput }}\n          </template>\n          <template\n            v-else-if=\"\n              !testDataApplied && currentCompOutput !== undefined && currentCompOutput !== null\n            \"\n          >\n            {{ currentCompOutput }}\n          </template>\n          <template v-else> --- </template>\n        </span>\n      </div>\n    </section>\n    <section\n      id=\"telemetryReferenceSection\"\n      class=\"c-comps__section c-comps__refs-and-controls\"\n      aria-describedby=\"telemetryReferences\"\n    >\n      <div class=\"c-cs__header c-section__header\">\n        <div id=\"telemetryReferences\" class=\"c-cs__header-label c-section__label\">\n          Telemetry References\n        </div>\n      </div>\n\n      <div\n        v-if=\"isEditing\"\n        class=\"c-comps__apply-test-data-control\"\n        :class=\"['c-comps__refs-controls c-cdef__controls', { disabled: !parameters?.length }]\"\n      >\n        <label class=\"c-toggle-switch\">\n          <input type=\"checkbox\" :checked=\"testDataApplied\" @change=\"toggleTestData\" />\n          <span class=\"c-toggle-switch__slider\" aria-label=\"Apply Test Data\"></span>\n          <span class=\"c-toggle-switch__label\">Apply Test Values</span>\n        </label>\n      </div>\n      <div class=\"c-comps__refs\">\n        <div v-for=\"parameter in parameters\" :key=\"parameter.keyString\" class=\"c-comps__ref\">\n          <div class=\"c-comps__ref-section\">\n            <div class=\"c-comps__ref-sub-section ref-and-path\">\n              <span class=\"c-test-datum__string\">Reference</span>\n              <input\n                v-if=\"isEditing\"\n                v-model=\"parameter.name\"\n                :aria-label=\"`Reference Name Input for ${parameter.name}`\"\n                type=\"text\"\n                class=\"c-input--md\"\n                @change=\"updateParameters\"\n              />\n              <div v-else class=\"--em\">{{ parameter.name }}</div>\n              <span class=\"c-test-datum__string\">=</span>\n              <span\n                class=\"c-comps__path-and-field\"\n                :aria-label=\"`Reference ${parameter.name} Object Path`\"\n              >\n                <ObjectPathString\n                  v-if=\"telemetryObjectsMap[parameter.keyString]\"\n                  :domain-object=\"telemetryObjectsMap[parameter.keyString]\"\n                  :show-object-itself=\"true\"\n                  class=\"c-comp__ref-path --em\"\n                />\n                <!-- drop down to select value from telemetry -->\n                <select\n                  v-if=\"isEditing\"\n                  v-model=\"parameter.valueToUse\"\n                  class=\"c-comp__ref-field\"\n                  @change=\"updateParameters\"\n                >\n                  <option\n                    v-for=\"parameterValueOption in parameterValueOptionsMap[parameter.keyString]\"\n                    :key=\"parameterValueOption.key\"\n                    :value=\"parameterValueOption.key\"\n                  >\n                    {{ parameterValueOption.name }}\n                  </option>\n                </select>\n                <div v-else class=\"c-comp__ref-field\">{{ parameter.valueToUse }}</div>\n              </span>\n            </div>\n\n            <div\n              v-if=\"isEditing\"\n              class=\"c-comps__ref-sub-section accum-vals\"\n              :class=\"['c-comps__refs-controls', { disabled: !parameters?.length }]\"\n            >\n              <label class=\"c-toggle-switch\">\n                <span class=\"c-toggle-switch__label\">Accumulate Values</span>\n                <input\n                  v-model=\"parameter.accumulateValues\"\n                  type=\"checkbox\"\n                  @change=\"updateAccumulateValues(parameter)\"\n                />\n                <span\n                  class=\"c-toggle-switch__slider\"\n                  aria-label=\"Toggle Parameter Accumulation\"\n                ></span>\n              </label>\n\n              <span v-if=\"isEditing && parameter.accumulateValues\" class=\"c-comps__label\"\n                >Sample Size</span\n              >\n              <input\n                v-if=\"isEditing && parameter.accumulateValues\"\n                v-model=\"parameter.sampleSize\"\n                :aria-label=\"`Sample Size for ${parameter.name}`\"\n                type=\"number\"\n                class=\"c-input--sm c-comps__value\"\n                @change=\"updateParameters\"\n              />\n            </div>\n\n            <div\n              v-if=\"!isEditing && parameter.accumulateValues\"\n              class=\"c-comps__ref-sub-section accum-vals\"\n            >\n              Accumulating values with sample size {{ parameter.sampleSize }}\n            </div>\n          </div>\n\n          <div v-if=\"isEditing\" class=\"c-comps__ref-section\">\n            <span class=\"c-comps__label\">Test value</span>\n            <input\n              v-if=\"isEditing\"\n              v-model=\"parameter.testValue\"\n              :aria-label=\"`Reference Test Value for ${parameter.name}`\"\n              type=\"text\"\n              class=\"c-input--md c-comps__value\"\n              @change=\"updateTestValue(parameter)\"\n            />\n          </div>\n        </div>\n      </div>\n    </section>\n    <section id=\"expressionSection\" class=\"c-comps__section c-comps__expression\">\n      <div class=\"c-cs__header c-section__header\">\n        <div class=\"c-cs__header-label c-section__label\">Expression</div>\n      </div>\n\n      <div v-if=\"!parameters?.length && isEditing\" class=\"hint\">\n        Drag in telemetry to add references for an expression.\n      </div>\n\n      <textarea\n        v-if=\"parameters?.length && isEditing\"\n        v-model=\"expression\"\n        class=\"c-comps__expression-value\"\n        placeholder=\"Enter an expression\"\n        @change=\"updateExpression\"\n      ></textarea>\n      <div v-else>\n        <div class=\"c-comps__expression-value\" aria-label=\"Expression\">\n          {{ expression }}\n        </div>\n      </div>\n      <span\n        v-if=\"expression && expressionOutput\"\n        class=\"icon-alert-triangle c-comps__expression-msg --bad\"\n      >\n        Invalid: {{ expressionOutput }}\n      </span>\n      <span\n        v-else-if=\"expression && !expressionOutput && isEditing\"\n        class=\"c-comps__expression-msg --good\"\n      >\n        Expression valid\n      </span>\n    </section>\n  </div>\n</template>\n\n<script setup>\nimport { evaluate } from 'mathjs';\nimport { inject, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue';\n\nimport ObjectPathString from '../../../ui/components/ObjectPathString.vue';\nimport CompsManager from '../CompsManager';\n\nconst openmct = inject('openmct');\nconst domainObject = inject('domainObject');\nconst compsManagerPool = inject('compsManagerPool');\nconst compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);\nconst currentCompOutput = ref(null);\nconst currentTestOutput = ref(null);\nconst testDataApplied = ref(false);\nconst parameters = ref(null);\nconst expression = ref(null);\nconst expressionOutput = ref(null);\nconst outputFormat = ref(null);\nconst parameterValueOptionsMap = ref({});\nconst telemetryObjectsMap = ref({});\n\nlet outputTelemetryCollection;\n\nconst props = defineProps({\n  isEditing: {\n    type: Boolean,\n    required: true\n  }\n});\n\nfunction computeMaxSampleSize() {\n  let maxSampleSize = 20;\n  if (parameters.value) {\n    maxSampleSize =\n      parameters.value.reduce((max, param) => {\n        if (param.accumulateValues) {\n          return Math.max(max, param.sampleSize);\n        }\n        return max;\n      }, 0) + 20;\n  }\n  return maxSampleSize;\n}\n\nfunction createOutputTelemetryCollection(maxSampleSize) {\n  const telemetryOptions = {\n    strategy: 'minmax',\n    size: maxSampleSize\n  };\n\n  // TODO: we should dynamically set size to the largest comp input window\n  const collection = openmct.telemetry.requestCollection(domainObject, telemetryOptions);\n\n  collection.on('add', telemetryProcessor);\n  collection.on('clear', clearData);\n\n  return collection;\n}\n\nonBeforeMount(async () => {\n  const maxSampleSize = computeMaxSampleSize();\n\n  outputTelemetryCollection = createOutputTelemetryCollection(maxSampleSize);\n  compsManager.on('parameterAdded', reloadParameters);\n  compsManager.on('parameterRemoved', reloadParameters);\n  compsManager.on('outputFormatChanged', updateOutputFormat);\n\n  // Track outstanding requests using a Promise\n  let outstandingRequests = 0;\n\n  const allRequestsComplete = new Promise((resolve) => {\n    outputTelemetryCollection.on('requestStarted', () => {\n      outstandingRequests++;\n    });\n\n    outputTelemetryCollection.on('requestEnded', () => {\n      outstandingRequests--;\n      if (outstandingRequests === 0) {\n        resolve();\n      }\n    });\n    outputTelemetryCollection.load(); // will implicitly load compsManager\n\n    // If no requests were started, resolve immediately\n    if (outstandingRequests === 0) {\n      resolve();\n    }\n  });\n\n  await allRequestsComplete;\n\n  // Update state after all requests are complete\n  parameters.value = compsManager.getParameters();\n  // Also get the metadata and objects from compsManager\n  expression.value = compsManager.getExpression();\n  outputFormat.value = compsManager.getOutputFormat();\n  applyTestData();\n});\n\nonBeforeUnmount(() => {\n  compsManager.off('parameterAdded', reloadParameters);\n  compsManager.off('parameterRemoved', reloadParameters);\n  compsManager.off('outputFormatChanged', updateOutputFormat);\n  if (outputTelemetryCollection) {\n    outputTelemetryCollection.destroy();\n  }\n});\n\nwatch(\n  () => props.isEditing,\n  (editMode) => {\n    if (!editMode) {\n      testDataApplied.value = false;\n    }\n  }\n);\n\nwatch(\n  () => parameters.value,\n  () => {\n    if (parameters.value?.length) {\n      const paramMetadataMap = {};\n      const telemetryObjMap = {};\n      parameters.value.forEach((param) => {\n        const metadataValues = compsManager.getMetaDataValuesForParameter(param.keyString);\n        if (metadataValues) {\n          paramMetadataMap[param.keyString] = metadataValues;\n        }\n        const telemetryObject = compsManager.getTelemetryObjectForParameter(param.keyString);\n        if (telemetryObject) {\n          telemetryObjMap[param.keyString] = telemetryObject;\n        }\n      });\n      parameterValueOptionsMap.value = paramMetadataMap;\n      telemetryObjectsMap.value = telemetryObjMap;\n    } else {\n      parameterValueOptionsMap.value = {};\n      telemetryObjectsMap.value = {};\n    }\n  }\n);\n\nfunction updateOutputFormat() {\n  outputFormat.value = compsManager.getOutputFormat();\n  // delete the metadata cache so that the new output format is used\n  openmct.telemetry.removeMetadataFromCache(domainObject);\n}\n\nfunction reloadParameters() {\n  domainObject.configuration.comps.parameters = compsManager.getParameters();\n  parameters.value = domainObject.configuration.comps.parameters;\n  openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);\n  compsManager.setDomainObject(domainObject);\n  applyTestData();\n}\n\nfunction updateParameters() {\n  openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);\n  compsManager.setDomainObject(domainObject);\n  applyTestData();\n  reload();\n}\n\nfunction updateAccumulateValues(parameter) {\n  if (parameter.accumulateValues) {\n    parameter.testValue = [''];\n  } else {\n    parameter.testValue = '';\n  }\n  updateParameters();\n}\n\nfunction updateTestValue(parameter) {\n  if (parameter.accumulateValues && parameter.testValue === '') {\n    parameter.testValue = [];\n  }\n  updateParameters();\n}\n\nfunction toggleTestData() {\n  testDataApplied.value = !testDataApplied.value;\n  if (testDataApplied.value) {\n    applyTestData();\n  } else {\n    clearData();\n  }\n}\n\nfunction updateExpression() {\n  openmct.objects.mutate(domainObject, `configuration.comps.expression`, expression.value);\n  compsManager.setDomainObject(domainObject);\n  applyTestData();\n  reload();\n}\n\nfunction getValueFormatter() {\n  const metaData = openmct.telemetry.getMetadata(domainObject);\n  const outputMetaDatum = metaData.values().find((metaDatum) => metaDatum.key === 'value');\n  return openmct.telemetry.getValueFormatter(outputMetaDatum);\n}\n\nfunction applyTestData() {\n  if (expression.value === undefined || parameters.value === undefined) {\n    return;\n  }\n  const scope = parameters.value.reduce((acc, parameter) => {\n    // try to parse the test value as JSON\n    try {\n      const parsedValue = JSON.parse(parameter.testValue);\n      acc[parameter.name] = parsedValue;\n    } catch (error) {\n      acc[parameter.name] = parameter.testValue;\n    }\n    return acc;\n  }, {});\n\n  // see which parameters are misconfigured as non-arrays\n  const misconfiguredParameterNames = parameters.value\n    .filter((parameter) => {\n      return parameter.accumulateValues && !Array.isArray(scope[parameter.name]);\n    })\n    .map((parameter) => parameter.name);\n  if (misconfiguredParameterNames.length) {\n    const misconfiguredParameterNamesString = misconfiguredParameterNames.join(', ');\n    currentTestOutput.value = null;\n    expressionOutput.value = `Reference \"${misconfiguredParameterNamesString}\" set to accumulating, but test values aren't arrays.`;\n    return;\n  }\n\n  try {\n    const testOutput = evaluate(expression.value, scope);\n    const formattedData = getValueFormatter().format(testOutput);\n    currentTestOutput.value = formattedData;\n    expressionOutput.value = null;\n  } catch (error) {\n    currentTestOutput.value = null;\n    expressionOutput.value = error.message;\n  }\n}\n\nfunction telemetryProcessor(data) {\n  if (testDataApplied.value) {\n    return;\n  }\n  // new data will come in as array, so just take the last element\n  const currentOutput = data[data.length - 1]?.value;\n  const formattedOutput = getValueFormatter().format(currentOutput);\n  currentCompOutput.value = formattedOutput;\n}\n\nfunction reload() {\n  clearData();\n  if (outputTelemetryCollection) {\n    outputTelemetryCollection.destroy();\n  }\n\n  const maxSampleSize = computeMaxSampleSize();\n\n  outputTelemetryCollection = createOutputTelemetryCollection(maxSampleSize);\n  outputTelemetryCollection.load();\n}\n\nfunction clearData() {\n  currentCompOutput.value = null;\n}\n</script>\n"
  },
  {
    "path": "src/plugins/comps/components/comps.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n@mixin expressionMsg($fg, $bg) {\n  $op: 0.4;\n  color: rgba($fg, $op * 1.5);\n  background: rgba($bg, $op);\n}\n\n.c-comps {\n  display: flex;\n  flex-direction: column;\n  gap: $interiorMarginLg;\n\n  .is-editing & {\n    padding: $interiorMargin;\n  }\n\n  &__output {\n    display: flex;\n    align-items: baseline;\n    gap: $interiorMargin;\n\n    &-label {\n      flex: 0 0 auto;\n      text-transform: uppercase;\n    }\n\n    &-value {\n      flex: 0 1 auto;\n    }\n  }\n\n  &__section,\n  &__refs {\n    display: flex;\n    flex-direction: column;\n    gap: $interiorMarginSm;\n  }\n\n\n  &__apply-test-data-control {\n    padding: $interiorMargin 0;\n  }\n\n  &__refs {\n\n  }\n\n  &__ref {\n    @include discreteItem();\n    align-items: start;\n    display: flex;\n    flex-direction: column;\n    padding: 0 $interiorMargin;\n    line-height: 170%; // Aligns text with controls like selects\n\n    > * + * {\n      border-top: 1px solid $colorInteriorBorder;\n    }\n  }\n\n  &__ref-section {\n    align-items: baseline;\n    display: flex;\n    flex-wrap: wrap;\n    gap: $interiorMargin;\n    padding: $interiorMargin 0;\n    width: 100%;\n  }\n\n  &__ref-sub-section {\n    align-items: baseline;\n    display: flex;\n    flex: 1 1 auto;\n    gap: $interiorMargin;\n\n    &.ref-and-path {\n      flex: 0 1 auto;\n      flex-wrap: wrap;\n    }\n  }\n\n  &__path-and-field {\n    align-items: start;\n    display: flex;\n    flex-wrap: wrap;\n    gap: $interiorMargin;\n\n    .c-comp__ref-path {\n      word-break: break-all;\n    }\n  }\n\n  &__label,\n  &__value {\n    white-space: nowrap;\n  }\n\n  &__expression {\n    *[class*=value] {\n      font-family: monospace;\n      resize: vertical; // Only applies to textarea\n    }\n    div[class*=value] {\n      padding: $interiorMargin;\n    }\n  }\n\n  &__expression-msg {\n    @include expressionMsg($colorOkFg, $colorOk);\n    border-radius: $basicCr;\n    display: flex; // Creates hanging indent from :before icon\n    padding: $interiorMarginSm $interiorMarginLg $interiorMarginSm $interiorMargin;\n    max-width: max-content;\n\n    &:before {\n      content: $glyph-icon-check;\n      font-family: symbolsfont;\n      margin-right: $interiorMarginSm;\n    }\n\n    &.--bad {\n      @include expressionMsg($colorErrorFg, $colorError);\n\n      &:before {\n        content: $glyph-icon-alert-triangle;\n      }\n    }\n  }\n\n  .--em {\n    color: $colorBodyFgEm;\n  }\n}\n"
  },
  {
    "path": "src/plugins/comps/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport CompsInspectorViewProvider from './CompsInspectorViewProvider.js';\nimport CompsMetadataProvider from './CompsMetadataProvider.js';\nimport CompsTelemetryProvider from './CompsTelemetryProvider.js';\nimport CompsViewProvider from './CompsViewProvider.js';\n\nexport default function DerivedTelemetryPlugin() {\n  const compsManagerPool = {};\n\n  return function install(openmct) {\n    openmct.types.addType('comps', {\n      name: 'Derived Telemetry',\n      key: 'comps',\n      description:\n        'Add one or more telemetry end points, apply a mathematical operation to them, and output the result as new telemetry.',\n      creatable: true,\n      cssClass: 'icon-derived-telemetry',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          comps: {\n            expression: '',\n            parameters: []\n          }\n        };\n        domainObject.composition = [];\n        domainObject.telemetry = {};\n      }\n    });\n    openmct.composition.addPolicy((parent, child) => {\n      if (parent.type === 'comps' && !openmct.telemetry.isTelemetryObject(child)) {\n        return false;\n      }\n      return true;\n    });\n    openmct.telemetry.addProvider(new CompsMetadataProvider(openmct, compsManagerPool));\n    openmct.telemetry.addProvider(new CompsTelemetryProvider(openmct, compsManagerPool));\n    openmct.objectViews.addProvider(new CompsViewProvider(openmct, compsManagerPool));\n    openmct.inspectorViews.addProvider(new CompsInspectorViewProvider(openmct, compsManagerPool));\n  };\n}\n"
  },
  {
    "path": "src/plugins/condition/Condition.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { v4 as uuid } from 'uuid';\n\nimport AllTelemetryCriterion from './criterion/AllTelemetryCriterion.js';\nimport TelemetryCriterion from './criterion/TelemetryCriterion.js';\nimport { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from './utils/constants.js';\nimport { evaluateResults } from './utils/evaluator.js';\nimport { getLatestTimestamp } from './utils/time.js';\n\n/*\n * conditionConfiguration = {\n *   id: uuid,\n *   trigger: 'any'/'all'/'not','xor',\n *   criteria: [\n *       {\n *           telemetry: '',\n *           operation: '',\n *           input: [],\n *           metadata: ''\n *       }\n *   ]\n * }\n */\nexport default class Condition extends EventEmitter {\n  #definition;\n  /**\n   * Manages criteria and emits the result of - true or false - based on criteria evaluated.\n   * @constructor\n   * @param definition: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }\n   * @param openmct\n   * @param conditionManager\n   */\n  constructor(definition, openmct, conditionManager) {\n    super();\n\n    this.openmct = openmct;\n    this.conditionManager = conditionManager;\n    this.criteria = [];\n    this.result = undefined;\n    this.timeSystems = this.openmct.time.getAllTimeSystems();\n    this.#definition = definition;\n\n    if (definition.configuration.criteria) {\n      this.createCriteria(definition.configuration.criteria);\n    }\n\n    this.trigger = definition.configuration.trigger;\n    this.summary = '';\n    this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);\n    this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);\n    this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);\n  }\n  get id() {\n    return this.#definition.id;\n  }\n  get configuration() {\n    return this.#definition.configuration;\n  }\n\n  updateResult(latestDataTable, telemetryIdThatChanged) {\n    if (!latestDataTable) {\n      console.log('no data received');\n      return;\n    }\n\n    // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate\n    if (this.hasNoTelemetry() || this.isTelemetryUsed(telemetryIdThatChanged)) {\n      const currentTimeSystemKey = this.openmct.time.getTimeSystem().key;\n      this.criteria.forEach((criterion) => {\n        if (this.isAnyOrAllTelemetry(criterion)) {\n          criterion.updateResult(latestDataTable, this.conditionManager.telemetryObjects);\n        } else {\n          const relevantDatum = latestDataTable.get(criterion.telemetryObjectIdAsString);\n          if (criterion.shouldUpdateResult(relevantDatum, currentTimeSystemKey)) {\n            criterion.updateResult(relevantDatum, currentTimeSystemKey);\n          }\n        }\n      });\n\n      this.result = evaluateResults(\n        this.criteria.map((criterion) => criterion.result),\n        this.trigger\n      );\n    }\n  }\n\n  isAnyOrAllTelemetry(criterion) {\n    return criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any');\n  }\n\n  hasNoTelemetry() {\n    const usesSomeTelemetry = this.criteria.some((criterion) => {\n      return this.isAnyOrAllTelemetry(criterion) || criterion.telemetry !== '';\n    });\n\n    return !usesSomeTelemetry;\n  }\n\n  isTelemetryUsed(id) {\n    return this.criteria.some((criterion) => {\n      return this.isAnyOrAllTelemetry(criterion) || criterion.usesTelemetry(id);\n    });\n  }\n\n  update(conditionConfiguration) {\n    this.updateTrigger(conditionConfiguration.configuration.trigger);\n    this.updateCriteria(conditionConfiguration.configuration.criteria);\n  }\n\n  updateTrigger(trigger) {\n    if (this.trigger !== trigger) {\n      this.trigger = trigger;\n    }\n  }\n\n  generateCriterion(criterionConfiguration) {\n    return {\n      id: criterionConfiguration.id || uuid(),\n      telemetry: criterionConfiguration.telemetry || '',\n      telemetryObjects: this.conditionManager.telemetryObjects,\n      operation: criterionConfiguration.operation || '',\n      input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input,\n      metadata: criterionConfiguration.metadata || ''\n    };\n  }\n\n  createCriteria(criterionConfigurations) {\n    criterionConfigurations.forEach((criterionConfiguration) => {\n      this.addCriterion(criterionConfiguration);\n    });\n  }\n\n  updateCriteria(criterionConfigurations) {\n    this.destroyCriteria();\n    this.createCriteria(criterionConfigurations);\n  }\n\n  updateTelemetryObjects() {\n    this.criteria.forEach((criterion) => {\n      criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);\n    });\n  }\n\n  /**\n   *  adds criterion to the condition.\n   */\n  addCriterion(criterionConfiguration) {\n    let criterion;\n    let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null);\n    if (\n      criterionConfiguration.telemetry &&\n      (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all')\n    ) {\n      criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct);\n    } else {\n      criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);\n    }\n\n    criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));\n    criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));\n    criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());\n    if (!this.criteria) {\n      this.criteria = [];\n    }\n\n    this.criteria.push(criterion);\n\n    return criterionConfigurationWithId.id;\n  }\n\n  findCriterion(id) {\n    let criterion;\n\n    for (let i = 0; i < this.criteria.length; i++) {\n      if (this.criteria[i].id === id) {\n        criterion = {\n          item: this.criteria[i],\n          index: i\n        };\n      }\n    }\n\n    return criterion;\n  }\n\n  updateCriterion(id, criterionConfiguration) {\n    let found = this.findCriterion(id);\n    if (found) {\n      const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);\n      let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);\n      newCriterion.on('criterionUpdated', this.handleCriterionUpdated);\n      newCriterion.on('telemetryIsOld', this.handleOldTelemetryCriterion);\n      newCriterion.on('telemetryStaleness', this.handleTelemetryStaleness);\n\n      let criterion = found.item;\n      criterion.unsubscribe();\n      criterion.off('criterionUpdated', this.handleCriterionUpdated);\n      criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);\n      newCriterion.off('telemetryStaleness', this.handleTelemetryStaleness);\n      this.criteria.splice(found.index, 1, newCriterion);\n    }\n  }\n\n  destroyCriterion(id) {\n    let found = this.findCriterion(id);\n    if (found) {\n      let criterion = found.item;\n      criterion.off('criterionUpdated', this.handleCriterionUpdated);\n      criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);\n      criterion.off('telemetryStaleness', this.handleTelemetryStaleness);\n      criterion.destroy();\n      this.criteria.splice(found.index, 1);\n\n      return true;\n    }\n\n    return false;\n  }\n\n  handleCriterionUpdated(criterion) {\n    let found = this.findCriterion(criterion.id);\n    if (found) {\n      this.criteria[found.index] = criterion.data;\n    }\n  }\n\n  handleOldTelemetryCriterion(updatedCriterion) {\n    this.result = evaluateResults(\n      this.criteria.map((criterion) => criterion.result),\n      this.trigger\n    );\n    let latestTimestamp = {};\n    latestTimestamp = getLatestTimestamp(\n      latestTimestamp,\n      updatedCriterion.data,\n      this.timeSystems,\n      this.openmct.time.getTimeSystem()\n    );\n    this.conditionManager.updateCurrentCondition(latestTimestamp, this);\n  }\n\n  handleTelemetryStaleness() {\n    this.result = evaluateResults(\n      this.criteria.map((criterion) => criterion.result),\n      this.trigger\n    );\n    this.conditionManager.updateCurrentCondition();\n  }\n\n  updateDescription() {\n    const triggerDescription = this.getTriggerDescription();\n    let description = '';\n    this.criteria.forEach((criterion, index) => {\n      if (!index) {\n        description = `Match if ${triggerDescription.prefix}`;\n      }\n\n      description = `${description} ${criterion.getDescription()} ${\n        index < this.criteria.length - 1 ? triggerDescription.conjunction : ''\n      }`;\n    });\n    this.summary = description;\n  }\n\n  getTriggerDescription() {\n    if (this.trigger) {\n      return {\n        conjunction: TRIGGER_CONJUNCTION[this.trigger],\n        prefix: `${TRIGGER_LABEL[this.trigger]}: `\n      };\n    } else {\n      return {\n        conjunction: '',\n        prefix: ''\n      };\n    }\n  }\n\n  requestLADConditionResult(options) {\n    let latestTimestamp;\n    let criteriaResults = {};\n    const criteriaRequests = this.criteria.map((criterion) =>\n      criterion.requestLAD(this.conditionManager.telemetryObjects, options)\n    );\n\n    return Promise.all(criteriaRequests).then((results) => {\n      results.forEach((resultObj) => {\n        const {\n          id,\n          data,\n          data: { result }\n        } = resultObj;\n        if (this.findCriterion(id)) {\n          criteriaResults[id] = Boolean(result);\n        }\n\n        latestTimestamp = getLatestTimestamp(\n          latestTimestamp,\n          data,\n          this.timeSystems,\n          this.openmct.time.getTimeSystem()\n        );\n      });\n\n      return {\n        id: this.id,\n        data: Object.assign({}, latestTimestamp, {\n          result: evaluateResults(Object.values(criteriaResults), this.trigger)\n        })\n      };\n    });\n  }\n\n  getCriteria() {\n    return this.criteria;\n  }\n\n  destroyCriteria() {\n    let success = true;\n    //looping through the array backwards since destroyCriterion modifies the criteria array\n    for (let i = this.criteria.length - 1; i >= 0; i--) {\n      success = success && this.destroyCriterion(this.criteria[i].id);\n    }\n\n    return success;\n  }\n\n  destroy() {\n    this.destroyCriteria();\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionManager.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { v4 as uuid } from 'uuid';\n\nimport Condition from './Condition.js';\nimport { getLatestTimestamp } from './utils/time.js';\n\nexport default class ConditionManager extends EventEmitter {\n  #latestDataTable = new Map();\n\n  /**\n   * @param {import('openmct.js').DomainObject} conditionSetDomainObject\n   * @param {import('openmct.js').OpenMCT} openmct\n   */\n  constructor(conditionSetDomainObject, openmct) {\n    super();\n    this.openmct = openmct;\n    this.conditionSetDomainObject = conditionSetDomainObject;\n    this.timeSystems = this.openmct.time.getAllTimeSystems();\n    this.composition = this.openmct.composition.get(conditionSetDomainObject);\n    this.composition.on('add', this.subscribeToTelemetry, this);\n    this.composition.on('remove', this.unsubscribeFromTelemetry, this);\n\n    this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);\n\n    this.compositionLoad = this.composition.load();\n    this.telemetryCollections = {};\n    this.telemetryObjects = {};\n    this.testData = {\n      conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,\n      applied: false\n    };\n    this.initialize();\n  }\n\n  subscribeToTelemetry(telemetryObject) {\n    const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n\n    if (this.telemetryCollections[keyString]) {\n      return;\n    }\n\n    const requestOptions = {\n      size: 1,\n      strategy: 'latest'\n    };\n\n    this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(\n      telemetryObject,\n      requestOptions\n    );\n\n    const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n    const telemetryMetaData = metadata ? metadata.valueMetadatas : [];\n\n    this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData };\n\n    this.telemetryCollections[keyString].on(\n      'add',\n      this.telemetryReceived.bind(this, telemetryObject)\n    );\n    this.telemetryCollections[keyString].load();\n\n    this.updateConditionTelemetryObjects();\n  }\n\n  unsubscribeFromTelemetry(endpointIdentifier) {\n    const keyString = this.openmct.objects.makeKeyString(endpointIdentifier);\n    if (!this.telemetryCollections[keyString]) {\n      return;\n    }\n\n    this.telemetryCollections[keyString].destroy();\n    this.telemetryCollections[keyString] = null;\n    this.telemetryObjects[keyString] = null;\n    this.removeConditionTelemetryObjects();\n\n    //force re-computation of condition set result as we might be in a state where\n    // there is no telemetry datum coming in for a while or at all.\n    const latestTimestamp = getLatestTimestamp(\n      {},\n      {},\n      this.timeSystems,\n      this.openmct.time.getTimeSystem()\n    );\n    this.updateConditionResults({ id: keyString });\n    this.updateCurrentCondition(latestTimestamp);\n\n    if (Object.keys(this.telemetryObjects).length === 0) {\n      // no telemetry objects\n      this.emit('noTelemetryObjects');\n    }\n  }\n\n  initialize() {\n    this.conditions = [];\n    if (this.conditionSetDomainObject.configuration.conditionCollection.length) {\n      this.conditionSetDomainObject.configuration.conditionCollection.forEach(\n        (conditionConfiguration, index) => {\n          this.initCondition(conditionConfiguration, index);\n        }\n      );\n    }\n\n    if (Object.keys(this.telemetryObjects).length === 0) {\n      // no telemetry objects\n      this.emit('noTelemetryObjects');\n    }\n  }\n\n  updateConditionTelemetryObjects() {\n    this.conditions.forEach((condition) => {\n      condition.updateTelemetryObjects();\n      let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(\n        (item) => item.id === condition.id\n      );\n      if (index > -1) {\n        //Only assign the summary, don't mutate the domain object\n        this.conditionSetDomainObject.configuration.conditionCollection[index].summary =\n          this.updateConditionDescription(condition);\n      }\n    });\n  }\n\n  removeConditionTelemetryObjects() {\n    let conditionsChanged = false;\n    this.conditionSetDomainObject.configuration.conditionCollection.forEach(\n      (conditionConfiguration, conditionIndex) => {\n        let conditionChanged = false;\n        conditionConfiguration.configuration.criteria.forEach((criterion, index) => {\n          const isAnyAllTelemetry =\n            criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all');\n          if (!isAnyAllTelemetry) {\n            const found = Object.values(this.telemetryObjects).find((telemetryObject) => {\n              return this.openmct.objects.areIdsEqual(\n                telemetryObject.identifier,\n                criterion.telemetry\n              );\n            });\n            if (!found) {\n              criterion.telemetry = '';\n              criterion.metadata = '';\n              criterion.input = [];\n              criterion.operation = '';\n              conditionChanged = true;\n            }\n          } else {\n            conditionChanged = true;\n          }\n        });\n        if (conditionChanged) {\n          this.updateCondition(conditionConfiguration, conditionIndex);\n          conditionsChanged = true;\n        }\n      }\n    );\n    if (conditionsChanged) {\n      this.persistConditions();\n    }\n  }\n\n  updateConditionDescription(condition) {\n    condition.updateDescription();\n\n    return condition.summary;\n  }\n\n  updateCondition(conditionConfiguration) {\n    let condition = this.findConditionById(conditionConfiguration.id);\n    if (condition) {\n      condition.update(conditionConfiguration);\n      conditionConfiguration.summary = this.updateConditionDescription(condition);\n    }\n\n    let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(\n      (item) => item.id === conditionConfiguration.id\n    );\n    if (index > -1) {\n      this.conditionSetDomainObject.configuration.conditionCollection[index] =\n        conditionConfiguration;\n      this.persistConditions();\n    }\n  }\n\n  initCondition(conditionConfiguration, index) {\n    let condition = new Condition(conditionConfiguration, this.openmct, this);\n    conditionConfiguration.summary = this.updateConditionDescription(condition);\n\n    if (index !== undefined) {\n      this.conditions.splice(index + 1, 0, condition);\n    } else {\n      this.conditions.unshift(condition);\n    }\n  }\n\n  createCondition(conditionConfiguration) {\n    let conditionObj;\n    if (conditionConfiguration) {\n      conditionObj = {\n        ...conditionConfiguration,\n        id: uuid(),\n        configuration: {\n          ...conditionConfiguration.configuration,\n          name: `Copy of ${conditionConfiguration.configuration.name}`\n        }\n      };\n    } else {\n      conditionObj = {\n        id: uuid(),\n        configuration: {\n          name: 'Unnamed Condition',\n          output: 'false',\n          trigger: 'all',\n          criteria: [\n            {\n              id: uuid(),\n              telemetry: '',\n              operation: '',\n              input: [],\n              metadata: ''\n            }\n          ]\n        },\n        summary: ''\n      };\n    }\n\n    return conditionObj;\n  }\n\n  addCondition() {\n    this.createAndSaveCondition();\n  }\n\n  cloneCondition(conditionConfiguration, index) {\n    let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration));\n    clonedConfig.configuration.criteria.forEach((criterion) => (criterion.id = uuid()));\n    this.createAndSaveCondition(index, clonedConfig);\n  }\n\n  createAndSaveCondition(index, conditionConfiguration) {\n    const newCondition = this.createCondition(conditionConfiguration);\n    if (index !== undefined) {\n      this.conditionSetDomainObject.configuration.conditionCollection.splice(\n        index + 1,\n        0,\n        newCondition\n      );\n    } else {\n      this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition);\n    }\n\n    this.initCondition(newCondition, index);\n    this.persistConditions();\n  }\n\n  removeCondition(id) {\n    let index = this.conditions.findIndex((item) => item.id === id);\n    if (index > -1) {\n      this.conditions[index].destroy();\n      this.conditions.splice(index, 1);\n    }\n\n    let conditionCollectionIndex =\n      this.conditionSetDomainObject.configuration.conditionCollection.findIndex(\n        (item) => item.id === id\n      );\n    if (conditionCollectionIndex > -1) {\n      this.conditionSetDomainObject.configuration.conditionCollection.splice(\n        conditionCollectionIndex,\n        1\n      );\n      this.persistConditions();\n    }\n  }\n\n  findConditionById(id) {\n    return this.conditions.find((condition) => condition.id === id);\n  }\n\n  reorderConditions(reorderPlan) {\n    let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);\n    let newCollection = [];\n    reorderPlan.forEach((reorderEvent) => {\n      let item = oldConditions[reorderEvent.oldIndex];\n      newCollection.push(item);\n    });\n    this.conditionSetDomainObject.configuration.conditionCollection = newCollection;\n    this.persistConditions();\n  }\n\n  getCurrentConditionLAD(conditionResults) {\n    const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;\n    let currentCondition = conditionCollection[conditionCollection.length - 1];\n\n    for (let i = 0; i < conditionCollection.length - 1; i++) {\n      if (conditionResults[conditionCollection[i].id]) {\n        //first condition to be true wins\n        currentCondition = conditionCollection[i];\n        break;\n      }\n    }\n\n    return currentCondition;\n  }\n\n  async requestLADConditionSetOutput(options) {\n    if (!this.conditions.length) {\n      return [];\n    }\n\n    await this.compositionLoad;\n\n    let latestTimestamp;\n    let conditionResults = {};\n    let nextLegOptions = { ...options };\n    delete nextLegOptions.onPartialResponse;\n\n    const results = await Promise.all(\n      this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))\n    );\n\n    results.forEach((resultObj) => {\n      const {\n        id,\n        data,\n        data: { result }\n      } = resultObj;\n\n      if (this.findConditionById(id)) {\n        conditionResults[id] = Boolean(result);\n      }\n\n      latestTimestamp = getLatestTimestamp(\n        latestTimestamp,\n        data,\n        this.timeSystems,\n        this.openmct.time.getTimeSystem()\n      );\n    });\n\n    if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {\n      return [];\n    }\n\n    const currentCondition = this.getCurrentConditionLAD(conditionResults);\n    const currentOutput = {\n      output: currentCondition.configuration.output,\n      id: this.conditionSetDomainObject.identifier,\n      conditionId: currentCondition.id,\n      ...latestTimestamp\n    };\n\n    return [currentOutput];\n  }\n\n  isTelemetryUsed(endpoint) {\n    const id = this.openmct.objects.makeKeyString(endpoint.identifier);\n\n    for (let condition of this.conditions) {\n      if (condition.isTelemetryUsed(id)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  shouldEvaluateNewTelemetry(currentTimestamp) {\n    return this.openmct.time.getBounds().end >= currentTimestamp;\n  }\n\n  telemetryReceived(endpoint, data) {\n    if (!this.isTelemetryUsed(endpoint)) {\n      return;\n    }\n\n    const datum = data[0];\n\n    const normalizedDatum = this.createNormalizedDatum(datum, endpoint);\n    const timeSystemKey = this.openmct.time.getTimeSystem().key;\n    const currentTimestamp = normalizedDatum[timeSystemKey];\n    const timestamp = {};\n\n    timestamp[timeSystemKey] = currentTimestamp;\n    this.#latestDataTable.set(normalizedDatum.id, normalizedDatum);\n\n    if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {\n      const matchingCondition = this.updateConditionResults(normalizedDatum.id);\n      this.updateCurrentCondition(timestamp, matchingCondition);\n    }\n  }\n\n  updateConditionResults(keyStringForUpdatedTelemetryObject) {\n    //We want to stop when the first condition evaluates to true.\n    const matchingCondition = this.conditions.find((condition) => {\n      condition.updateResult(this.#latestDataTable, keyStringForUpdatedTelemetryObject);\n\n      return condition.result === true;\n    });\n\n    return matchingCondition;\n  }\n\n  updateCurrentCondition(timestamp, matchingCondition) {\n    const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;\n    const defaultCondition = conditionCollection[conditionCollection.length - 1];\n\n    const currentCondition = matchingCondition || defaultCondition;\n\n    this.emit(\n      'conditionSetResultUpdated',\n      Object.assign(\n        {\n          output: currentCondition.configuration.output,\n          id: this.conditionSetDomainObject.identifier,\n          conditionId: currentCondition.id\n        },\n        timestamp\n      )\n    );\n  }\n\n  getTestData(metadatum, identifier) {\n    let data = undefined;\n    if (this.testData.applied) {\n      const found = this.testData.conditionTestInputs.find(\n        (testInput) =>\n          testInput.metadata === metadatum.source &&\n          this.openmct.objects.areIdsEqual(testInput.telemetry, identifier)\n      );\n      if (found) {\n        data = found.value;\n      }\n    }\n\n    return data;\n  }\n\n  createNormalizedDatum(telemetryDatum, endpoint) {\n    const id = this.openmct.objects.makeKeyString(endpoint.identifier);\n    const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;\n\n    const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {\n      const testValue = this.getTestData(metadatum, endpoint.identifier);\n      const formatter = this.openmct.telemetry.getValueFormatter(metadatum);\n      datum[metadatum.key] =\n        testValue !== undefined\n          ? formatter.parse(testValue)\n          : formatter.parse(telemetryDatum[metadatum.source]);\n\n      return datum;\n    }, {});\n\n    normalizedDatum.id = id;\n\n    return normalizedDatum;\n  }\n\n  updateTestData(testData) {\n    if (!_.isEqual(testData, this.testData)) {\n      this.testData = JSON.parse(JSON.stringify(testData));\n      this.openmct.objects.mutate(\n        this.conditionSetDomainObject,\n        'configuration.conditionTestData',\n        this.testData.conditionTestInputs\n      );\n    }\n  }\n\n  persistConditions() {\n    this.openmct.objects.mutate(\n      this.conditionSetDomainObject,\n      'configuration.conditionCollection',\n      this.conditionSetDomainObject.configuration.conditionCollection\n    );\n  }\n\n  destroy() {\n    this.composition.off('add', this.subscribeToTelemetry, this);\n    this.composition.off('remove', this.unsubscribeFromTelemetry, this);\n    Object.values(this.telemetryCollections).forEach((telemetryCollection) =>\n      telemetryCollection.destroy()\n    );\n\n    this.conditions.forEach((condition) => {\n      condition.destroy();\n    });\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionManagerSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ConditionManager from './ConditionManager.js';\n\ndescribe('ConditionManager', () => {\n  let conditionMgr;\n  let mockListener;\n  let openmct = {};\n  let mockDefaultCondition = {\n    isDefault: true,\n    id: '1234-5678',\n    configuration: {\n      criteria: []\n    }\n  };\n  let mockCondition1 = {\n    id: '2345-6789',\n    configuration: {\n      criteria: []\n    }\n  };\n  let updatedMockCondition1 = {\n    id: '2345-6789',\n    configuration: {\n      trigger: 'xor',\n      criteria: []\n    }\n  };\n  let mockCondition2 = {\n    id: '3456-7890',\n    configuration: {\n      criteria: []\n    }\n  };\n  let conditionSetDomainObject = {\n    identifier: {\n      namespace: '',\n      key: '600a7372-8d48-4dc4-98b6-548611b1ff7e'\n    },\n    type: 'conditionSet',\n    location: 'mine',\n    configuration: {\n      conditionCollection: [mockCondition1, mockCondition2, mockDefaultCondition]\n    }\n  };\n  let mockComposition;\n  let loader;\n  let mockTimeSystems;\n\n  function mockAngularComponents() {\n    let mockInjector = jasmine.createSpyObj('$injector', ['get']);\n\n    let mockInstantiate = jasmine.createSpy('mockInstantiate');\n    mockInstantiate.and.returnValue(mockInstantiate);\n\n    let mockDomainObject = {\n      useCapability: function () {\n        return mockDefaultCondition;\n      }\n    };\n    mockInstantiate.and.callFake(function () {\n      return mockDomainObject;\n    });\n    mockInjector.get.and.callFake(function (service) {\n      return {\n        instantiate: mockInstantiate\n      }[service];\n    });\n\n    openmct.$injector = mockInjector;\n  }\n\n  beforeEach(function () {\n    mockAngularComponents();\n    mockListener = jasmine.createSpy('mockListener');\n    loader = {};\n    loader.promise = new Promise(function (resolve, reject) {\n      loader.resolve = resolve;\n      loader.reject = reject;\n    });\n\n    mockComposition = jasmine.createSpyObj('compositionCollection', ['load', 'on', 'off']);\n    mockComposition.load.and.callFake(() => {\n      setTimeout(() => {\n        loader.resolve();\n      });\n\n      return loader.promise;\n    });\n    mockComposition.on('add', mockListener);\n    mockComposition.on('remove', mockListener);\n    openmct.composition = jasmine.createSpyObj('compositionAPI', ['get']);\n    openmct.composition.get.and.returnValue(mockComposition);\n\n    openmct.objects = jasmine.createSpyObj('objects', [\n      'get',\n      'makeKeyString',\n      'observe',\n      'mutate'\n    ]);\n    openmct.objects.get.and.returnValues(\n      new Promise(function (resolve, reject) {\n        resolve(conditionSetDomainObject);\n      }),\n      new Promise(function (resolve, reject) {\n        resolve(mockCondition1);\n      }),\n      new Promise(function (resolve, reject) {\n        resolve(mockCondition2);\n      }),\n      new Promise(function (resolve, reject) {\n        resolve(mockDefaultCondition);\n      })\n    );\n    openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key);\n    openmct.objects.observe.and.returnValue(function () {});\n    openmct.objects.mutate.and.returnValue(function () {});\n\n    mockTimeSystems = {\n      key: 'utc'\n    };\n    openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);\n    openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);\n\n    conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);\n\n    conditionMgr.on('conditionSetResultUpdated', mockListener);\n    conditionMgr.on('telemetryReceived', mockListener);\n  });\n\n  it('creates a conditionCollection with a default condition', function () {\n    expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(\n      3\n    );\n    let defaultConditionId = conditionMgr.conditions[2].id;\n    expect(defaultConditionId).toEqual(mockDefaultCondition.id);\n  });\n\n  it('reorders a conditionCollection', function () {\n    let reorderPlan = [\n      {\n        oldIndex: 1,\n        newIndex: 0\n      },\n      {\n        oldIndex: 0,\n        newIndex: 1\n      },\n      {\n        oldIndex: 2,\n        newIndex: 2\n      }\n    ];\n    conditionMgr.reorderConditions(reorderPlan);\n    expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(\n      3\n    );\n    expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(\n      mockCondition2.id\n    );\n    expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[1].id).toEqual(\n      mockCondition1.id\n    );\n  });\n\n  it('updates the right condition after reorder', function () {\n    let reorderPlan = [\n      {\n        oldIndex: 1,\n        newIndex: 0\n      },\n      {\n        oldIndex: 0,\n        newIndex: 1\n      },\n      {\n        oldIndex: 2,\n        newIndex: 2\n      }\n    ];\n    conditionMgr.reorderConditions(reorderPlan);\n    conditionMgr.updateCondition(updatedMockCondition1);\n    expect(conditionMgr.conditions[1].trigger).toEqual(updatedMockCondition1.configuration.trigger);\n  });\n\n  it('removes the right condition after reorder', function () {\n    let reorderPlan = [\n      {\n        oldIndex: 1,\n        newIndex: 0\n      },\n      {\n        oldIndex: 0,\n        newIndex: 1\n      },\n      {\n        oldIndex: 2,\n        newIndex: 2\n      }\n    ];\n    conditionMgr.reorderConditions(reorderPlan);\n    conditionMgr.removeCondition(mockCondition1.id);\n    expect(conditionMgr.conditions.length).toEqual(2);\n    expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(\n      mockCondition2.id\n    );\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/ConditionSetCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function ConditionSetCompositionPolicy(openmct) {\n  return {\n    allow: function (parent, child) {\n      if (parent.type === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) {\n        return false;\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionSetCompositionPolicySpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy.js';\n\ndescribe('ConditionSetCompositionPolicy', () => {\n  let policy;\n  let testTelemetryObject;\n  let openmct = {};\n  let parentDomainObject;\n\n  beforeAll(function () {\n    testTelemetryObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-object'\n      },\n      type: 'test-object',\n      name: 'Test Object',\n      telemetry: {\n        values: [\n          {\n            key: 'some-key',\n            name: 'Some attribute',\n            hints: {\n              domain: 1\n            }\n          },\n          {\n            key: 'some-other-key',\n            name: 'Another attribute',\n            hints: {\n              range: 1\n            }\n          }\n        ]\n      }\n    };\n    openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);\n    openmct.objects.get.and.returnValue(testTelemetryObject);\n    openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);\n    openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']);\n    policy = new ConditionSetCompositionPolicy(openmct);\n    parentDomainObject = {};\n  });\n\n  it('returns true for object types that are not conditionSets', function () {\n    parentDomainObject.type = 'random';\n    openmct.telemetry.isTelemetryObject.and.returnValue(false);\n    expect(policy.allow(parentDomainObject, {})).toBe(true);\n  });\n\n  it('returns false for object types that are not telemetry objects when parent is a conditionSet', function () {\n    parentDomainObject.type = 'conditionSet';\n    openmct.telemetry.isTelemetryObject.and.returnValue(false);\n    expect(policy.allow(parentDomainObject, {})).toBe(false);\n  });\n\n  it('returns true for object types that are telemetry objects when parent is a conditionSet', function () {\n    parentDomainObject.type = 'conditionSet';\n    openmct.telemetry.isTelemetryObject.and.returnValue(true);\n    expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true);\n  });\n\n  it('returns true for object types that are telemetry objects when parent is not a conditionSet', function () {\n    parentDomainObject.type = 'random';\n    openmct.telemetry.isTelemetryObject.and.returnValue(true);\n    expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/ConditionSetMetadataProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class ConditionSetMetadataProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  supportsMetadata(domainObject) {\n    return domainObject.type === 'conditionSet';\n  }\n\n  getDomains(domainObject) {\n    return this.openmct.time.getAllTimeSystems().map(function (ts, i) {\n      return {\n        key: ts.key,\n        name: ts.name,\n        format: ts.timeFormat,\n        hints: {\n          domain: i\n        }\n      };\n    });\n  }\n\n  getMetadata(domainObject) {\n    const enumerations = domainObject.configuration.conditionCollection.map((condition, index) => {\n      return {\n        string: condition.configuration.output,\n        value: index\n      };\n    });\n\n    return {\n      values: this.getDomains().concat([\n        {\n          key: 'state',\n          source: 'output',\n          name: 'State',\n          format: 'enum',\n          enumerations: enumerations,\n          hints: {\n            range: 1\n          }\n        },\n        {\n          key: 'output',\n          name: 'Value',\n          format: 'string',\n          hints: {\n            range: 2\n          }\n        }\n      ])\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionSetTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ConditionManager from './ConditionManager.js';\n\nexport default class ConditionSetTelemetryProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.conditionManagerPool = {};\n  }\n\n  isTelemetryObject(domainObject) {\n    return domainObject.type === 'conditionSet';\n  }\n\n  supportsRequest(domainObject) {\n    return domainObject.type === 'conditionSet';\n  }\n\n  supportsSubscribe(domainObject) {\n    return domainObject.type === 'conditionSet';\n  }\n\n  async request(domainObject, options) {\n    let conditionManager = this.getConditionManager(domainObject);\n    let latestOutput = await conditionManager.requestLADConditionSetOutput(options);\n    return latestOutput;\n  }\n\n  subscribe(domainObject, callback) {\n    let conditionManager = this.getConditionManager(domainObject);\n\n    conditionManager.on('conditionSetResultUpdated', (data) => {\n      callback(data);\n    });\n\n    return this.destroyConditionManager.bind(\n      this,\n      this.openmct.objects.makeKeyString(domainObject.identifier)\n    );\n  }\n\n  /**\n   * returns conditionManager instance for corresponding domain object\n   * creates the instance if it is not yet created\n   * @private\n   */\n  getConditionManager(domainObject) {\n    const id = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n    if (!this.conditionManagerPool[id]) {\n      this.conditionManagerPool[id] = new ConditionManager(domainObject, this.openmct);\n    }\n\n    return this.conditionManagerPool[id];\n  }\n\n  /**\n   * cleans up and destroys conditionManager instance for corresponding domain object id\n   * can be called manually for views that only request but do not subscribe to data\n   */\n  destroyConditionManager(id) {\n    if (this.conditionManagerPool[id]) {\n      this.conditionManagerPool[id].off('conditionSetResultUpdated');\n      this.conditionManagerPool[id].destroy();\n      delete this.conditionManagerPool[id];\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionSetViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport ConditionSet from './components/ConditionSet.vue';\n\nexport default class ConditionSetViewProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.name = 'Conditions View';\n    this.key = 'conditionSet.view';\n    this.cssClass = 'icon-conditional';\n  }\n\n  canView(domainObject, objectPath) {\n    const isConditionSet = domainObject.type === 'conditionSet';\n\n    return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);\n  }\n\n  canEdit(domainObject, objectPath) {\n    const isConditionSet = domainObject.type === 'conditionSet';\n\n    return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);\n  }\n\n  view(domainObject, objectPath) {\n    let _destroy = null;\n    let component = null;\n\n    return {\n      show: (container, isEditing) => {\n        const { vNode, destroy } = mount(\n          {\n            el: container,\n            components: {\n              ConditionSet\n            },\n            provide: {\n              openmct: this.openmct,\n              domainObject,\n              objectPath\n            },\n            data() {\n              return {\n                isEditing\n              };\n            },\n            template: '<condition-set :isEditing=\"isEditing\"></condition-set>'\n          },\n          {\n            app: this.openmct.app,\n            element: container\n          }\n        );\n        _destroy = destroy;\n        component = vNode.componentInstance;\n      },\n      onEditModeChange: (isEditing) => {\n        component.isEditing = isEditing;\n      },\n      destroy: () => {\n        if (_destroy) {\n          _destroy();\n        }\n        component = null;\n      }\n    };\n  }\n\n  priority(domainObject) {\n    if (domainObject.type === 'conditionSet') {\n      return this.openmct.priority.HIGHEST;\n    } else {\n      return this.openmct.priority.DEFAULT;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/ConditionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Condition from './Condition.js';\nimport TelemetryCriterion from './criterion/TelemetryCriterion.js';\nimport { TRIGGER } from './utils/constants.js';\n\nlet openmct = {};\nlet testConditionDefinition;\nlet testTelemetryObject;\nlet conditionObj;\nlet conditionManager;\nlet mockTelemetryReceived;\nlet mockTimeSystems;\n\ndescribe('The condition', function () {\n  beforeEach(() => {\n    conditionManager = jasmine.createSpyObj('conditionManager', [\n      'on',\n      'updateConditionDescription'\n    ]);\n    mockTelemetryReceived = jasmine.createSpy('listener');\n    conditionManager.on('telemetryReceived', mockTelemetryReceived);\n    conditionManager.updateConditionDescription.and.returnValue(function () {});\n\n    testTelemetryObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-object'\n      },\n      type: 'test-object',\n      name: 'Test Object',\n      telemetry: {\n        valueMetadatas: [\n          {\n            key: 'some-key',\n            source: 'some-key',\n            name: 'Some attribute',\n            hints: {\n              range: 2\n            }\n          },\n          {\n            key: 'utc',\n            source: 'utc',\n            name: 'Time',\n            format: 'utc',\n            hints: {\n              domain: 1\n            }\n          },\n          {\n            key: 'value',\n            source: 'value',\n            name: 'Test',\n            format: 'string'\n          }\n        ]\n      }\n    };\n    conditionManager.telemetryObjects = {\n      'test-object': testTelemetryObject\n    };\n    openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);\n    openmct.objects.get.and.returnValue(\n      new Promise(function (resolve, reject) {\n        resolve(testTelemetryObject);\n      })\n    );\n    openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);\n    openmct.telemetry = jasmine.createSpyObj('telemetry', [\n      'isTelemetryObject',\n      'subscribe',\n      'getMetadata',\n      'getValueFormatter'\n    ]);\n    openmct.telemetry.isTelemetryObject.and.returnValue(true);\n    openmct.telemetry.subscribe.and.returnValue(function () {});\n    openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);\n    openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {\n      return {\n        parse(input) {\n          return input;\n        }\n      };\n    });\n\n    mockTimeSystems = {\n      key: 'utc'\n    };\n    openmct.time = jasmine.createSpyObj('time', [\n      'getTimeSystem',\n      'getAllTimeSystems',\n      'on',\n      'off'\n    ]);\n    openmct.time.getTimeSystem.and.returnValue({ key: 'utc' });\n    openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);\n    //openmct.time.getTimeSystem.and.returnValue();\n    openmct.time.on.and.returnValue(() => {});\n    openmct.time.off.and.returnValue(() => {});\n\n    testConditionDefinition = {\n      id: '123-456',\n      configuration: {\n        name: 'mock condition',\n        output: 'mock output',\n        trigger: TRIGGER.ANY,\n        criteria: [\n          {\n            id: '1234-5678-9999-0000',\n            operation: 'equalTo',\n            input: ['0'],\n            metadata: 'value',\n            telemetry: testTelemetryObject.identifier\n          }\n        ]\n      }\n    };\n\n    conditionObj = new Condition(testConditionDefinition, openmct, conditionManager);\n  });\n\n  it('generates criteria with the correct properties', function () {\n    const testCriterion = testConditionDefinition.configuration.criteria[0];\n    let criterion = conditionObj.generateCriterion(testCriterion);\n    expect(criterion.id).toBeDefined();\n    expect(criterion.operation).toEqual(testCriterion.operation);\n    expect(criterion.input).toEqual(testCriterion.input);\n    expect(criterion.metadata).toEqual(testCriterion.metadata);\n    expect(criterion.telemetry).toEqual(testCriterion.telemetry);\n  });\n\n  it('initializes with an id', function () {\n    expect(conditionObj.id).toBeDefined();\n  });\n\n  it('initializes with criteria from the condition definition', function () {\n    expect(conditionObj.criteria.length).toEqual(1);\n    let criterion = conditionObj.criteria[0];\n    expect(criterion instanceof TelemetryCriterion).toBeTrue();\n    expect(criterion.operator).toEqual(testConditionDefinition.configuration.criteria[0].operator);\n    expect(criterion.input).toEqual(testConditionDefinition.configuration.criteria[0].input);\n    expect(criterion.metadata).toEqual(testConditionDefinition.configuration.criteria[0].metadata);\n  });\n\n  it('initializes with the trigger from the condition definition', function () {\n    expect(conditionObj.trigger).toEqual(testConditionDefinition.configuration.trigger);\n  });\n\n  it('destroys all criteria for a condition', function () {\n    const result = conditionObj.destroyCriteria();\n    expect(result).toBeTrue();\n    expect(conditionObj.criteria.length).toEqual(0);\n  });\n\n  it('keeps the old result new telemetry data is not used by it', function () {\n    const latestDataTable = new Map();\n    latestDataTable.set(testTelemetryObject.identifier.key, {\n      value: '0',\n      utc: 'Hi',\n      id: testTelemetryObject.identifier.key\n    });\n    conditionObj.updateResult(latestDataTable, testTelemetryObject.identifier.key);\n\n    expect(conditionObj.result).toBeTrue();\n\n    latestDataTable.set('1234', {\n      value: '1',\n      utc: 'Hi',\n      id: '1234'\n    });\n\n    conditionObj.updateResult(latestDataTable, '1234');\n    expect(conditionObj.result).toBeTrue();\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/StyleRuleManager.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nexport default class StyleRuleManager extends EventEmitter {\n  constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {\n    super();\n    this.openmct = openmct;\n    this.callback = callback;\n    this.refreshData = this.refreshData.bind(this);\n    this.toggleSubscription = this.toggleSubscription.bind(this);\n    if (suppressSubscriptionOnEdit) {\n      this.openmct.editor.on('isEditing', this.toggleSubscription);\n      this.isEditing = this.openmct.editor.editing;\n    }\n\n    if (styleConfiguration) {\n      // We don't set the selectedConditionId here because we want condition set computation to happen before we apply any selected style\n      const styleConfigurationWithNoSelection = Object.assign(styleConfiguration, {\n        selectedConditionId: ''\n      });\n      this.initialize(styleConfigurationWithNoSelection);\n      if (styleConfiguration.conditionSetIdentifier) {\n        this.openmct.time.on('boundsChanged', this.refreshData);\n        this.subscribeToConditionSet();\n      } else {\n        this.applyStaticStyle();\n      }\n    }\n  }\n\n  toggleSubscription(isEditing) {\n    this.isEditing = isEditing;\n    if (this.isEditing) {\n      if (this.stopProvidingTelemetry) {\n        this.stopProvidingTelemetry();\n        delete this.stopProvidingTelemetry;\n      }\n\n      if (this.conditionSetIdentifier) {\n        this.applySelectedConditionStyle();\n      }\n    } else if (this.conditionSetIdentifier) {\n      //reset the selected style and let the condition set output determine what it should be\n      this.selectedConditionId = undefined;\n      this.currentStyle = undefined;\n      this.updateDomainObjectStyle();\n      this.subscribeToConditionSet();\n    }\n  }\n\n  initialize(styleConfiguration) {\n    this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;\n    this.selectedConditionId = styleConfiguration.selectedConditionId;\n    this.staticStyle = styleConfiguration.staticStyle;\n    this.defaultConditionId = styleConfiguration.defaultConditionId;\n    this.updateConditionStylesMap(styleConfiguration.styles || []);\n  }\n\n  subscribeToConditionSet() {\n    if (this.stopProvidingTelemetry) {\n      this.stopProvidingTelemetry();\n      delete this.stopProvidingTelemetry;\n    }\n\n    this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {\n      this.openmct.telemetry.request(conditionSetDomainObject).then((output) => {\n        if (\n          output &&\n          output.length &&\n          this.conditionSetIdentifier &&\n          this.openmct.objects.areIdsEqual(\n            conditionSetDomainObject.identifier,\n            this.conditionSetIdentifier\n          )\n        ) {\n          this.handleConditionSetResultUpdated(output[0]);\n        }\n      });\n      if (\n        this.conditionSetIdentifier &&\n        this.openmct.objects.areIdsEqual(\n          conditionSetDomainObject.identifier,\n          this.conditionSetIdentifier\n        )\n      ) {\n        this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(\n          conditionSetDomainObject,\n          this.handleConditionSetResultUpdated.bind(this)\n        );\n      }\n    });\n  }\n\n  refreshData(bounds, isTick) {\n    if (!isTick) {\n      let options = {\n        start: bounds.start,\n        end: bounds.end,\n        size: 1,\n        strategy: 'latest'\n      };\n      this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {\n        this.openmct.telemetry.request(conditionSetDomainObject, options).then((output) => {\n          if (output && output.length) {\n            this.handleConditionSetResultUpdated(output[0]);\n          }\n        });\n      });\n    }\n  }\n\n  updateObjectStyleConfig(styleConfiguration) {\n    if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {\n      this.initialize(styleConfiguration || {});\n      this.applyStaticStyle();\n      this.destroy(true);\n    } else {\n      let isNewConditionSet =\n        !this.conditionSetIdentifier ||\n        !this.openmct.objects.areIdsEqual(\n          this.conditionSetIdentifier,\n          styleConfiguration.conditionSetIdentifier\n        );\n      this.initialize(styleConfiguration);\n      if (this.isEditing) {\n        this.applySelectedConditionStyle();\n      } else {\n        //Only resubscribe if the conditionSet has changed.\n        if (isNewConditionSet) {\n          this.subscribeToConditionSet();\n        }\n      }\n    }\n  }\n\n  updateConditionStylesMap(conditionStyles) {\n    let conditionStyleMap = {};\n    conditionStyles.forEach((conditionStyle) => {\n      if (conditionStyle.conditionId) {\n        conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style;\n      } else {\n        conditionStyleMap.static = conditionStyle.style;\n      }\n    });\n    this.conditionalStyleMap = conditionStyleMap;\n  }\n\n  handleConditionSetResultUpdated(resultData) {\n    let foundStyle = this.conditionalStyleMap[resultData.conditionId];\n    if (foundStyle) {\n      if (foundStyle !== this.currentStyle) {\n        this.currentStyle = foundStyle;\n      }\n\n      this.updateDomainObjectStyle();\n    } else {\n      this.applyStaticStyle();\n    }\n  }\n\n  updateDomainObjectStyle() {\n    if (this.callback) {\n      this.callback(Object.assign({}, this.currentStyle));\n    }\n  }\n\n  applySelectedConditionStyle() {\n    const conditionId = this.selectedConditionId || this.defaultConditionId;\n    if (!conditionId) {\n      this.applyStaticStyle();\n    } else if (this.conditionalStyleMap[conditionId]) {\n      this.currentStyle = this.conditionalStyleMap[conditionId];\n      this.updateDomainObjectStyle();\n    }\n  }\n\n  applyStaticStyle() {\n    if (this.staticStyle) {\n      this.currentStyle = this.staticStyle.style;\n    } else {\n      if (this.currentStyle) {\n        Object.keys(this.currentStyle).forEach((key) => {\n          this.currentStyle[key] = '__no_value';\n        });\n      }\n    }\n\n    this.updateDomainObjectStyle();\n  }\n\n  destroy(skipEventListeners) {\n    if (this.stopProvidingTelemetry) {\n      this.stopProvidingTelemetry();\n      delete this.stopProvidingTelemetry;\n    }\n\n    if (!skipEventListeners) {\n      this.openmct.time.off('boundsChanged', this.refreshData);\n      this.openmct.editor.off('isEditing', this.toggleSubscription);\n    }\n\n    this.conditionSetIdentifier = undefined;\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/components/ConditionCollection.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <section\n    id=\"conditionCollection\"\n    :class=\"{ 'is-expanded': expanded }\"\n    aria-label=\"Condition Set Condition Collection\"\n  >\n    <div class=\"c-cs__header c-section__header\">\n      <button\n        class=\"c-disclosure-triangle c-tree__item__view-control is-enabled\"\n        :class=\"{ 'c-disclosure-triangle--expanded': expanded }\"\n        :aria-expanded=\"expanded\"\n        aria-controls=\"conditionContent\"\n        @click=\"toggleExpanded\"\n      ></button>\n      <div class=\"c-cs__header-label c-section__label\">Conditions</div>\n    </div>\n    <div v-if=\"expanded\" id=\"conditionContent\" class=\"c-cs__content\">\n      <div\n        v-show=\"isEditing\"\n        class=\"hint\"\n        :class=\"{ 's-status-icon-warning-lo': !telemetryObjs.length }\"\n      >\n        <template v-if=\"!telemetryObjs.length\"\n          >Drag telemetry into this Condition Set to configure Conditions and add\n          criteria.</template\n        >\n        <template v-else\n          >The first condition to match is the one that is applied. Drag conditions to\n          reorder.</template\n        >\n      </div>\n\n      <button\n        v-show=\"isEditing\"\n        id=\"addCondition\"\n        class=\"c-button c-button--major icon-plus labeled\"\n        aria-labelledby=\"addConditionButtonLabel\"\n        @click=\"addCondition\"\n      >\n        <span id=\"addConditionButtonLabel\" class=\"c-cs-button__label\">Add Condition</span>\n      </button>\n\n      <div class=\"c-cs__conditions-h\" :class=\"{ 'is-active-dragging': isDragging }\">\n        <Condition\n          v-for=\"(condition, index) in conditionCollection\"\n          :key=\"condition.id\"\n          :condition=\"condition\"\n          :current-condition-id=\"currentConditionId\"\n          :condition-index=\"index\"\n          :telemetry=\"telemetryObjs\"\n          :is-editing=\"isEditing\"\n          :move-index=\"moveIndex\"\n          :is-dragging=\"isDragging\"\n          @update-condition=\"updateCondition\"\n          @remove-condition=\"removeCondition\"\n          @clone-condition=\"cloneCondition\"\n          @set-move-index=\"setMoveIndex\"\n          @drag-complete=\"dragComplete\"\n          @drop-condition=\"dropCondition\"\n        />\n      </div>\n    </div>\n  </section>\n</template>\n\n<script>\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport ConditionManager from '../ConditionManager.js';\nimport Condition from './ConditionItem.vue';\n\nexport default {\n  components: {\n    Condition\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'domainObject'],\n  props: {\n    isEditing: Boolean,\n    testData: {\n      type: Object,\n      required: true,\n      default: () => {\n        return {\n          applied: false,\n          conditionTestInputs: []\n        };\n      }\n    }\n  },\n  emits: [\n    'condition-set-result-updated',\n    'no-telemetry-objects',\n    'telemetry-updated',\n    'telemetry-staleness'\n  ],\n  data() {\n    return {\n      expanded: true,\n      conditionCollection: [],\n      conditionResults: {},\n      conditions: [],\n      telemetryObjs: [],\n      moveIndex: undefined,\n      isDragging: false,\n      dragCounter: 0,\n      currentConditionId: ''\n    };\n  },\n  watch: {\n    testData: {\n      handler() {\n        this.updateTestData();\n      },\n      deep: true\n    }\n  },\n  unmounted() {\n    this.composition.off('add', this.addTelemetryObject);\n    this.composition.off('remove', this.removeTelemetryObject);\n    if (this.conditionManager) {\n      this.conditionManager.off('conditionSetResultUpdated', this.handleConditionSetResultUpdated);\n      this.conditionManager.off('noTelemetryObjects', this.emitNoTelemetryObjectEvent);\n      this.conditionManager.destroy();\n    }\n\n    if (this.stopObservingForChanges) {\n      this.stopObservingForChanges();\n    }\n  },\n  mounted() {\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('add', this.addTelemetryObject);\n    this.composition.on('remove', this.removeTelemetryObject);\n    this.composition.load();\n    this.conditionCollection = this.domainObject.configuration.conditionCollection;\n    this.observeForChanges();\n    this.conditionManager = new ConditionManager(this.domainObject, this.openmct);\n    this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);\n    this.conditionManager.on('noTelemetryObjects', this.emitNoTelemetryObjectEvent);\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject, () => {\n        this.emitStaleness({\n          keyString: domainObject.identifier,\n          stalenessResponse: { isStale: false }\n        });\n      });\n      this.subscribeToStaleness(domainObject, (stalenessResponse) => {\n        this.emitStaleness({\n          keyString: domainObject.identifier,\n          stalenessResponse: stalenessResponse\n        });\n      });\n    });\n  },\n  methods: {\n    handleConditionSetResultUpdated(data) {\n      this.currentConditionId = data.conditionId;\n      this.$emit('condition-set-result-updated', data);\n    },\n    emitNoTelemetryObjectEvent(data) {\n      this.currentConditionId = '';\n      this.$emit('no-telemetry-objects');\n    },\n    observeForChanges() {\n      this.stopObservingForChanges = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.conditionCollection',\n        (newConditionCollection) => {\n          //this forces children to re-render\n          this.conditionCollection = newConditionCollection.map((condition) => condition);\n        }\n      );\n    },\n    setMoveIndex(index) {\n      this.moveIndex = index;\n      this.isDragging = true;\n    },\n    dropCondition(targetIndex) {\n      const oldIndexArr = Object.keys(this.conditionCollection);\n      const newIndexArr = this.rearrangeIndices(oldIndexArr, this.moveIndex, targetIndex);\n      const reorderPlan = [];\n\n      for (let i = 0; i < oldIndexArr.length; i++) {\n        reorderPlan.push({\n          oldIndex: Number(newIndexArr[i]),\n          newIndex: i\n        });\n      }\n\n      this.reorder(reorderPlan);\n    },\n    dragComplete() {\n      this.isDragging = false;\n    },\n    rearrangeIndices(arr, old_index, new_index) {\n      while (old_index < 0) {\n        old_index += arr.length;\n      }\n\n      while (new_index < 0) {\n        new_index += arr.length;\n      }\n\n      if (new_index >= arr.length) {\n        let k = new_index - arr.length;\n        while (k-- + 1) {\n          arr.push(undefined);\n        }\n      }\n\n      arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);\n\n      return arr;\n    },\n    addTelemetryObject(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n      this.telemetryObjs.push(domainObject);\n      this.$emit('telemetry-updated', this.telemetryObjs);\n\n      this.subscribeToStaleness(domainObject, (stalenessResponse) => {\n        this.emitStaleness({\n          keyString,\n          stalenessResponse: stalenessResponse\n        });\n      });\n    },\n    removeTelemetryObject(identifier) {\n      const keyString = this.openmct.objects.makeKeyString(identifier);\n      const index = this.telemetryObjs.findIndex((obj) => {\n        let objId = this.openmct.objects.makeKeyString(obj.identifier);\n\n        return objId === keyString;\n      });\n\n      const domainObject = this.telemetryObjs[index];\n      this.triggerUnsubscribeFromStaleness(domainObject, () => {\n        this.emitStaleness({\n          keyString,\n          stalenessResponse: { isStale: false }\n        });\n      });\n\n      if (index > -1) {\n        this.telemetryObjs.splice(index, 1);\n      }\n    },\n    emitStaleness(stalenessObject) {\n      this.$emit('telemetry-staleness', stalenessObject);\n    },\n    addCondition() {\n      this.conditionManager.addCondition();\n    },\n    updateCondition(data) {\n      this.conditionManager.updateCondition(data.condition);\n    },\n    removeCondition(id) {\n      this.conditionManager.removeCondition(id);\n    },\n    reorder(reorderPlan) {\n      this.conditionManager.reorderConditions(reorderPlan);\n    },\n    cloneCondition(data) {\n      this.conditionManager.cloneCondition(data.condition, data.index);\n    },\n    updateTestData() {\n      this.conditionManager.updateTestData(this.testData);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/ConditionDescription.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-style__condition-desc\">\n    <span v-if=\"showLabel && condition\" class=\"c-style__condition-desc__name c-condition__name\">\n      {{ condition.configuration.name }}\n    </span>\n    <span v-if=\"!condition.isDefault\" class=\"c-style__condition-desc__text\">\n      {{ description }}\n    </span>\n    <span v-else class=\"c-style__condition-desc__text\">\n      Match if no other condition is matched\n    </span>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'ConditionDescription',\n  inject: ['openmct'],\n  props: {\n    showLabel: {\n      type: Boolean,\n      default: false\n    },\n    condition: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  computed: {\n    description() {\n      return this.condition ? this.condition.summary : '';\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/ConditionError.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div v-if=\"conditionErrors.length\" class=\"c-condition__errors\">\n    <div\n      v-for=\"(error, index) in conditionErrors\"\n      :key=\"index\"\n      class=\"u-alert u-alert--block u-alert--with-icon\"\n    >\n      {{ error.message.errorText }} {{ error.additionalInfo }}\n    </div>\n  </div>\n</template>\n\n<script>\nimport { ERROR } from '@/plugins/condition/utils/constants';\n\nexport default {\n  name: 'ConditionError',\n  inject: ['openmct'],\n  props: {\n    condition: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  data() {\n    return {\n      conditionErrors: []\n    };\n  },\n  mounted() {\n    this.getConditionErrors();\n  },\n  methods: {\n    getConditionErrors() {\n      if (this.condition) {\n        this.condition.configuration.criteria.forEach((criterion, index) => {\n          this.getCriterionErrors(criterion, index);\n        });\n      }\n    },\n    getCriterionErrors(criterion, index) {\n      //It is sufficient to check for absence of telemetry here since the condition manager ensures that telemetry for a criterion is set if it exists\n      const isInvalidTelemetry =\n        !criterion.telemetry && criterion.telemetry !== 'all' && criterion.telemetry !== 'any';\n      if (isInvalidTelemetry) {\n        this.conditionErrors.push({\n          message: ERROR.TELEMETRY_NOT_FOUND,\n          additionalInfo: ''\n        });\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/ConditionItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-condition-h\"\n    :class=\"{ 'is-drag-target': draggingOver }\"\n    :aria-label=\"conditionSetLabel\"\n    @dragover.prevent\n    @drop.prevent=\"dropCondition($event, conditionIndex)\"\n    @dragenter=\"dragEnter($event, conditionIndex)\"\n    @dragleave=\"dragLeave($event, conditionIndex)\"\n  >\n    <div class=\"c-condition-h__drop-target\"></div>\n    <div\n      v-if=\"isEditing\"\n      :class=\"{ 'is-current': condition.id === currentConditionId }\"\n      class=\"c-condition c-condition--edit\"\n    >\n      <!-- Edit view -->\n      <div class=\"c-condition__header\">\n        <span\n          class=\"c-condition__drag-grippy c-grippy c-grippy--vertical-drag\"\n          title=\"Drag to reorder conditions\"\n          :class=\"[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]\"\n          :draggable=\"!condition.isDefault\"\n          @dragstart=\"dragStart\"\n          @dragend=\"dragEnd\"\n        ></span>\n\n        <span\n          class=\"c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled\"\n          :class=\"{ 'c-disclosure-triangle--expanded': expanded }\"\n          @click=\"expanded = !expanded\"\n        ></span>\n\n        <span class=\"c-condition__name\" aria-label=\"Condition Name Label\">{{\n          condition.configuration.name\n        }}</span>\n        <span class=\"c-condition__summary\">\n          <template v-if=\"!condition.isDefault && !canEvaluateCriteria\"> Define criteria </template>\n          <span v-else>\n            <ConditionDescription :show-label=\"false\" :condition=\"condition\" />\n          </span>\n        </span>\n\n        <div class=\"c-condition__buttons\">\n          <button\n            v-if=\"!condition.isDefault\"\n            class=\"c-click-icon c-condition__duplicate-button icon-duplicate\"\n            title=\"Duplicate this condition\"\n            @click=\"cloneCondition\"\n          ></button>\n\n          <button\n            v-if=\"!condition.isDefault\"\n            class=\"c-click-icon c-condition__delete-button icon-trash\"\n            title=\"Delete this condition\"\n            @click=\"removeCondition\"\n          ></button>\n        </div>\n      </div>\n      <div v-if=\"expanded\" class=\"c-condition__definition c-cdef\">\n        <span class=\"c-cdef__separator c-row-separator\"></span>\n        <span class=\"c-cdef__label\">Condition Name</span>\n        <span class=\"c-cdef__controls\">\n          <input\n            v-model=\"condition.configuration.name\"\n            class=\"t-condition-input__name\"\n            aria-label=\"Condition Name Input\"\n            type=\"text\"\n            @change=\"persist\"\n          />\n        </span>\n\n        <span class=\"c-cdef__label\">Output</span>\n        <span class=\"c-cdef__controls\">\n          <span class=\"c-cdef__control\">\n            <select\n              v-model=\"selectedOutputSelection\"\n              aria-label=\"Condition Output Type\"\n              @change=\"setOutputValue\"\n            >\n              <option v-for=\"option in outputOptions\" :key=\"option\" :value=\"option\">\n                {{ initCap(option) }}\n              </option>\n            </select>\n          </span>\n          <span class=\"c-cdef__control\">\n            <input\n              v-if=\"selectedOutputSelection === outputOptions[2]\"\n              v-model=\"condition.configuration.output\"\n              aria-label=\"Condition Output String\"\n              class=\"t-condition-name-input\"\n              type=\"text\"\n              @change=\"persist\"\n            />\n          </span>\n        </span>\n\n        <div v-if=\"!condition.isDefault\" class=\"c-cdef__match-and-criteria\">\n          <span class=\"c-cdef__separator c-row-separator\"></span>\n          <span class=\"c-cdef__label\">Match</span>\n          <span class=\"c-cdef__controls\">\n            <select\n              v-model=\"condition.configuration.trigger\"\n              aria-label=\"Condition Trigger\"\n              @change=\"persist\"\n            >\n              <option v-for=\"option in triggers\" :key=\"option.value\" :value=\"option.value\">\n                {{ option.label }}\n              </option>\n            </select>\n          </span>\n\n          <template v-if=\"telemetry.length || condition.configuration.criteria.length\">\n            <div\n              v-for=\"(criterion, index) in condition.configuration.criteria\"\n              :key=\"criterion.id\"\n              class=\"c-cdef__criteria\"\n            >\n              <Criterion\n                :telemetry=\"telemetry\"\n                :criterion=\"criterion\"\n                :index=\"index\"\n                :trigger=\"condition.configuration.trigger\"\n                :is-default=\"condition.configuration.criteria.length === 1\"\n                @persist=\"persist\"\n              />\n              <div class=\"c-cdef__criteria__buttons\">\n                <button\n                  class=\"c-click-icon c-cdef__criteria-duplicate-button icon-duplicate\"\n                  title=\"Duplicate this criteria\"\n                  @click=\"cloneCriterion(index)\"\n                ></button>\n                <button\n                  v-if=\"!(condition.configuration.criteria.length === 1)\"\n                  class=\"c-click-icon c-cdef__criteria-duplicate-button icon-trash\"\n                  title=\"Delete this criteria\"\n                  @click=\"removeCriterion(index)\"\n                ></button>\n              </div>\n            </div>\n          </template>\n          <div class=\"c-cdef__separator c-row-separator\"></div>\n          <div class=\"c-cdef__controls\">\n            <button\n              :disabled=\"!telemetry.length\"\n              :aria-label=\"`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`\"\n              class=\"c-cdef__add-criteria-button c-button c-button--labeled icon-plus\"\n              @click=\"addCriteria\"\n            >\n              <span class=\"c-button__label\">Add Criteria</span>\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      v-else\n      class=\"c-condition c-condition--browse\"\n      :class=\"{ 'is-current': condition.id === currentConditionId }\"\n    >\n      <!-- Browse view -->\n      <div class=\"c-condition__header\">\n        <span class=\"c-condition__name\">\n          {{ condition.configuration.name }}\n        </span>\n        <span class=\"c-condition__output\"> Output: {{ condition.configuration.output }} </span>\n      </div>\n      <div class=\"c-condition__summary\">\n        <ConditionDescription :show-label=\"false\" :condition=\"condition\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { v4 as uuid } from 'uuid';\n\nimport { TRIGGER, TRIGGER_LABEL } from '@/plugins/condition/utils/constants';\n\nimport ConditionDescription from './ConditionDescription.vue';\nimport Criterion from './CriterionItem.vue';\n\nexport default {\n  components: {\n    Criterion,\n    ConditionDescription\n  },\n  inject: ['openmct'],\n  props: {\n    currentConditionId: {\n      type: String,\n      default: ''\n    },\n    condition: {\n      type: Object,\n      required: true\n    },\n    conditionIndex: {\n      type: Number,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      required: true,\n      default: false\n    },\n    telemetry: {\n      type: Array,\n      required: true,\n      default: () => []\n    },\n    isDragging: {\n      type: Boolean,\n      default: false\n    },\n    moveIndex: {\n      type: Number,\n      default: 0\n    }\n  },\n  emits: [\n    'set-move-index',\n    'drag-complete',\n    'drop-condition',\n    'remove-condition',\n    'clone-condition',\n    'update-condition'\n  ],\n  data() {\n    return {\n      currentCriteria: this.currentCriteria,\n      expanded: true,\n      trigger: 'all',\n      selectedOutputSelection: '',\n      outputOptions: ['false', 'true', 'string'],\n      criterionIndex: 0,\n      draggingOver: false,\n      isDefault: this.condition.isDefault\n    };\n  },\n  computed: {\n    conditionSetLabel() {\n      let label;\n\n      if (this.condition.id === this.currentConditionId) {\n        label = 'Active Condition Set Condition';\n      } else {\n        label = 'Condition Set Condition';\n      }\n\n      return label;\n    },\n    triggers() {\n      const keys = Object.keys(TRIGGER);\n      const triggerOptions = [];\n      keys.forEach((trigger) => {\n        triggerOptions.push({\n          value: TRIGGER[trigger],\n          label: `when ${TRIGGER_LABEL[TRIGGER[trigger]]}`\n        });\n      });\n\n      return triggerOptions;\n    },\n    canEvaluateCriteria: function () {\n      let criteria = this.condition.configuration.criteria;\n      if (criteria.length) {\n        let lastCriterion = criteria[criteria.length - 1];\n        if (\n          lastCriterion.telemetry &&\n          lastCriterion.operation &&\n          (lastCriterion.input.length ||\n            lastCriterion.operation === 'isDefined' ||\n            lastCriterion.operation === 'isUndefined')\n        ) {\n          return true;\n        }\n      }\n\n      return false;\n    }\n  },\n  unmounted() {\n    this.destroy();\n  },\n  mounted() {\n    this.setOutputSelection();\n  },\n  methods: {\n    setOutputSelection() {\n      let conditionOutput = this.condition.configuration.output;\n      if (conditionOutput) {\n        if (conditionOutput !== 'false' && conditionOutput !== 'true') {\n          this.selectedOutputSelection = 'string';\n        } else {\n          this.selectedOutputSelection = conditionOutput;\n        }\n      }\n    },\n    setOutputValue() {\n      if (this.selectedOutputSelection === 'string') {\n        this.condition.configuration.output = '';\n      } else {\n        this.condition.configuration.output = this.selectedOutputSelection;\n      }\n\n      this.persist();\n    },\n    addCriteria() {\n      const criteriaObject = {\n        id: uuid(),\n        telemetry: '',\n        operation: '',\n        input: '',\n        metadata: ''\n      };\n      this.condition.configuration.criteria.push(criteriaObject);\n    },\n    dragStart(event) {\n      event.dataTransfer.clearData();\n      event.dataTransfer.setData('dragging', event.target); // required for FF to initiate drag\n      event.dataTransfer.effectAllowed = 'copyMove';\n      event.dataTransfer.setDragImage(event.target.closest('.c-condition-h'), 0, 0);\n      this.$emit('set-move-index', this.conditionIndex);\n    },\n    dragEnd() {\n      this.dragStarted = false;\n      this.$emit('drag-complete');\n    },\n    dropCondition(event, targetIndex) {\n      if (!this.isDragging) {\n        return;\n      }\n\n      if (targetIndex > this.moveIndex) {\n        targetIndex--;\n      } // for 'downward' move\n\n      if (this.isValidTarget(targetIndex)) {\n        this.dragElement = undefined;\n        this.draggingOver = false;\n        this.$emit('drop-condition', targetIndex);\n      }\n    },\n    dragEnter(event, targetIndex) {\n      if (!this.isDragging) {\n        return;\n      }\n\n      if (targetIndex > this.moveIndex) {\n        targetIndex--;\n      } // for 'downward' move\n\n      if (this.isValidTarget(targetIndex)) {\n        this.dragElement = event.target.parentElement;\n        this.draggingOver = true;\n      }\n    },\n    dragLeave(event) {\n      if (event.target.parentElement === this.dragElement) {\n        this.draggingOver = false;\n        this.dragElement = undefined;\n      }\n    },\n    isValidTarget(targetIndex) {\n      return this.moveIndex !== targetIndex;\n    },\n    destroy() {},\n    removeCondition() {\n      this.$emit('remove-condition', this.condition.id);\n    },\n    cloneCondition() {\n      this.$emit('clone-condition', {\n        condition: this.condition,\n        index: this.conditionIndex\n      });\n    },\n    removeCriterion(index) {\n      this.condition.configuration.criteria.splice(index, 1);\n      this.persist();\n    },\n    cloneCriterion(index) {\n      const clonedCriterion = JSON.parse(\n        JSON.stringify(this.condition.configuration.criteria[index])\n      );\n      clonedCriterion.id = uuid();\n      this.condition.configuration.criteria.splice(index + 1, 0, clonedCriterion);\n      this.persist();\n    },\n    persist() {\n      this.$emit('update-condition', {\n        condition: this.condition\n      });\n    },\n    initCap(str) {\n      return str.charAt(0).toUpperCase() + str.slice(1);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/ConditionSet.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-cs\" :class=\"{ 'is-stale': isStale }\" aria-label=\"Condition Set\">\n    <section class=\"c-cs__current-output c-section\">\n      <div class=\"c-output-featured\">\n        <span class=\"c-output-featured__label\">Current Output</span>\n        <span class=\"c-output-featured__value\" aria-label=\"Current Output Value\">\n          <template v-if=\"currentConditionOutput\">\n            {{ currentConditionOutput }}\n          </template>\n          <template v-else> --- </template>\n        </span>\n      </div>\n    </section>\n    <div class=\"c-cs__test-data-and-conditions-w\">\n      <TestData\n        class=\"c-cs__test-data\"\n        :is-editing=\"isEditing\"\n        :test-data=\"testData\"\n        :telemetry=\"telemetryObjs\"\n        @update-test-data=\"updateTestData\"\n      />\n      <ConditionCollection\n        class=\"c-cs__conditions\"\n        :is-editing=\"isEditing\"\n        :test-data=\"testData\"\n        @condition-set-result-updated=\"updateCurrentOutput\"\n        @no-telemetry-objects=\"updateCurrentOutput('---')\"\n        @telemetry-updated=\"updateTelemetry\"\n        @telemetry-staleness=\"handleStaleness\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport ConditionCollection from './ConditionCollection.vue';\nimport TestData from './TestData.vue';\n\nexport default {\n  components: {\n    TestData,\n    ConditionCollection\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'domainObject'],\n  props: {\n    isEditing: Boolean\n  },\n  data() {\n    return {\n      currentConditionOutput: '',\n      telemetryObjs: [],\n      testData: {}\n    };\n  },\n  mounted() {\n    this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.testData = {\n      applied: false,\n      conditionTestInputs: this.domainObject.configuration.conditionTestData || []\n    };\n  },\n  methods: {\n    updateCurrentOutput(currentConditionResult) {\n      this.currentConditionOutput = currentConditionResult.output;\n    },\n    updateDefaultOutput(output) {\n      this.currentConditionOutput = output;\n    },\n    updateTelemetry(telemetryObjs) {\n      this.telemetryObjs = telemetryObjs;\n    },\n    updateTestData(testData) {\n      this.testData = testData;\n    },\n    handleStaleness({ keyString, stalenessResponse }) {\n      this.addOrRemoveStaleObject(keyString, stalenessResponse);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/CriterionItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"u-contents\">\n    <div class=\"c-cdef__separator c-row-separator\"></div>\n    <span class=\"c-cdef__label\">{{ setRowLabel }}</span>\n    <span class=\"c-cdef__controls\">\n      <span class=\"c-cdef__control\">\n        <select\n          ref=\"telemetrySelect\"\n          v-model=\"criterion.telemetry\"\n          aria-label=\"Criterion Telemetry Selection\"\n          @change=\"updateMetadataOptions\"\n        >\n          <option value=\"\">- Select Telemetry -</option>\n          <option value=\"all\">all telemetry</option>\n          <option value=\"any\">any telemetry</option>\n          <option\n            v-for=\"telemetryOption in telemetry\"\n            :key=\"telemetryOption.identifier.key\"\n            :value=\"telemetryOption.identifier\"\n          >\n            {{ telemetryOption.name }}\n          </option>\n        </select>\n      </span>\n      <span v-if=\"criterion.telemetry\" class=\"c-cdef__control\">\n        <select\n          ref=\"metadataSelect\"\n          v-model=\"criterion.metadata\"\n          aria-label=\"Criterion Metadata Selection\"\n          @change=\"updateOperations\"\n        >\n          <option value=\"\">- Select Field -</option>\n          <option v-for=\"option in telemetryMetadataOptions\" :key=\"option.key\" :value=\"option.key\">\n            {{ option.name }}\n          </option>\n          <option value=\"dataReceived\">any data received</option>\n        </select>\n      </span>\n      <span v-if=\"criterion.telemetry && criterion.metadata\" class=\"c-cdef__control\">\n        <select\n          v-model=\"criterion.operation\"\n          aria-label=\"Criterion Comparison Selection\"\n          @change=\"updateInputVisibilityAndValues\"\n        >\n          <option value=\"\">- Select Comparison -</option>\n          <option v-for=\"option in filteredOps\" :key=\"option.name\" :value=\"option.name\">\n            {{ option.text }}\n          </option>\n        </select>\n        <template v-if=\"!enumerations.length\">\n          <span\n            v-for=\"(item, inputIndex) in inputCount\"\n            :key=\"inputIndex\"\n            class=\"c-cdef__control__inputs\"\n          >\n            <input\n              v-model=\"criterion.input[inputIndex]\"\n              class=\"c-cdef__control__input\"\n              aria-label=\"Criterion Input\"\n              :type=\"setInputType\"\n              @change=\"persist\"\n            />\n            <span v-if=\"inputIndex < inputCount - 1\">and</span>\n          </span>\n          <span\n            v-if=\"criterion.metadata === 'dataReceived' && criterion.operation.name === IS_OLD_KEY\"\n            >seconds</span\n          >\n        </template>\n        <span v-else>\n          <span v-if=\"inputCount && criterion.operation\" class=\"c-cdef__control\">\n            <select\n              v-model=\"criterion.input[0]\"\n              aria-label=\"Criterion Else Selection\"\n              @change=\"persist\"\n            >\n              <option\n                v-for=\"option in enumerations\"\n                :key=\"option.string\"\n                :value=\"option.value.toString()\"\n              >\n                {{ option.string }}\n              </option>\n            </select>\n          </span>\n        </span>\n      </span>\n    </span>\n  </div>\n</template>\n\n<script>\nimport { IS_OLD_KEY, IS_STALE_KEY, TRIGGER_CONJUNCTION } from '../utils/constants.js';\nimport { INPUT_TYPES, OPERATIONS } from '../utils/operations.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    criterion: {\n      type: Object,\n      required: true\n    },\n    telemetry: {\n      type: Array,\n      required: true,\n      default: () => []\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    trigger: {\n      type: String,\n      required: true\n    }\n  },\n  emits: ['persist'],\n  data() {\n    return {\n      telemetryMetadataOptions: [],\n      operations: OPERATIONS,\n      inputCount: 0,\n      rowLabel: '',\n      operationFormat: '',\n      enumerations: [],\n      inputTypes: INPUT_TYPES,\n      IS_OLD_KEY\n    };\n  },\n  computed: {\n    setRowLabel: function () {\n      let operator = TRIGGER_CONJUNCTION[this.trigger];\n\n      return (this.index !== 0 ? operator : '') + ' when';\n    },\n    filteredOps: function () {\n      if (this.criterion.metadata === 'dataReceived') {\n        return this.operations.filter((op) => op.name === IS_OLD_KEY || op.name === IS_STALE_KEY);\n      } else {\n        return this.operations.filter((op) => op.appliesTo.indexOf(this.operationFormat) !== -1);\n      }\n    },\n    setInputType: function () {\n      let type = '';\n      for (let i = 0; i < this.filteredOps.length; i++) {\n        if (this.criterion.operation === this.filteredOps[i].name) {\n          if (this.filteredOps[i].appliesTo.length) {\n            type = this.inputTypes[this.filteredOps[i].appliesTo[0]];\n          } else {\n            type = 'text';\n          }\n\n          break;\n        }\n      }\n\n      return type;\n    }\n  },\n  watch: {\n    telemetry: {\n      handler(newTelemetry, oldTelemetry) {\n        this.checkTelemetry();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.updateMetadataOptions();\n  },\n  methods: {\n    checkTelemetry() {\n      if (this.criterion.telemetry) {\n        const isAnyAllTelemetry =\n          this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all';\n        const telemetryForCriterionExists = this.telemetry.find((telemetryObj) =>\n          this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier)\n        );\n        if (!isAnyAllTelemetry && !telemetryForCriterionExists) {\n          //telemetry being used was removed. So reset this criterion.\n          this.criterion.telemetry = '';\n          this.criterion.metadata = '';\n          this.criterion.input = [];\n          this.criterion.operation = '';\n          this.persist();\n        } else {\n          this.updateMetadataOptions();\n        }\n      }\n    },\n    updateOperationFormat() {\n      this.enumerations = [];\n      let foundMetadata = this.telemetryMetadataOptions.find((value) => {\n        return value.key === this.criterion.metadata;\n      });\n      if (foundMetadata) {\n        if (foundMetadata.enumerations !== undefined) {\n          this.operationFormat = 'enum';\n          this.enumerations = foundMetadata.enumerations;\n        } else if (foundMetadata.format === 'string' || foundMetadata.format === 'number') {\n          this.operationFormat = foundMetadata.format;\n        } else if (Object.prototype.hasOwnProperty.call(foundMetadata.hints, 'range')) {\n          this.operationFormat = 'number';\n        } else if (Object.prototype.hasOwnProperty.call(foundMetadata.hints, 'domain')) {\n          this.operationFormat = 'number';\n        } else if (foundMetadata.key === 'name') {\n          this.operationFormat = 'string';\n        } else {\n          this.operationFormat = 'number';\n        }\n      } else if (this.criterion.metadata === 'dataReceived') {\n        this.operationFormat = 'number';\n      }\n\n      this.updateInputVisibilityAndValues();\n    },\n    updateMetadataOptions(ev) {\n      if (ev) {\n        this.clearDependentFields(ev.target);\n        this.persist();\n      }\n\n      if (this.criterion.telemetry) {\n        let telemetryObjects = this.telemetry;\n        if (this.criterion.telemetry !== 'all' && this.criterion.telemetry !== 'any') {\n          const found = this.telemetry.find((telemetryObj) =>\n            this.openmct.objects.areIdsEqual(telemetryObj.identifier, this.criterion.telemetry)\n          );\n          telemetryObjects = found ? [found] : [];\n        }\n\n        this.telemetryMetadataOptions = [];\n        telemetryObjects.forEach((telemetryObject) => {\n          let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);\n          this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);\n        });\n        this.updateOperations();\n      }\n    },\n    addMetaDataOptions(options) {\n      if (!this.telemetryMetadataOptions) {\n        this.telemetryMetadataOptions = options;\n      }\n\n      options.forEach((option) => {\n        const found = this.telemetryMetadataOptions.find((metadataOption) => {\n          return (\n            metadataOption.key &&\n            metadataOption.key === option.key &&\n            metadataOption.name &&\n            metadataOption.name === option.name\n          );\n        });\n        if (!found) {\n          this.telemetryMetadataOptions.push(option);\n        }\n      });\n    },\n    updateOperations(ev) {\n      this.updateOperationFormat();\n      if (ev) {\n        this.clearDependentFields(ev.target);\n        this.persist();\n      }\n    },\n    updateInputVisibilityAndValues(ev) {\n      if (ev) {\n        this.clearDependentFields();\n        this.persist();\n      }\n\n      for (let i = 0; i < this.filteredOps.length; i++) {\n        if (this.criterion.operation === this.filteredOps[i].name) {\n          this.inputCount = this.filteredOps[i].inputCount;\n        }\n      }\n\n      if (!this.inputCount) {\n        this.criterion.input = [];\n      }\n    },\n    clearDependentFields(el) {\n      if (el === this.$refs.telemetrySelect) {\n        this.criterion.metadata = '';\n      } else if (el === this.$refs.metadataSelect) {\n        if (!this.filteredOps.find((operation) => operation.name === this.criterion.operation)) {\n          this.criterion.operation = '';\n          this.criterion.input = this.enumerations.length\n            ? [this.enumerations[0].value.toString()]\n            : [];\n          this.inputCount = 0;\n        }\n      } else {\n        if (this.enumerations.length && !this.criterion.input.length) {\n          this.criterion.input = [this.enumerations[0].value.toString()];\n        }\n\n        this.inputCount = 0;\n      }\n    },\n    persist() {\n      this.$emit('persist', this.criterion);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/CurrentOutput.vue",
    "content": ""
  },
  {
    "path": "src/plugins/condition/components/TestData.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <section\n    v-show=\"isEditing\"\n    id=\"test-data\"\n    :class=\"{ 'is-expanded': expanded }\"\n    aria-label=\"Condition Set Test Data\"\n  >\n    <div class=\"c-cs__header c-section__header\">\n      <span\n        class=\"c-disclosure-triangle c-tree__item__view-control is-enabled\"\n        :class=\"{ 'c-disclosure-triangle--expanded': expanded }\"\n        @click=\"expanded = !expanded\"\n      ></span>\n      <div class=\"c-cs__header-label c-section__label\">Test Data</div>\n    </div>\n    <div v-if=\"expanded\" class=\"c-cs__content\">\n      <div :class=\"['c-cs__test-data__controls c-cdef__controls', { disabled: !telemetry.length }]\">\n        <label class=\"c-toggle-switch\">\n          <input type=\"checkbox\" :checked=\"isApplied\" @change=\"applyTestData\" />\n          <span class=\"c-toggle-switch__slider\" aria-label=\"Apply Test Data\"></span>\n          <span class=\"c-toggle-switch__label\">Apply Test Data</span>\n        </label>\n      </div>\n      <div class=\"c-cs-tests\">\n        <span\n          v-for=\"(testInput, tIndex) in testInputs\"\n          :key=\"tIndex\"\n          class=\"c-test-datum c-cs-test\"\n        >\n          <span class=\"c-cs-test__label\">Set</span>\n          <span class=\"c-cs-test__controls\">\n            <span class=\"c-cdef__control\">\n              <select\n                v-model=\"testInput.telemetry\"\n                aria-label=\"Test Data Telemetry Selection\"\n                @change=\"updateMetadata(testInput)\"\n              >\n                <option value=\"\">- Select Telemetry -</option>\n                <option\n                  v-for=\"(telemetryOption, index) in telemetry\"\n                  :key=\"index\"\n                  :value=\"telemetryOption.identifier\"\n                >\n                  {{ telemetryOption.name }}\n                </option>\n              </select>\n            </span>\n            <span v-if=\"testInput.telemetry\" class=\"c-cdef__control\">\n              <select\n                v-model=\"testInput.metadata\"\n                aria-label=\"Test Data Metadata Selection\"\n                @change=\"updateTestData\"\n              >\n                <option value=\"\">- Select Field -</option>\n                <option\n                  v-for=\"(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]\"\n                  :key=\"index\"\n                  :value=\"option.key\"\n                >\n                  {{ option.name }}\n                </option>\n              </select>\n            </span>\n            <span v-if=\"testInput.metadata\" class=\"c-cdef__control__inputs\">\n              <input\n                v-model=\"testInput.value\"\n                placeholder=\"Enter test input\"\n                type=\"text\"\n                class=\"c-cdef__control__input\"\n                aria-label=\"Test Data Input\"\n                @change=\"updateTestData\"\n              />\n            </span>\n          </span>\n          <div class=\"c-cs-test__buttons\">\n            <button\n              class=\"c-click-icon c-test-data__duplicate-button icon-duplicate\"\n              title=\"Duplicate this test datum\"\n              @click=\"addTestInput(testInput)\"\n            ></button>\n            <button\n              class=\"c-click-icon c-test-data__delete-button icon-trash\"\n              title=\"Delete this test datum\"\n              @click=\"removeTestInput(tIndex)\"\n            ></button>\n          </div>\n        </span>\n      </div>\n      <button\n        v-show=\"isEditing\"\n        id=\"addTestDatum\"\n        class=\"c-button c-button--major icon-plus labeled\"\n        @click=\"addTestInput\"\n      >\n        <span class=\"c-cs-button__label\" aria-label=\"Add Test Datum\">Add Test Datum</span>\n      </button>\n    </div>\n  </section>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    isEditing: Boolean,\n    telemetry: {\n      type: Array,\n      required: true,\n      default: () => []\n    },\n    testData: {\n      type: Object,\n      required: true,\n      default: () => {\n        return {\n          applied: false,\n          conditionTestInputs: []\n        };\n      }\n    }\n  },\n  emits: ['update-test-data'],\n  data() {\n    return {\n      expanded: true,\n      isApplied: false,\n      testInputs: [],\n      telemetryMetadataOptions: {}\n    };\n  },\n  watch: {\n    isEditing(editing) {\n      if (!editing) {\n        this.resetApplied();\n      }\n    },\n    telemetry: {\n      handler() {\n        this.initializeMetadata();\n      },\n      deep: true\n    },\n    testData: {\n      handler() {\n        this.initialize();\n      },\n      deep: true\n    }\n  },\n  beforeUnmount() {\n    this.resetApplied();\n  },\n  mounted() {\n    this.initialize();\n    this.initializeMetadata();\n  },\n  methods: {\n    applyTestData() {\n      this.isApplied = !this.isApplied;\n      this.updateTestData();\n    },\n    initialize() {\n      if (this.testData && this.testData.conditionTestInputs) {\n        this.testInputs = this.testData.conditionTestInputs;\n      }\n\n      if (!this.testInputs.length) {\n        this.addTestInput();\n      }\n    },\n    initializeMetadata() {\n      this.telemetry.forEach((telemetryObject) => {\n        const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n        let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);\n        if (telemetryMetadata) {\n          this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();\n        } else {\n          this.telemetryMetadataOptions[id] = [];\n        }\n      });\n    },\n    addTestInput(testInput) {\n      this.testInputs.push(\n        Object.assign(\n          {\n            telemetry: '',\n            metadata: '',\n            input: ''\n          },\n          testInput\n        )\n      );\n    },\n    removeTestInput(index) {\n      this.testInputs.splice(index, 1);\n      this.updateTestData();\n    },\n    getId(identifier) {\n      if (identifier) {\n        return this.openmct.objects.makeKeyString(identifier);\n      }\n\n      return [];\n    },\n    updateMetadata(testInput) {\n      if (testInput.telemetry) {\n        const id = this.openmct.objects.makeKeyString(testInput.telemetry);\n        if (this.telemetryMetadataOptions[id]) {\n          return;\n        }\n\n        let telemetryMetadata = this.openmct.telemetry.getMetadata(testInput);\n        this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();\n      }\n    },\n    resetApplied() {\n      this.isApplied = false;\n      this.updateTestData();\n    },\n    updateTestData() {\n      this.$emit('update-test-data', {\n        applied: this.isApplied,\n        conditionTestInputs: this.testInputs\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/conditionals.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/***************************** DRAGGING */\n.is-active-dragging {\n  .c-condition-h__drop-target {\n    height: 3px;\n    margin-bottom: $interiorMarginSm;\n  }\n}\n\n.c-condition-h {\n  &__drop-target {\n    border-radius: $controlCr;\n    height: 0;\n    min-height: 0;\n    transition: background-color, height;\n    transition-duration: 150ms;\n  }\n\n  &.is-drag-target {\n    .c-condition > * {\n      pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements\n    }\n\n    .c-condition-h__drop-target {\n      background-color: rgba($colorKey, 0.7);\n    }\n  }\n}\n\n.c-cs {\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 auto;\n  gap: $interiorMargin;\n  height: 100%;\n  overflow: hidden;\n\n  &.is-stale {\n    @include isStaleHolder();\n  }\n\n  /************************** CONDITION SET LAYOUT */\n  &__current-output {\n    flex: 0 0 auto;\n  }\n\n  &__test-data-and-conditions-w {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    height: 100%;\n    overflow: hidden;\n  }\n\n  &__test-data,\n  &__conditions {\n    flex: 0 0 auto;\n    overflow: hidden;\n  }\n\n  &__test-data {\n    flex: 0 0 auto;\n    max-height: 50%;\n\n    &.is-expanded {\n      margin-bottom: $interiorMargin * 4;\n    }\n  }\n\n  &__conditions {\n    flex: 1 1 auto;\n\n    //> * + * {\n    //  margin-top: $interiorMarginSm;\n    //}\n  }\n\n  &__content {\n    display: flex;\n    flex-direction: column;\n    gap: $interiorMarginSm;\n    flex: 0 1 auto;\n    overflow: hidden;\n\n    > * {\n      flex: 0 0 auto;\n      overflow: hidden;\n      //+ * {\n      //  margin-top: $interiorMarginSm;\n      //}\n    }\n\n    .c-button {\n      align-self: start;\n    }\n  }\n\n  .is-editing & {\n    // Add some space to kick away from blue editing border indication\n    padding: $interiorMargin;\n  }\n\n  section {\n    display: flex;\n    flex-direction: column;\n    gap: $interiorMargin;\n    overflow: hidden;\n  }\n\n  &__conditions-h {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    overflow: auto;\n    padding-right: $interiorMarginSm;\n\n    > * + * {\n      margin-top: $interiorMarginSm;\n    }\n  }\n\n  .hint {\n    padding: $interiorMarginSm;\n  }\n\n  /************************** SPECIFIC ITEMS */\n  &__current-output-value {\n    flex-direction: row;\n    align-items: baseline;\n    padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;\n\n    > * {\n      padding: $interiorMargin 0; // Must do this to align label and value\n    }\n\n    &__label {\n      color: $colorInspectorSectionHeaderFg;\n      opacity: 0.9;\n      text-transform: uppercase;\n    }\n\n    &__value {\n      $p: $interiorMargin * 3;\n      font-size: 1.25em;\n      margin-left: $interiorMargin;\n      padding-left: $p;\n      padding-right: $p;\n      background: rgba(black, 0.2);\n      border-radius: 5px;\n    }\n  }\n}\n\n/***************************** CONDITIONS AND TEST DATUM ELEMENTS */\n.c-condition,\n.c-test-datum {\n  @include discreteItem();\n  display: flex;\n  padding: $interiorMargin;\n  line-height: 170%; // Aligns text with controls like selects\n}\n\n.c-cdef,\n.c-cs-test {\n  &__controls {\n    display: flex;\n    flex: 1 1 auto;\n    flex-wrap: wrap;\n\n    > * > * {\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &__buttons {\n    white-space: nowrap;\n  }\n}\n\n.c-condition {\n  border: 1px solid transparent;\n  flex-direction: column;\n  min-width: 400px;\n\n  > * + * {\n    margin-top: $interiorMarginSm;\n  }\n  &--browse {\n    .c-condition__summary {\n      border-top: 1px solid $colorInteriorBorder;\n      padding-top: $interiorMargin;\n    }\n  }\n\n  /***************************** HEADER */\n  &__header {\n    $h: 22px;\n    display: flex;\n    align-items: start;\n    align-content: stretch;\n    overflow: hidden;\n    min-height: $h;\n    line-height: $h;\n\n    > * {\n      flex: 0 0 auto;\n      + * {\n        margin-left: $interiorMarginSm;\n      }\n    }\n  }\n\n  &__drag-grippy {\n    transform: translateY(50%);\n  }\n\n  &__name {\n    font-weight: bold;\n    align-self: baseline; // Fixes bold line-height offset problem\n  }\n\n  &__output,\n  &__summary {\n    flex: 1 1 auto;\n  }\n\n  &.is-current {\n    $c: $colorBodyFg;\n    border-color: rgba($c, 0.2);\n    background: rgba($c, 0.2);\n  }\n}\n\n/***************************** CONDITION DEFINITION, EDITING */\n.c-cdef {\n  display: grid;\n  grid-row-gap: $interiorMarginSm;\n  grid-column-gap: $interiorMargin;\n  grid-auto-columns: min-content 1fr max-content;\n  align-items: start;\n  min-width: 150px;\n  margin-left: 29px;\n  overflow: hidden;\n\n  &__criteria,\n  &__match-and-criteria {\n    display: contents;\n  }\n\n  &__label {\n    grid-column: 1;\n    text-align: right;\n    white-space: nowrap;\n  }\n\n  &__separator {\n    grid-column: 1 / span 3;\n  }\n\n  &__controls {\n    align-items: flex-start;\n    grid-column: 2;\n\n    > * > * {\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &__buttons {\n    grid-column: 3;\n  }\n}\n\n.c-c__drag-ghost {\n  width: 100%;\n  min-height: $interiorMarginSm;\n\n  &.dragging {\n    min-height: 5em;\n    background-color: lightblue;\n    border-radius: 2px;\n  }\n}\n\n/***************************** TEST DATA */\n.c-cs__test-data {\n  &__controls {\n    flex: 0 0 auto;\n  }\n}\n\n.c-cs-tests {\n  flex: 0 1 auto;\n  overflow: auto;\n  padding-right: $interiorMarginSm;\n\n  > * + * {\n    margin-top: $interiorMarginSm;\n  }\n}\n\n.c-cs-test {\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/components/inspector/ConditionalStylesView.vue",
    "content": ""
  },
  {
    "path": "src/plugins/condition/components/inspector/StyleEditor.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-style has-local-controls c-toolbar\">\n    <div class=\"c-style__controls\">\n      <div\n        :class=\"[\n          { 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },\n          { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }\n        ]\"\n        :style=\"[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]\"\n        class=\"c-style-thumb\"\n      >\n        <span\n          class=\"c-style-thumb__text\"\n          :class=\"{ 'hide-nice': !hasProperty(styleItem.style.color) }\"\n        >\n          ABC\n        </span>\n      </div>\n\n      <ToolbarColorPicker\n        v-if=\"hasProperty(styleItem.style.border)\"\n        class=\"c-style__toolbar-button--border-color u-menu-to--center\"\n        :options=\"borderColorOption\"\n        @change=\"updateStyleValue\"\n      />\n      <ToolbarColorPicker\n        v-if=\"hasProperty(styleItem.style.backgroundColor)\"\n        class=\"c-style__toolbar-button--background-color u-menu-to--center\"\n        :options=\"backgroundColorOption\"\n        @change=\"updateStyleValue\"\n      />\n      <ToolbarColorPicker\n        v-if=\"hasProperty(styleItem.style.color)\"\n        class=\"c-style__toolbar-button--color u-menu-to--center\"\n        :options=\"colorOption\"\n        @change=\"updateStyleValue\"\n      />\n      <ToolbarButton\n        v-if=\"hasProperty(encodedImageUrl)\"\n        class=\"c-style__toolbar-button--image-url\"\n        :options=\"imageUrlOption\"\n        @change=\"updateStyleValue\"\n      />\n      <ToolbarToggleButton\n        v-if=\"hasProperty(styleItem.style.isStyleInvisible)\"\n        class=\"c-style__toolbar-button--toggle-visible\"\n        :options=\"isStyleInvisibleOption\"\n        @change=\"updateStyleValue\"\n      />\n    </div>\n\n    <!-- Save Styles -->\n    <ToolbarButton\n      v-if=\"canSaveStyle\"\n      ref=\"saveStyleButton\"\n      class=\"c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major\"\n      :options=\"saveOptions\"\n      @click=\"saveItemStyle()\"\n    />\n  </div>\n</template>\n\n<script>\nimport { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';\nimport { getStylesWithoutNoneValue } from '@/plugins/condition/utils/styleUtils';\nimport ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';\nimport ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';\nimport ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';\n\nimport { encode_url } from '../../../../utils/encoding';\n\nexport default {\n  name: 'StyleEditor',\n  components: {\n    ToolbarButton,\n    ToolbarColorPicker,\n    ToolbarToggleButton\n  },\n  inject: ['openmct'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      required: true\n    },\n    mixedStyles: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    nonSpecificFontProperties: {\n      type: Array,\n      required: true\n    },\n    styleItem: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['persist', 'save-style'],\n  computed: {\n    itemStyle() {\n      return getStylesWithoutNoneValue(this.styleItem.style);\n    },\n    borderColorOption() {\n      let value = this.styleItem.style.border.replace('1px solid ', '');\n\n      return {\n        icon: 'icon-line-horz',\n        title: STYLE_CONSTANTS.borderColorTitle,\n        value: this.normalizeValueForSwatch(value),\n        property: 'border',\n        isEditing: this.isEditing,\n        nonSpecific: this.mixedStyles.indexOf('border') > -1\n      };\n    },\n    backgroundColorOption() {\n      let value = this.styleItem.style.backgroundColor;\n\n      return {\n        icon: 'icon-paint-bucket',\n        title: STYLE_CONSTANTS.backgroundColorTitle,\n        value: this.normalizeValueForSwatch(value),\n        property: 'backgroundColor',\n        isEditing: this.isEditing,\n        nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1\n      };\n    },\n    colorOption() {\n      let value = this.styleItem.style.color;\n\n      return {\n        icon: 'icon-font',\n        title: STYLE_CONSTANTS.textColorTitle,\n        value: this.normalizeValueForSwatch(value),\n        property: 'color',\n        isEditing: this.isEditing,\n        nonSpecific: this.mixedStyles.indexOf('color') > -1\n      };\n    },\n    imageUrlOption() {\n      return {\n        icon: 'icon-image',\n        title: STYLE_CONSTANTS.imagePropertiesTitle,\n        dialog: {\n          name: 'Image Properties',\n          sections: [\n            {\n              rows: [\n                {\n                  key: 'url',\n                  control: 'textfield',\n                  name: 'Image URL',\n                  cssClass: 'l-input-lg'\n                }\n              ]\n            }\n          ]\n        },\n        property: 'imageUrl',\n        formKeys: ['url'],\n        value: { url: this.encodedImageUrl },\n        isEditing: this.isEditing,\n        nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1\n      };\n    },\n    encodedImageUrl() {\n      return encode_url(this.styleItem.style.imageUrl);\n    },\n    isStyleInvisibleOption() {\n      return {\n        value: this.styleItem.style.isStyleInvisible,\n        property: 'isStyleInvisible',\n        isEditing: this.isEditing,\n        options: [\n          {\n            value: '',\n            icon: 'icon-eye-disabled',\n            title: STYLE_CONSTANTS.visibilityHidden\n          },\n          {\n            value: STYLE_CONSTANTS.isStyleInvisible,\n            icon: 'icon-eye-open',\n            title: STYLE_CONSTANTS.visibilityVisible\n          }\n        ]\n      };\n    },\n    saveOptions() {\n      return {\n        icon: 'icon-save',\n        title: 'Save style',\n        isEditing: this.isEditing\n      };\n    },\n    canSaveStyle() {\n      return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length;\n    }\n  },\n  methods: {\n    hasProperty(property) {\n      return property !== undefined;\n    },\n    normalizeValueForSwatch(value) {\n      if (value && value.indexOf('__no_value') > -1) {\n        return value.replace('__no_value', 'transparent');\n      }\n\n      return value;\n    },\n    normalizeValueForStyle(value) {\n      if (value && value === 'transparent') {\n        return '__no_value';\n      }\n\n      return value;\n    },\n    updateStyleValue(value, item) {\n      value = this.normalizeValueForStyle(value);\n      if (item.property === 'border') {\n        value = '1px solid ' + value;\n      }\n\n      if (value && value.url !== undefined) {\n        this.styleItem.style[item.property] = value.url;\n      } else {\n        this.styleItem.style[item.property] = value;\n      }\n\n      this.$emit('persist', this.styleItem, item.property);\n    },\n    saveItemStyle() {\n      this.$emit('save-style', this.itemStyle);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/inspector/StylesView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__styles c-inspect-styles\">\n    <div\n      v-if=\"isStaticAndConditionalStyles\"\n      class=\"c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon\"\n    >\n      Your selection includes one or more items that use Conditional Styling. Applying a static\n      style below will replace any Conditional Styling with the new choice.\n    </div>\n    <template v-if=\"!conditionSetDomainObject\">\n      <FontStyleEditor\n        v-if=\"canStyleFont\"\n        :font-style=\"consolidatedFontStyle\"\n        @set-font-property=\"setFontProperty\"\n      />\n      <div class=\"c-inspect-styles__content\">\n        <div v-if=\"staticStyle\" class=\"c-inspect-styles__style\">\n          <StyleEditor\n            ref=\"styleEditor\"\n            class=\"c-inspect-styles__editor\"\n            :style-item=\"staticStyle\"\n            :is-editing=\"allowEditing\"\n            :mixed-styles=\"mixedStyles\"\n            :non-specific-font-properties=\"nonSpecificFontProperties\"\n            @persist=\"updateStaticStyle\"\n            @save-style=\"saveStyle\"\n          />\n        </div>\n        <button\n          v-if=\"allowEditing\"\n          id=\"addConditionSet\"\n          class=\"c-button c-button--major c-toggle-styling-button labeled\"\n          @click=\"addConditionSet\"\n        >\n          <span class=\"c-cs-button__label\">Use Conditional Styling...</span>\n        </button>\n      </div>\n    </template>\n    <template v-else>\n      <div class=\"c-inspect-styles__content c-inspect-styles__condition-set c-inspect-styles__elem\">\n        <a v-if=\"conditionSetDomainObject\" class=\"c-object-label\" @click=\"navigateOrPreview\">\n          <span class=\"c-object-label__type-icon icon-conditional\"></span>\n          <span class=\"c-object-label__name\">{{ conditionSetDomainObject.name }}</span>\n        </a>\n        <template v-if=\"allowEditing\">\n          <button id=\"changeConditionSet\" class=\"c-button labeled\" @click=\"addConditionSet\">\n            <span class=\"c-button__label\">Change...</span>\n          </button>\n\n          <button\n            class=\"c-click-icon icon-x\"\n            title=\"Remove conditional styles\"\n            @click=\"removeConditionSet\"\n          ></button>\n        </template>\n      </div>\n\n      <div\n        v-if=\"isConditionWidget && allowEditing\"\n        class=\"c-inspect-styles__elem c-inspect-styles__output-label-toggle\"\n      >\n        <label class=\"c-toggle-switch\">\n          <input\n            type=\"checkbox\"\n            :checked=\"useConditionSetOutputAsLabel\"\n            @change=\"updateConditionSetOutputLabel\"\n          />\n          <span class=\"c-toggle-switch__slider\"></span>\n          <span class=\"c-toggle-switch__label\">Use Condition Set output as label</span>\n        </label>\n      </div>\n      <div v-if=\"isConditionWidget && !allowEditing\" class=\"c-inspect-styles__elem\">\n        <span class=\"c-toggle-switch__label\"\n          >Condition Set output as label: <span v-if=\"useConditionSetOutputAsLabel\"> Yes</span\n          ><span v-else> No</span>\n        </span>\n      </div>\n\n      <FontStyleEditor\n        v-if=\"canStyleFont\"\n        :font-style=\"consolidatedFontStyle\"\n        @set-font-property=\"setFontProperty\"\n      />\n\n      <div v-if=\"conditionsLoaded\" class=\"c-inspect-styles__conditions\">\n        <div\n          v-for=\"(conditionStyle, index) in conditionalStyles\"\n          :key=\"index\"\n          class=\"c-inspect-styles__condition\"\n          :class=\"{ 'is-current': conditionStyle.conditionId === selectedConditionId }\"\n          @click=\"applySelectedConditionStyle(conditionStyle.conditionId)\"\n        >\n          <ConditionError\n            :show-label=\"true\"\n            :condition=\"getCondition(conditionStyle.conditionId)\"\n          />\n          <ConditionDescription\n            :show-label=\"true\"\n            :condition=\"getCondition(conditionStyle.conditionId)\"\n          />\n          <StyleEditor\n            class=\"c-inspect-styles__editor\"\n            :style-item=\"conditionStyle\"\n            :non-specific-font-properties=\"nonSpecificFontProperties\"\n            :is-editing=\"allowEditing\"\n            @persist=\"updateConditionalStyle\"\n            @save-style=\"saveStyle\"\n          />\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport ConditionDescription from '@/plugins/condition/components/ConditionDescription.vue';\nimport ConditionError from '@/plugins/condition/components/ConditionError.vue';\nimport {\n  getApplicableStylesForItem,\n  getConditionSetIdentifierForItem,\n  getConsolidatedStyleValues\n} from '@/plugins/condition/utils/styleUtils';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport FontStyleEditor from '../../../inspectorViews/styles/FontStyleEditor.vue';\nimport StyleEditor from './StyleEditor.vue';\n\nconst NON_SPECIFIC = '??';\nconst NON_STYLEABLE_CONTAINER_TYPES = ['layout', 'flexible-layout', 'tabs'];\nconst NON_STYLEABLE_LAYOUT_ITEM_TYPES = ['line-view', 'box-view', 'ellipse-view', 'image-view'];\n\nexport default {\n  name: 'StylesView',\n  components: {\n    FontStyleEditor,\n    StyleEditor,\n    ConditionError,\n    ConditionDescription\n  },\n  inject: ['openmct', 'selection', 'stylesManager'],\n  data() {\n    return {\n      staticStyle: undefined,\n      isEditing: this.openmct.editor.isEditing(),\n      mixedStyles: [],\n      isStaticAndConditionalStyles: false,\n      conditionalStyles: [],\n      conditionSetDomainObject: undefined,\n      conditions: undefined,\n      conditionsLoaded: false,\n      navigateToPath: '',\n      selectedConditionId: '',\n      items: [],\n      domainObject: undefined,\n      consolidatedFontStyle: {},\n      useConditionSetOutputAsLabel: false\n    };\n  },\n  computed: {\n    locked() {\n      return this.selection.some((selectionPath) => {\n        const self = selectionPath[0].context.item;\n        const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined;\n\n        return (self && self.locked) || (parent && parent.locked);\n      });\n    },\n    allowEditing() {\n      return this.isEditing && !this.locked;\n    },\n    isConditionWidget() {\n      const hasConditionWidgetObjects =\n        this.domainObjectsById &&\n        Object.values(this.domainObjectsById).some((object) => object.type === 'conditionWidget');\n\n      return (\n        hasConditionWidgetObjects ||\n        (this.domainObject && this.domainObject.type === 'conditionWidget')\n      );\n    },\n    styleableFontItems() {\n      return this.selection.filter((selectionPath) => {\n        const item = selectionPath[0].context.item;\n        const itemType = item && item.type;\n        const layoutItem = selectionPath[0].context.layoutItem;\n        const layoutItemType = layoutItem && layoutItem.type;\n\n        if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) {\n          return false;\n        }\n\n        if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) {\n          return false;\n        }\n\n        return true;\n      });\n    },\n    nonSpecificFontProperties() {\n      if (!this.consolidatedFontStyle) {\n        return [];\n      }\n\n      return Object.keys(this.consolidatedFontStyle).filter(\n        (property) => this.consolidatedFontStyle[property] === NON_SPECIFIC\n      );\n    },\n    canStyleFont() {\n      return this.styleableFontItems.length && this.allowEditing;\n    }\n  },\n  unmounted() {\n    this.removeListeners();\n    this.openmct.editor.off('isEditing', this.setEditState);\n    this.stylesManager.off('styleSelected', this.applyStyleToSelection);\n  },\n  mounted() {\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n    this.isMultipleSelection = this.selection.length > 1;\n    this.getObjectsAndItemsFromSelection();\n    this.useConditionSetOutputAsLabel = this.getConfigurationForLabel();\n\n    if (!this.isMultipleSelection) {\n      let objectStyles = this.getObjectStyles();\n      this.initializeStaticStyle(objectStyles);\n      if (objectStyles && objectStyles.conditionSetIdentifier) {\n        this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);\n        this.conditionalStyles = objectStyles.styles;\n      }\n    } else {\n      this.initializeStaticStyle();\n    }\n\n    this.setConsolidatedFontStyle();\n\n    this.openmct.editor.on('isEditing', this.setEditState);\n    this.stylesManager.on('styleSelected', this.applyStyleToSelection);\n  },\n  methods: {\n    getConfigurationForLabel() {\n      const childObjectUsesLabels = Object.values(this.domainObjectsById || {}).some(\n        (object) => object.configuration && object.configuration.useConditionSetOutputAsLabel\n      );\n      const domainObjectUsesLabels =\n        this.domainObject &&\n        this.domainObject.configuration &&\n        this.domainObject.configuration.useConditionSetOutputAsLabel;\n\n      return childObjectUsesLabels || domainObjectUsesLabels;\n    },\n    getObjectStyles() {\n      let objectStyles;\n      if (this.domainObjectsById) {\n        const domainObject = Object.values(this.domainObjectsById)[0];\n        if (domainObject.configuration && domainObject.configuration.objectStyles) {\n          objectStyles = domainObject.configuration.objectStyles;\n        }\n      } else if (this.items.length) {\n        const itemId = this.items[0].id;\n        if (\n          this.domainObject &&\n          this.domainObject.configuration &&\n          this.domainObject.configuration.objectStyles &&\n          this.domainObject.configuration.objectStyles[itemId]\n        ) {\n          objectStyles = this.domainObject.configuration.objectStyles[itemId];\n        }\n      } else if (\n        this.domainObject &&\n        this.domainObject.configuration &&\n        this.domainObject.configuration.objectStyles\n      ) {\n        objectStyles = this.domainObject.configuration.objectStyles;\n      }\n\n      return objectStyles;\n    },\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n      if (this.isEditing) {\n        if (this.stopProvidingTelemetry) {\n          this.stopProvidingTelemetry();\n          delete this.stopProvidingTelemetry;\n        }\n      } else {\n        //reset the selectedConditionID so that the condition set computation can drive it.\n        this.applySelectedConditionStyle('');\n        this.subscribeToConditionSet();\n      }\n    },\n    enableConditionSetNav() {\n      this.openmct.objects\n        .getOriginalPath(this.conditionSetDomainObject.identifier)\n        .then((objectPath) => {\n          this.objectPath = objectPath;\n          this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);\n        });\n    },\n    navigateOrPreview(event) {\n      // If editing, display condition set in Preview overlay; otherwise nav to it while browsing\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        this.previewAction.invoke(this.objectPath);\n      } else {\n        this.openmct.router.navigate(this.navigateToPath);\n      }\n    },\n    isItemType(type, item) {\n      return item && item.type === type;\n    },\n    canPersistObject(item) {\n      return this.openmct.objects.isPersistable(item.identifier);\n    },\n    hasConditionalStyle(domainObject, layoutItem) {\n      const id = layoutItem ? layoutItem.id : undefined;\n\n      return getConditionSetIdentifierForItem(domainObject, id) !== undefined;\n    },\n    getObjectsAndItemsFromSelection() {\n      let domainObject;\n      let subObjects = [];\n      let itemsWithConditionalStyles = 0;\n\n      //multiple selection\n      let itemInitialStyles = [];\n      let itemStyle;\n      this.selection.forEach((selectionItem) => {\n        const item = selectionItem[0].context.item;\n        const layoutItem = selectionItem[0].context.layoutItem;\n        const isChildItem = selectionItem.length > 1;\n\n        if (!item && !layoutItem) {\n          // cases where selection is used for table cells\n          return;\n        }\n\n        if (!isChildItem) {\n          domainObject = item;\n          itemStyle = getApplicableStylesForItem(item);\n          if (this.hasConditionalStyle(item)) {\n            itemsWithConditionalStyles += 1;\n          }\n        } else {\n          this.canHide = true;\n          domainObject = selectionItem[1].context.item;\n          if (\n            (item && !layoutItem) ||\n            (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))\n          ) {\n            subObjects.push(item);\n            itemStyle = getApplicableStylesForItem(item);\n            if (this.hasConditionalStyle(item)) {\n              itemsWithConditionalStyles += 1;\n            }\n          } else {\n            itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);\n            this.items.push({\n              id: layoutItem.id,\n              applicableStyles: itemStyle\n            });\n            if (this.hasConditionalStyle(item, layoutItem)) {\n              itemsWithConditionalStyles += 1;\n            }\n          }\n        }\n\n        itemInitialStyles.push(itemStyle);\n      });\n      this.isStaticAndConditionalStyles = this.isMultipleSelection && itemsWithConditionalStyles;\n      const { styles, mixedStyles } = getConsolidatedStyleValues(itemInitialStyles);\n      this.initialStyles = styles;\n      this.mixedStyles = mixedStyles;\n      // main layout\n      this.domainObject = domainObject;\n      this.removeListeners();\n      if (this.domainObject) {\n        this.stopObserving = this.openmct.objects.observe(\n          this.domainObject,\n          '*',\n          (newDomainObject) => (this.domainObject = newDomainObject)\n        );\n        this.stopObservingItems = this.openmct.objects.observe(\n          this.domainObject,\n          'configuration.items',\n          this.updateDomainObjectItemStyles\n        );\n      }\n\n      subObjects.forEach(this.registerListener);\n    },\n    updateDomainObjectItemStyles(newItems) {\n      let keys = Object.keys(this.domainObject.configuration.objectStyles || {});\n      keys.forEach((key) => {\n        if (this.isKeyItemId(key)) {\n          if (!newItems.find((item) => item.id === key)) {\n            this.removeItemStyles(key);\n          }\n        }\n      });\n    },\n    isKeyItemId(key) {\n      return (\n        key !== 'styles' &&\n        key !== 'staticStyle' &&\n        key !== 'fontStyle' &&\n        key !== 'defaultConditionId' &&\n        key !== 'selectedConditionId' &&\n        key !== 'conditionSetIdentifier'\n      );\n    },\n    registerListener(domainObject) {\n      let id = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n      if (!this.domainObjectsById) {\n        this.domainObjectsById = {};\n      }\n\n      if (!this.domainObjectsById[id]) {\n        this.domainObjectsById[id] = domainObject;\n        this.observeObject(domainObject, id);\n      }\n    },\n    observeObject(domainObject, id) {\n      let unobserveObject = this.openmct.objects.observe(domainObject, '*', (newObject) => {\n        this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));\n      });\n      this.unObserveObjects.push(unobserveObject);\n    },\n    removeListeners() {\n      if (this.stopObserving) {\n        this.stopObserving();\n      }\n\n      if (this.stopObservingItems) {\n        this.stopObservingItems();\n      }\n\n      if (this.stopProvidingTelemetry) {\n        this.stopProvidingTelemetry();\n        delete this.stopProvidingTelemetry;\n      }\n\n      if (this.unObserveObjects) {\n        this.unObserveObjects.forEach((unObserveObject) => {\n          unObserveObject();\n        });\n      }\n\n      this.unObserveObjects = [];\n    },\n    subscribeToConditionSet() {\n      if (this.stopProvidingTelemetry) {\n        this.stopProvidingTelemetry();\n        delete this.stopProvidingTelemetry;\n      }\n\n      if (this.conditionSetDomainObject) {\n        this.openmct.telemetry.request(this.conditionSetDomainObject).then((output) => {\n          if (output && output.length) {\n            this.handleConditionSetResultUpdated(output[0]);\n          }\n        });\n        this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(\n          this.conditionSetDomainObject,\n          this.handleConditionSetResultUpdated.bind(this)\n        );\n      }\n    },\n    handleConditionSetResultUpdated(resultData) {\n      this.selectedConditionId = resultData ? resultData.conditionId : '';\n    },\n    initialize(conditionSetDomainObject) {\n      //If there are new conditions in the conditionSet we need to set those styles to default\n      this.conditionSetDomainObject = conditionSetDomainObject;\n      this.enableConditionSetNav();\n      this.initializeConditionalStyles();\n    },\n    initializeConditionalStyles() {\n      if (!this.conditions) {\n        this.conditions = {};\n      }\n\n      let conditionalStyles = [];\n      this.conditionSetDomainObject.configuration.conditionCollection.forEach(\n        (conditionConfiguration, index) => {\n          if (conditionConfiguration.isDefault) {\n            this.selectedConditionId = conditionConfiguration.id;\n          }\n\n          this.conditions[conditionConfiguration.id] = conditionConfiguration;\n          let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);\n          let output = { output: conditionConfiguration.configuration.output };\n          if (foundStyle) {\n            foundStyle.style = Object.assign(\n              this.canHide ? { isStyleInvisible: '' } : {},\n              this.initialStyles,\n              foundStyle.style,\n              output\n            );\n            conditionalStyles.push(foundStyle);\n          } else {\n            conditionalStyles.splice(index, 0, {\n              conditionId: conditionConfiguration.id,\n              style: Object.assign(\n                this.canHide ? { isStyleInvisible: '' } : {},\n                this.initialStyles,\n                output\n              )\n            });\n          }\n        }\n      );\n      //we're doing this so that we remove styles for any conditions that have been removed from the condition set\n      this.conditionalStyles = conditionalStyles;\n      this.conditionsLoaded = true;\n      this.getAndPersistStyles(null, this.selectedConditionId);\n      if (!this.isEditing) {\n        this.subscribeToConditionSet();\n      }\n    },\n    //TODO: Double check how this works for single styles\n    initializeStaticStyle(objectStyles) {\n      let staticStyle = objectStyles && objectStyles.staticStyle;\n      if (staticStyle) {\n        this.staticStyle = {\n          style: Object.assign({}, this.initialStyles, staticStyle.style)\n        };\n      } else {\n        this.staticStyle = {\n          style: Object.assign({}, this.initialStyles)\n        };\n      }\n    },\n    removeItemStyles(itemId) {\n      let domainObjectStyles =\n        (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};\n      if (itemId && domainObjectStyles[itemId]) {\n        delete domainObjectStyles[itemId];\n\n        if (Object.keys(domainObjectStyles).length <= 0) {\n          domainObjectStyles = undefined;\n        }\n\n        this.persist(this.domainObject, domainObjectStyles);\n      }\n    },\n    findStyleByConditionId(id) {\n      return this.conditionalStyles.find((conditionalStyle) => conditionalStyle.conditionId === id);\n    },\n    getCondition(id) {\n      return this.conditions ? this.conditions[id] : {};\n    },\n    addConditionSet() {\n      const conditionWidgetParent = this.openmct.router.path[1];\n      const formStructure = {\n        title: 'Select Condition Set',\n        sections: [\n          {\n            name: 'Location',\n            cssClass: 'grows',\n            rows: [\n              {\n                key: 'location',\n                name: 'Condition Set',\n                cssClass: 'grows',\n                control: 'locator',\n                required: true,\n                parent: conditionWidgetParent,\n                validate: (data) => data.value[0].type === 'conditionSet'\n              }\n            ]\n          }\n        ]\n      };\n\n      this.openmct.forms.showForm(formStructure).then((data) => {\n        this.conditionSetDomainObject = data.location[0];\n        this.conditionalStyles = [];\n        this.initializeConditionalStyles();\n      });\n    },\n    removeConditionSet() {\n      this.conditionSetDomainObject = undefined;\n      this.conditionalStyles = [];\n      let domainObjectStyles =\n        (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};\n      if (this.domainObjectsById) {\n        const domainObjects = Object.values(this.domainObjectsById);\n        domainObjects.forEach((domainObject) => {\n          let objectStyles =\n            (domainObject.configuration && domainObject.configuration.objectStyles) || {};\n          this.removeConditionalStyles(objectStyles);\n          if (objectStyles && Object.keys(objectStyles).length <= 0) {\n            objectStyles = undefined;\n          }\n\n          this.persist(domainObject, objectStyles);\n        });\n      }\n\n      if (this.items.length) {\n        this.items.forEach((item) => {\n          const itemId = item.id;\n          this.removeConditionalStyles(domainObjectStyles, itemId);\n          if (domainObjectStyles[itemId] && Object.keys(domainObjectStyles[itemId]).length <= 0) {\n            delete domainObjectStyles[itemId];\n          }\n        });\n      } else {\n        this.removeConditionalStyles(domainObjectStyles);\n      }\n\n      if (domainObjectStyles && Object.keys(domainObjectStyles).length <= 0) {\n        domainObjectStyles = undefined;\n      }\n\n      this.persist(this.domainObject, domainObjectStyles);\n\n      if (this.stopProvidingTelemetry) {\n        this.stopProvidingTelemetry();\n        delete this.stopProvidingTelemetry;\n      }\n    },\n    removeConditionalStyles(domainObjectStyles, itemId) {\n      if (itemId && domainObjectStyles[itemId]) {\n        domainObjectStyles[itemId].conditionSetIdentifier = undefined;\n        delete domainObjectStyles[itemId].conditionSetIdentifier;\n        domainObjectStyles[itemId].selectedConditionId = undefined;\n        domainObjectStyles[itemId].defaultConditionId = undefined;\n        domainObjectStyles[itemId].styles = undefined;\n        delete domainObjectStyles[itemId].styles;\n      } else {\n        domainObjectStyles.conditionSetIdentifier = undefined;\n        delete domainObjectStyles.conditionSetIdentifier;\n        domainObjectStyles.selectedConditionId = undefined;\n        domainObjectStyles.defaultConditionId = undefined;\n        domainObjectStyles.styles = undefined;\n        delete domainObjectStyles.styles;\n      }\n    },\n    updateStaticStyle(staticStyle, property) {\n      //update the static style for each of the layoutItems as well as each sub object item\n      this.staticStyle = staticStyle;\n      this.removeConditionSet();\n      this.getAndPersistStyles(property);\n    },\n    updateConditionalStyle(conditionStyle, property) {\n      let foundStyle = this.findStyleByConditionId(conditionStyle.conditionId);\n      if (foundStyle) {\n        foundStyle.style = conditionStyle.style;\n        this.selectedConditionId = foundStyle.conditionId;\n        this.getAndPersistStyles(property);\n      }\n    },\n    getAndPersistStyles(property, defaultConditionId) {\n      this.persist(\n        this.domainObject,\n        this.getDomainObjectStyle(this.domainObject, property, this.items, defaultConditionId)\n      );\n      if (this.domainObjectsById) {\n        const domainObjects = Object.values(this.domainObjectsById);\n        domainObjects.forEach((domainObject) => {\n          this.persist(\n            domainObject,\n            this.getDomainObjectStyle(domainObject, property, null, defaultConditionId)\n          );\n        });\n      }\n\n      if (!this.items.length && !this.domainObjectsById) {\n        this.persist(\n          this.domainObject,\n          this.getDomainObjectStyle(this.domainObject, property, null, defaultConditionId)\n        );\n      }\n\n      this.isStaticAndConditionalStyles = false;\n      if (property) {\n        let foundIndex = this.mixedStyles.indexOf(property);\n        if (foundIndex > -1) {\n          this.mixedStyles.splice(foundIndex, 1);\n        }\n      }\n    },\n    getDomainObjectStyle(domainObject, property, items, defaultConditionId) {\n      let objectStyle = {\n        styles: this.conditionalStyles,\n        staticStyle: this.staticStyle,\n        selectedConditionId: this.selectedConditionId\n      };\n      if (defaultConditionId) {\n        objectStyle.defaultConditionId = defaultConditionId;\n      }\n\n      if (this.conditionSetDomainObject) {\n        objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;\n      }\n\n      let domainObjectStyles =\n        (domainObject.configuration && domainObject.configuration.objectStyles) || {};\n\n      if (items) {\n        items.forEach((item) => {\n          let itemStaticStyle = {};\n          let itemConditionalStyle = { styles: [] };\n          if (!this.conditionSetDomainObject) {\n            if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {\n              itemStaticStyle = Object.assign({}, domainObjectStyles[item.id].staticStyle.style);\n            }\n\n            if (item.applicableStyles[property] !== undefined) {\n              itemStaticStyle[property] = this.staticStyle.style[property];\n            }\n\n            if (Object.keys(itemStaticStyle).length <= 0) {\n              itemStaticStyle = undefined;\n            }\n\n            domainObjectStyles[item.id] = { staticStyle: { style: itemStaticStyle } };\n          } else {\n            objectStyle.styles.forEach((conditionalStyle, index) => {\n              let style = {};\n              if (domainObject.configuration.useConditionSetOutputAsLabel) {\n                style.output = conditionalStyle.style.output;\n              } else {\n                style.output = '';\n              }\n\n              Object.keys(item.applicableStyles)\n                .concat(['isStyleInvisible'])\n                .forEach((key) => {\n                  style[key] = conditionalStyle.style[key];\n                });\n              itemConditionalStyle.styles.push({\n                ...conditionalStyle,\n                style\n              });\n            });\n            domainObjectStyles[item.id] = {\n              ...domainObjectStyles[item.id],\n              ...objectStyle,\n              ...itemConditionalStyle\n            };\n          }\n        });\n      } else {\n        if (domainObject.configuration.useConditionSetOutputAsLabel !== true) {\n          let objectConditionStyle = JSON.parse(JSON.stringify(objectStyle));\n          objectConditionStyle.styles.forEach((conditionalStyle) => {\n            conditionalStyle.style.output = '';\n          });\n          domainObjectStyles = {\n            ...domainObjectStyles,\n            ...objectConditionStyle\n          };\n        } else {\n          domainObjectStyles = {\n            ...domainObjectStyles,\n            ...objectStyle\n          };\n        }\n      }\n\n      return domainObjectStyles;\n    },\n    applySelectedConditionStyle(conditionId) {\n      this.selectedConditionId = conditionId;\n      this.getAndPersistStyles();\n    },\n    persistLabelConfiguration() {\n      if (this.domainObjectsById) {\n        Object.values(this.domainObjectsById).forEach((object) => {\n          this.openmct.objects.mutate(\n            object,\n            'configuration.useConditionSetOutputAsLabel',\n            this.useConditionSetOutputAsLabel\n          );\n        });\n      } else {\n        this.openmct.objects.mutate(\n          this.domainObject,\n          'configuration.useConditionSetOutputAsLabel',\n          this.useConditionSetOutputAsLabel\n        );\n      }\n\n      this.getAndPersistStyles();\n    },\n    persist(domainObject, style) {\n      this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);\n    },\n    applyStyleToSelection(style) {\n      if (!this.allowEditing) {\n        return;\n      }\n\n      this.updateSelectionFontStyle(style);\n      this.updateSelectionStyle(style);\n    },\n    updateSelectionFontStyle(style) {\n      const fontSizeProperty = {\n        fontSize: style.fontSize\n      };\n      const fontProperty = {\n        font: style.font\n      };\n\n      this.setFontProperty(fontSizeProperty);\n      this.setFontProperty(fontProperty);\n    },\n    updateSelectionStyle(style) {\n      const foundStyle = this.findStyleByConditionId(this.selectedConditionId);\n\n      if (foundStyle && !this.isStaticAndConditionalStyles) {\n        Object.entries(style).forEach(([property, value]) => {\n          if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) {\n            foundStyle.style[property] = value;\n          }\n        });\n        this.getAndPersistStyles();\n      } else {\n        this.removeConditionSet();\n        Object.entries(style).forEach(([property, value]) => {\n          if (\n            this.staticStyle.style[property] !== undefined &&\n            this.staticStyle.style[property] !== value\n          ) {\n            this.staticStyle.style[property] = value;\n            this.getAndPersistStyles(property);\n          }\n        });\n      }\n    },\n    saveStyle(style) {\n      const styleToSave = {\n        ...style,\n        ...this.consolidatedFontStyle\n      };\n\n      this.stylesManager.save(styleToSave);\n    },\n    setConsolidatedFontStyle() {\n      const styles = [];\n\n      this.styleableFontItems.forEach((styleable) => {\n        const fontStyle = this.getFontStyle(styleable[0]);\n\n        styles.push(fontStyle);\n      });\n\n      if (styles.length) {\n        const hasConsolidatedFontSize =\n          styles.length &&\n          styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);\n        const hasConsolidatedFont =\n          styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);\n\n        const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC;\n        const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC;\n\n        this.consolidatedFontStyle.fontSize = fontSize;\n        this.consolidatedFontStyle.font = font;\n      }\n    },\n    getFontStyle(selectionPath) {\n      const item = selectionPath.context.item;\n      const layoutItem = selectionPath.context.layoutItem;\n      let fontStyle = item && item.configuration && item.configuration.fontStyle;\n\n      // support for legacy where font styling in layouts only\n      if (!fontStyle) {\n        fontStyle = {\n          fontSize: (layoutItem && layoutItem.fontSize) || 'default',\n          font: (layoutItem && layoutItem.font) || 'default'\n        };\n      }\n\n      return fontStyle;\n    },\n    setFontProperty(fontStyleObject) {\n      let layoutDomainObject;\n      const [property, value] = Object.entries(fontStyleObject)[0];\n\n      this.styleableFontItems.forEach((styleable) => {\n        if (!this.isLayoutObject(styleable)) {\n          const fontStyle = this.getFontStyle(styleable[0]);\n          fontStyle[property] = value;\n\n          this.openmct.objects.mutate(\n            styleable[0].context.item,\n            'configuration.fontStyle',\n            fontStyle\n          );\n        } else {\n          // all layoutItems in this context will share same parent layout\n          if (!layoutDomainObject) {\n            layoutDomainObject = styleable[1].context.item;\n          }\n\n          // save layout item font style to parent layout configuration\n          const layoutItemIndex = styleable[0].context.index;\n          const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex];\n\n          layoutItemConfiguration[property] = value;\n        }\n      });\n\n      if (layoutDomainObject) {\n        this.openmct.objects.mutate(\n          layoutDomainObject,\n          'configuration.items',\n          layoutDomainObject.configuration.items\n        );\n      }\n\n      // sync vue component on font update\n      this.consolidatedFontStyle[property] = value;\n    },\n    isLayoutObject(selectionPath) {\n      const layoutItemType =\n        selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;\n\n      return layoutItemType && layoutItemType !== 'subobject-view';\n    },\n    updateConditionSetOutputLabel(event) {\n      this.useConditionSetOutputAsLabel = event.target.checked === true;\n      this.persistLabelConfiguration();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/condition/components/inspector/conditional-styles.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/********************************************* INSPECTOR STYLES TAB */\n.c-inspect-styles {\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__content,\n  &__conditions,\n  &__condition {\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n  }\n\n  &__content {\n    display: flex;\n    flex-direction: column;\n  }\n\n  &__elem {\n    border-bottom: 1px solid $colorInteriorBorder;\n    padding-bottom: $interiorMargin;\n  }\n\n  &__condition-set {\n    align-items: baseline;\n    display: flex;\n    flex-direction: row;\n\n    .c-object-label {\n      flex: 1 1 auto;\n    }\n\n    .c-button {\n      flex: 0 0 auto;\n    }\n  }\n\n  &__style {\n    padding-bottom: $interiorMargin;\n  }\n\n  &__condition {\n    padding: $interiorMargin;\n  }\n\n  &__condition {\n    @include discreteItem();\n    border: 1px solid transparent;\n    pointer-events: none; // Prevent selecting when the object isn't being edited\n\n    &.is-current {\n      $c: $colorBodyFg;\n      border-color: rgba($c, 0.2);\n      background: rgba($c, 0.2);\n    }\n\n    .is-editing & {\n      cursor: pointer;\n      pointer-events: initial;\n\n      &:hover {\n        background: rgba($colorBodyFg, 0.1);\n      }\n\n      &.is-current {\n        $c: $editUIColorBg;\n        border-color: $c;\n        background: rgba($c, 0.1);\n      }\n    }\n  }\n\n  .c-style {\n    padding: 2px; // Allow a bit of room for thumb box-shadow\n\n    &__condition-desc {\n      @include ellipsize();\n    }\n  }\n}\n\n.c-inspect-styles__style {\n  .is-editing & {\n    border-bottom: 1px solid $colorInteriorBorder;\n  }\n}\n\n.l-shell:not(.is-editing) .c-inspect-styles {\n  .c-toolbar {\n    // Disabled-look toolbar when not editing\n    pointer-events: none;\n    cursor: inherit;\n\n    // Hide control buttons, like image URL\n    [class*='--image-url'] {\n      display: none;\n    }\n\n    // Make buttons look disabled by knocking back icon, not swatch element\n    .c-icon-button {\n      &:before {\n        opacity: $controlDisabledOpacity;\n      }\n    }\n  }\n}\n\n.c-toggle-styling-button {\n  display: none;\n\n  .is-editing & {\n    display: block;\n    align-self: flex-end;\n  }\n}\n\n.is-style-invisible {\n  display: none !important;\n\n  .is-editing & {\n    display: block !important;\n    opacity: 0.2;\n  }\n\n  &.c-style-thumb {\n    display: block !important;\n    background-color: transparent !important;\n    border-color: transparent !important;\n    @include bgCheckerboard($size: 10px, $imp: true);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/criterion/AllTelemetryCriterion.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { getOperatorText } from '@/plugins/condition/utils/operations';\nimport StalenessUtils from '@/utils/staleness';\n\nimport { evaluateResults } from '../utils/evaluator.js';\nimport { checkIfOld, getLatestTimestamp } from '../utils/time.js';\nimport TelemetryCriterion from './TelemetryCriterion.js';\n\nexport default class AllTelemetryCriterion extends TelemetryCriterion {\n  #emptyMap = new Map();\n  /**\n   * Subscribes/Unsubscribes to telemetry and emits the result\n   * of operations performed on the telemetry data returned and a given input value.\n   * @constructor\n   * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }\n   * @param openmct\n   */\n\n  initialize() {\n    this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };\n    this.telemetryDataCache = {};\n\n    if (this.isValid() && this.isOldCheck() && this.isValidInput()) {\n      this.checkForOldData(this.telemetryObjects || {});\n    }\n\n    if (this.isValid() && this.isStalenessCheck()) {\n      this.subscribeToStaleness(this.telemetryObjects || {});\n    }\n  }\n\n  checkForOldData(telemetryObjects) {\n    if (!this.ageCheck) {\n      this.ageCheck = {};\n    }\n\n    Object.values(telemetryObjects).forEach((telemetryObject) => {\n      const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      if (!this.ageCheck[id]) {\n        this.ageCheck[id] = checkIfOld((data) => {\n          this.handleOldTelemetry(id, data);\n        }, this.input[0] * 1000);\n      }\n    });\n  }\n\n  handleOldTelemetry(id, data) {\n    if (this.telemetryDataCache) {\n      this.telemetryDataCache[id] = true;\n      this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);\n    }\n\n    this.emitEvent('telemetryIsOld', data);\n  }\n\n  subscribeToStaleness(telemetryObjects) {\n    if (!this.stalenessSubscription) {\n      this.stalenessSubscription = {};\n    }\n\n    Object.values(telemetryObjects).forEach((telemetryObject) => {\n      const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      if (!this.stalenessSubscription[id]) {\n        this.stalenessSubscription[id] = {};\n        this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(\n          this.openmct,\n          telemetryObject\n        );\n        this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => {\n          if (stalenessResponse !== undefined) {\n            this.handleStaleTelemetry(id, stalenessResponse);\n          }\n        });\n        this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(\n          telemetryObject,\n          (stalenessResponse) => {\n            this.handleStaleTelemetry(id, stalenessResponse);\n          }\n        );\n      }\n    });\n  }\n\n  handleStaleTelemetry(id, stalenessResponse) {\n    if (this.telemetryDataCache) {\n      if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {\n        this.telemetryDataCache[id] = stalenessResponse.isStale;\n        this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);\n\n        this.emitEvent('telemetryStaleness');\n      }\n    }\n  }\n\n  isValid() {\n    return (\n      (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation\n    );\n  }\n\n  updateTelemetryObjects(telemetryObjects) {\n    this.telemetryObjects = { ...telemetryObjects };\n    this.removeTelemetryDataCache();\n\n    if (this.isValid() && this.isOldCheck() && this.isValidInput()) {\n      this.checkForOldData(this.telemetryObjects || {});\n    }\n\n    if (this.isValid() && this.isStalenessCheck()) {\n      this.subscribeToStaleness(this.telemetryObjects || {});\n    }\n  }\n\n  removeTelemetryDataCache() {\n    const telemetryCacheIds = Object.keys(this.telemetryDataCache);\n    Object.values(this.telemetryObjects).forEach((telemetryObject) => {\n      const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      const foundIndex = telemetryCacheIds.indexOf(id);\n      if (foundIndex > -1) {\n        telemetryCacheIds.splice(foundIndex, 1);\n      }\n    });\n    telemetryCacheIds.forEach((id) => {\n      delete this.telemetryDataCache[id];\n      delete this.ageCheck[id];\n      this.stalenessSubscription[id].unsubscribe();\n      this.stalenessSubscription[id].stalenessUtils.destroy();\n      delete this.stalenessSubscription[id];\n    });\n  }\n\n  formatData(data, telemetryObjects) {\n    if (data) {\n      this.telemetryDataCache[data.id] = this.computeResult(data);\n    }\n\n    let keys = Object.keys(telemetryObjects);\n    keys.forEach((key) => {\n      let telemetryObject = telemetryObjects[key];\n      const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      if (this.telemetryDataCache[id] === undefined) {\n        this.telemetryDataCache[id] = false;\n      }\n    });\n\n    const datum = {\n      result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)\n    };\n\n    if (data) {\n      this.openmct.time.getAllTimeSystems().forEach((timeSystem) => {\n        datum[timeSystem.key] = data[timeSystem.key];\n      });\n    }\n\n    return datum;\n  }\n\n  updateResult(allTelemetryDataMap, telemetryObjects) {\n    const validatedData = this.isValid() ? allTelemetryDataMap : this.#emptyMap;\n\n    if (validatedData && !this.isStalenessCheck()) {\n      if (this.isOldCheck()) {\n        Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {\n          if (this.ageCheck?.[objectIdKeystring]) {\n            this.ageCheck[objectIdKeystring].update(validatedData.get(objectIdKeystring));\n          }\n\n          this.telemetryDataCache[objectIdKeystring] = false;\n        });\n      } else {\n        Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {\n          this.telemetryDataCache[objectIdKeystring] = this.computeResult(\n            validatedData.get(objectIdKeystring)\n          );\n        });\n      }\n    }\n\n    Object.values(telemetryObjects).forEach((telemetryObject) => {\n      const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n      if (this.telemetryDataCache[id] === undefined) {\n        this.telemetryDataCache[id] = false;\n      }\n    });\n\n    this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);\n  }\n\n  requestLAD(telemetryObjects, requestOptions) {\n    //We pass in the global time context here\n    let options = {\n      strategy: 'latest',\n      size: 1,\n      timeContext: this.openmct.time.getContextForView([])\n    };\n\n    if (requestOptions !== undefined) {\n      options = Object.assign(options, requestOptions);\n    }\n\n    if (!this.isValid()) {\n      return this.formatData({}, telemetryObjects);\n    }\n\n    let keys = Object.keys(Object.assign({}, telemetryObjects));\n    const telemetryRequests = keys.map((key) =>\n      this.openmct.telemetry.request(telemetryObjects[key], options)\n    );\n\n    let telemetryDataCache = {};\n\n    return Promise.all(telemetryRequests).then((telemetryRequestsResults) => {\n      let latestTimestamp;\n      const timeSystems = this.openmct.time.getAllTimeSystems();\n      const timeSystem = this.openmct.time.getTimeSystem();\n\n      telemetryRequestsResults.forEach((results, index) => {\n        const latestDatum =\n          Array.isArray(results) && results.length ? results[results.length - 1] : {};\n        const datumId = keys[index];\n        const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);\n\n        telemetryDataCache[datumId] = this.computeResult(normalizedDatum);\n\n        latestTimestamp = getLatestTimestamp(\n          latestTimestamp,\n          normalizedDatum,\n          timeSystems,\n          timeSystem\n        );\n      });\n\n      const datum = {\n        result: evaluateResults(Object.values(telemetryDataCache), this.telemetry),\n        ...latestTimestamp\n      };\n\n      return {\n        id: this.id,\n        data: datum\n      };\n    });\n  }\n\n  getDescription() {\n    const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry';\n    let metadataValue = this.metadata === 'dataReceived' ? '' : this.metadata;\n    let inputValue = this.input;\n    if (this.metadata) {\n      const telemetryObjects = Object.values(this.telemetryObjects);\n      for (let i = 0; i < telemetryObjects.length; i++) {\n        const telemetryObject = telemetryObjects[i];\n        const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata);\n        if (metadataObject) {\n          metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata;\n          inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;\n          break;\n        }\n      }\n    }\n\n    return `${telemetryDescription} ${metadataValue} ${getOperatorText(\n      this.operation,\n      inputValue\n    )}`;\n  }\n\n  destroy() {\n    delete this.telemetryObjects;\n    delete this.telemetryDataCache;\n\n    if (this.ageCheck) {\n      Object.values(this.ageCheck).forEach((subscription) => subscription.clear);\n      delete this.ageCheck;\n    }\n\n    if (this.stalenessSubscription) {\n      Object.values(this.stalenessSubscription).forEach((subscription) => {\n        subscription.unsubscribe();\n        subscription.stalenessUtils.destroy();\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/criterion/TelemetryCriterion.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport StalenessUtils from '@/utils/staleness';\n\nimport { IS_OLD_KEY, IS_STALE_KEY } from '../utils/constants.js';\nimport { getOperatorText, OPERATIONS } from '../utils/operations.js';\nimport { checkIfOld } from '../utils/time.js';\n\nexport default class TelemetryCriterion extends EventEmitter {\n  #lastUpdated;\n  #lastTimeSystem;\n  #comparator;\n\n  /**\n   * Subscribes/Unsubscribes to telemetry and emits the result\n   * of operations performed on the telemetry data returned and a given input value.\n   * @constructor\n   * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }\n   * @param {import('../../../MCT.js').OpenMCT} openmct\n   */\n  constructor(telemetryDomainObjectDefinition, openmct) {\n    super();\n\n    /**\n     * @type {import('../../../MCT.js').MCT}\n     */\n    this.openmct = openmct;\n    this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;\n    this.id = telemetryDomainObjectDefinition.id;\n    this.telemetry = telemetryDomainObjectDefinition.telemetry;\n    this.operation = telemetryDomainObjectDefinition.operation;\n    this.#comparator = this.#findOperation(this.operation);\n    this.input = telemetryDomainObjectDefinition.input;\n    this.metadata = telemetryDomainObjectDefinition.metadata;\n    this.result = undefined;\n    this.ageCheck = undefined;\n    this.unsubscribeFromStaleness = undefined;\n\n    this.initialize();\n    this.emitEvent('criterionUpdated', this);\n\n    this.openmct.time.on('clockChanged', this.subscribeToStaleness);\n  }\n\n  initialize() {\n    this.telemetryObjectIdAsString = '';\n    if (![undefined, null, ''].includes(this.telemetryDomainObjectDefinition?.telemetry)) {\n      this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(\n        this.telemetryDomainObjectDefinition.telemetry\n      );\n    }\n\n    this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);\n\n    if (this.isValid() && this.isOldCheck() && this.isValidInput()) {\n      this.checkForOldData();\n    }\n\n    if (this.isValid() && this.isStalenessCheck()) {\n      this.subscribeToStaleness();\n    }\n  }\n\n  usesTelemetry(id) {\n    return this.telemetryObjectIdAsString && this.telemetryObjectIdAsString === id;\n  }\n\n  checkForOldData() {\n    if (this.ageCheck) {\n      this.ageCheck.clear();\n    }\n    this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);\n  }\n\n  handleOldTelemetry(data) {\n    this.result = true;\n    this.emitEvent('telemetryIsOld', data);\n  }\n\n  subscribeToStaleness() {\n    if (this.unsubscribeFromStaleness) {\n      this.unsubscribeFromStaleness();\n    }\n\n    if (!this.telemetryObject) {\n      return;\n    }\n\n    if (!this.stalenessUtils) {\n      this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject);\n    }\n\n    this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this));\n    this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(\n      this.telemetryObject,\n      this.handleStaleTelemetry.bind(this)\n    );\n  }\n\n  handleStaleTelemetry(stalenessResponse) {\n    if (\n      stalenessResponse !== undefined &&\n      this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)\n    ) {\n      this.result = stalenessResponse.isStale;\n      this.emitEvent('telemetryStaleness');\n    }\n  }\n\n  isValid() {\n    return this.telemetryObject && this.metadata && this.operation;\n  }\n\n  isOldCheck() {\n    return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY;\n  }\n\n  isStalenessCheck() {\n    return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY;\n  }\n\n  isValidInput() {\n    return this.input instanceof Array && this.input.length;\n  }\n\n  updateTelemetryObjects(telemetryObjects) {\n    this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];\n\n    if (this.isValid() && this.isOldCheck() && this.isValidInput()) {\n      this.checkForOldData();\n    }\n\n    if (this.isValid() && this.isStalenessCheck()) {\n      this.subscribeToStaleness();\n    }\n  }\n\n  createNormalizedDatum(telemetryDatum, endpoint) {\n    if (!telemetryDatum) {\n      return;\n    }\n\n    const id = this.openmct.objects.makeKeyString(endpoint.identifier);\n    const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;\n    const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {\n      const formatter = this.openmct.telemetry.getValueFormatter(metadatum);\n      datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);\n\n      return datum;\n    }, {});\n\n    normalizedDatum.id = id;\n\n    return normalizedDatum;\n  }\n\n  formatData(data) {\n    const datum = {\n      result: this.computeResult(data)\n    };\n\n    if (data) {\n      this.openmct.time.getAllTimeSystems().forEach((timeSystem) => {\n        datum[timeSystem.key] = data[timeSystem.key];\n      });\n    }\n\n    return datum;\n  }\n  shouldUpdateResult(datum, timesystem) {\n    const dataIsDefined = datum !== undefined;\n    const hasTimeSystemChanged =\n      this.#lastTimeSystem === undefined || this.#lastTimeSystem !== timesystem;\n    const isCacheStale = this.#lastUpdated === undefined || datum[timesystem] > this.#lastUpdated;\n\n    return dataIsDefined && (hasTimeSystemChanged || isCacheStale);\n  }\n  updateResult(data, currentTimeSystemKey) {\n    const validatedData = this.isValid() ? data : {};\n\n    if (!this.isStalenessCheck()) {\n      if (this.isOldCheck()) {\n        if (this.ageCheck) {\n          this.ageCheck.update(validatedData);\n        }\n\n        this.result = false;\n      } else {\n        this.result = this.computeResult(validatedData);\n      }\n      this.#lastUpdated = data[currentTimeSystemKey];\n      this.#lastTimeSystem = currentTimeSystemKey;\n    }\n  }\n\n  requestLAD(telemetryObjects, requestOptions) {\n    //We pass in the global time context here\n    let options = {\n      strategy: 'latest',\n      size: 1,\n      timeContext: this.openmct.time.getContextForView([])\n    };\n\n    if (requestOptions !== undefined) {\n      options = Object.assign(options, requestOptions);\n    }\n\n    if (!this.isValid()) {\n      return {\n        id: this.id,\n        data: this.formatData({})\n      };\n    }\n\n    let telemetryObject = this.telemetryObject;\n\n    return this.openmct.telemetry\n      .request(this.telemetryObject, options)\n      .then((results) => {\n        const latestDatum = results.length ? results[results.length - 1] : {};\n        const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObject);\n\n        return {\n          id: this.id,\n          data: this.formatData(normalizedDatum)\n        };\n      })\n      .catch((error) => {\n        return {\n          id: this.id,\n          data: this.formatData()\n        };\n      });\n  }\n\n  #findOperation(operation) {\n    for (let i = 0; i < OPERATIONS.length; i++) {\n      if (operation === OPERATIONS[i].name) {\n        return OPERATIONS[i].operation;\n      }\n    }\n\n    return null;\n  }\n\n  computeResult(data) {\n    let result = false;\n    if (data) {\n      let params = [];\n      params.push(data[this.metadata]);\n      if (this.isValidInput()) {\n        this.input.forEach((input) => params.push(input));\n      }\n\n      if (typeof this.#comparator === 'function') {\n        result = Boolean(this.#comparator(params));\n      }\n    }\n\n    return result;\n  }\n\n  emitEvent(eventName, data) {\n    this.emit(eventName, {\n      id: this.id,\n      data: data\n    });\n  }\n\n  getMetaDataObject(telemetryObject, metadata) {\n    let metadataObject;\n    if (metadata) {\n      const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      if (telemetryMetadata) {\n        metadataObject = telemetryMetadata.valueMetadatas.find(\n          (valueMetadata) => valueMetadata.key === metadata\n        );\n      }\n    }\n\n    return metadataObject;\n  }\n\n  getInputValueFromMetaData(metadataObject, input) {\n    let inputValue;\n    if (metadataObject) {\n      if (metadataObject.enumerations && input.length) {\n        const enumeration = metadataObject.enumerations.find(\n          (item) => item.value.toString() === input[0].toString()\n        );\n        if (enumeration !== undefined && enumeration.string) {\n          inputValue = [enumeration.string];\n        }\n      }\n    }\n\n    return inputValue;\n  }\n\n  getMetadataValueFromMetaData(metadataObject) {\n    let metadataValue;\n    if (metadataObject) {\n      if (metadataObject.name) {\n        metadataValue = metadataObject.name;\n      }\n    }\n\n    return metadataValue;\n  }\n\n  getDescription(criterion, index) {\n    let description;\n    if (!this.telemetry || !this.telemetryObject || this.telemetryObject.type === 'unknown') {\n      description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`;\n    } else {\n      const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata);\n      const metadataValue =\n        this.getMetadataValueFromMetaData(metadataObject) ||\n        (this.metadata === 'dataReceived' ? '' : this.metadata);\n      const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;\n      description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(\n        this.operation,\n        inputValue\n      )}`;\n    }\n\n    return description;\n  }\n\n  destroy() {\n    delete this.telemetryObject;\n    delete this.telemetryObjectIdAsString;\n\n    if (this.ageCheck) {\n      delete this.ageCheck;\n    }\n\n    this.openmct.time.off('clockChanged', this.subscribeToStaleness);\n\n    if (this.stalenessUtils) {\n      this.stalenessUtils.destroy();\n    }\n\n    if (this.unsubscribeFromStaleness) {\n      this.unsubscribeFromStaleness();\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/condition/criterion/TelemetryCriterionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { getMockTelemetry } from 'utils/testing';\n\nimport TelemetryCriterion from './TelemetryCriterion.js';\n\nlet openmct = {};\nlet mockListener;\nlet testCriterionDefinition;\nlet testTelemetryObject;\nlet telemetryCriterion;\nlet mockTelemetry = getMockTelemetry();\n\ndescribe('The telemetry criterion', function () {\n  beforeEach(() => {\n    testTelemetryObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-object'\n      },\n      type: 'test-object',\n      name: 'Test Object',\n      telemetry: {\n        valueMetadatas: [\n          {\n            key: 'value',\n            name: 'Value',\n            hints: {\n              range: 2\n            }\n          },\n          {\n            key: 'utc',\n            name: 'Time',\n            format: 'utc',\n            hints: {\n              domain: 1\n            }\n          },\n          {\n            key: 'testSource',\n            source: 'value',\n            name: 'Test',\n            format: 'string'\n          }\n        ]\n      }\n    };\n    openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);\n    openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);\n    openmct.telemetry = jasmine.createSpyObj('telemetry', [\n      'isTelemetryObject',\n      'subscribe',\n      'getMetadata',\n      'getValueFormatter',\n      'request'\n    ]);\n    openmct.telemetry.isTelemetryObject.and.returnValue(true);\n    openmct.telemetry.subscribe.and.returnValue(function () {});\n    openmct.telemetry.getValueFormatter.and.returnValue({\n      parse: function (value) {\n        return value;\n      }\n    });\n    openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);\n\n    openmct.time = jasmine.createSpyObj('timeAPI', [\n      'timeSystem',\n      'bounds',\n      'getAllTimeSystems',\n      'getContextForView',\n      'on',\n      'off'\n    ]);\n    openmct.time.timeSystem.and.returnValue({ key: 'system' });\n    openmct.time.bounds.and.returnValue({\n      start: 0,\n      end: 1\n    });\n    openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]);\n    openmct.time.getContextForView.and.returnValue({});\n    openmct.time.on.and.returnValue(() => {});\n    openmct.time.off.and.returnValue(() => {});\n\n    testCriterionDefinition = {\n      id: 'test-criterion-id',\n      telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),\n      operation: 'textContains',\n      metadata: 'testSource',\n      input: ['Hell'],\n      telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject }\n    };\n\n    mockListener = jasmine.createSpy('listener');\n\n    telemetryCriterion = new TelemetryCriterion(testCriterionDefinition, openmct);\n\n    telemetryCriterion.on('criterionResultUpdated', mockListener);\n  });\n\n  it('initializes with a telemetry objectId as string', function () {\n    expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(\n      testTelemetryObject.identifier.key\n    );\n  });\n\n  it('returns a result on new data from relevant telemetry providers', function () {\n    telemetryCriterion.updateResult(\n      {\n        testSource: 'Hello',\n        utc: 'Hi',\n        id: testTelemetryObject.identifier.key\n      },\n      'utc'\n    );\n    expect(telemetryCriterion.result).toBeTrue();\n  });\n\n  describe('the LAD request', () => {\n    beforeEach(() => {\n      let telemetryRequestResolve;\n      let telemetryRequestPromise = new Promise((resolve) => {\n        telemetryRequestResolve = resolve;\n      });\n      openmct.telemetry.request.and.callFake(() => {\n        setTimeout(() => {\n          telemetryRequestResolve(mockTelemetry);\n        }, 100);\n\n        return telemetryRequestPromise;\n      });\n    });\n\n    it('returns results for slow LAD requests', function () {\n      const criteriaRequest = telemetryCriterion.requestLAD();\n      telemetryCriterion.destroy();\n      expect(telemetryCriterion.telemetryObject).toBeUndefined();\n      setTimeout(() => {\n        criteriaRequest.then((result) => {\n          expect(result).toBeDefined();\n        });\n      }, 300);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { v4 as uuid } from 'uuid';\n\nimport ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy.js';\nimport ConditionSetMetadataProvider from './ConditionSetMetadataProvider.js';\nimport ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider.js';\nimport ConditionSetViewProvider from './ConditionSetViewProvider.js';\n\nexport default function ConditionPlugin() {\n  return function install(openmct) {\n    openmct.types.addType('conditionSet', {\n      name: 'Condition Set',\n      key: 'conditionSet',\n      description:\n        'Monitor and evaluate telemetry values in real-time with a wide variety of criteria. Use to control the styling of many objects in Open MCT.',\n      creatable: true,\n      cssClass: 'icon-conditional',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          conditionTestData: [],\n          conditionCollection: [\n            {\n              isDefault: true,\n              id: uuid(),\n              configuration: {\n                name: 'Default',\n                output: 'Default',\n                trigger: 'all',\n                criteria: []\n              },\n              summary: 'Default condition'\n            }\n          ]\n        };\n        domainObject.composition = [];\n        domainObject.telemetry = {};\n      }\n    });\n    let compositionPolicy = new ConditionSetCompositionPolicy(openmct);\n    openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy));\n    openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));\n    openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));\n    openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/condition/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport ConditionManager from '@/plugins/condition/ConditionManager';\n\nimport stylesManager from '../inspectorViews/styles/StylesManager.js';\nimport StylesView from './components/inspector/StylesView.vue';\nimport ConditionPlugin from './plugin.js';\nimport StyleRuleManager from './StyleRuleManager.js';\nimport { IS_OLD_KEY } from './utils/constants.js';\nimport { getApplicableStylesForItem } from './utils/styleUtils.js';\n\ndescribe('the plugin', function () {\n  let conditionSetDefinition;\n  let mockConditionSetDomainObject;\n  let mockListener;\n  let element;\n  let child;\n  let openmct;\n  let testTelemetryObject;\n\n  beforeEach((done) => {\n    testTelemetryObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-object'\n      },\n      type: 'test-object',\n      name: 'Test Object',\n      telemetry: {\n        values: [\n          {\n            key: 'some-key2',\n            source: 'some-key2',\n            name: 'Some attribute',\n            hints: {\n              range: 2\n            }\n          },\n          {\n            key: 'utc',\n            name: 'Time',\n            format: 'utc',\n            hints: {\n              domain: 1\n            },\n            source: 'utc'\n          },\n          {\n            key: 'testSource',\n            source: 'value',\n            name: 'Test',\n            format: 'string'\n          },\n          {\n            key: 'some-key',\n            source: 'some-key',\n            hints: {\n              domain: 1\n            }\n          }\n        ]\n      }\n    };\n\n    openmct = createOpenMct();\n    openmct.install(new ConditionPlugin());\n\n    conditionSetDefinition = openmct.types.get('conditionSet').definition;\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    mockConditionSetDomainObject = {\n      identifier: {\n        key: 'testConditionSetKey',\n        namespace: ''\n      },\n      type: 'conditionSet'\n    };\n\n    mockListener = jasmine.createSpy('mockListener');\n\n    openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);\n\n    conditionSetDefinition.initialize(mockConditionSetDomainObject);\n\n    spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  let mockConditionSetObject = {\n    name: 'Condition Set',\n    key: 'conditionSet',\n    creatable: true\n  };\n\n  it('defines a conditionSet object type with the correct key', () => {\n    expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key);\n  });\n\n  describe('the conditionSet object', () => {\n    it('is creatable', () => {\n      expect(conditionSetDefinition.creatable).toEqual(mockConditionSetObject.creatable);\n    });\n\n    it('initializes with an empty composition list', () => {\n      expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();\n      expect(mockConditionSetDomainObject.composition.length).toEqual(0);\n    });\n  });\n\n  describe('the condition set usage for condition widgets', () => {\n    let conditionWidgetItem;\n    let selection;\n    let component;\n    let _destroy;\n    let styleViewComponentObject;\n    const conditionSetDomainObject = {\n      configuration: {\n        conditionTestData: [\n          {\n            telemetry: '',\n            metadata: '',\n            input: ''\n          }\n        ],\n        conditionCollection: [\n          {\n            id: '39584410-cbf9-499e-96dc-76f27e69885d',\n            configuration: {\n              name: 'Unnamed Condition',\n              output: 'Sine > 0',\n              trigger: 'all',\n              criteria: [\n                {\n                  id: '85fbb2f7-7595-42bd-9767-a932266c5225',\n                  telemetry: {\n                    namespace: '',\n                    key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a'\n                  },\n                  operation: 'greaterThan',\n                  input: ['0'],\n                  metadata: 'sin'\n                },\n                {\n                  id: '35400132-63b0-425c-ac30-8197df7d5862',\n                  telemetry: 'any',\n                  operation: 'enumValueIs',\n                  input: ['0'],\n                  metadata: 'state'\n                }\n              ]\n            },\n            summary:\n              'Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF '\n          },\n          {\n            isDefault: true,\n            id: '2532d90a-e0d6-4935-b546-3123522da2de',\n            configuration: {\n              name: 'Default',\n              output: 'Default',\n              trigger: 'all',\n              criteria: []\n            },\n            summary: ''\n          }\n        ]\n      },\n      composition: [\n        {\n          namespace: '',\n          key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a'\n        },\n        {\n          namespace: '',\n          key: '077ffa67-e78f-4e99-80e0-522ac33a3888'\n        }\n      ],\n      telemetry: {},\n      name: 'Condition Set',\n      type: 'conditionSet',\n      identifier: {\n        namespace: '',\n        key: '863012c1-f6ca-4ab0-aed7-fd43d5e4cd12'\n      }\n    };\n\n    beforeEach(() => {\n      conditionWidgetItem = {\n        label: 'Condition Widget',\n        conditionalLabel: '',\n        configuration: {},\n        name: 'Condition Widget',\n        type: 'conditionWidget',\n        identifier: {\n          namespace: '',\n          key: 'c5e636c1-6771-4c9c-b933-8665cab189b3'\n        }\n      };\n      selection = [\n        [\n          {\n            context: {\n              item: conditionWidgetItem,\n              supportsMultiSelect: false\n            }\n          }\n        ]\n      ];\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount({\n        components: {\n          StylesView\n        },\n        provide: {\n          openmct: openmct,\n          selection: selection,\n          stylesManager\n        },\n        template: '<styles-view ref=\"root\"/>'\n      });\n\n      component = vNode.componentInstance;\n      _destroy = destroy;\n\n      return nextTick().then(() => {\n        styleViewComponentObject = component.$refs.root;\n        styleViewComponentObject.setEditState(true);\n      });\n    });\n\n    afterEach(() => {\n      _destroy();\n    });\n\n    it('does not include the output label when the flag is disabled', () => {\n      styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;\n      styleViewComponentObject.conditionalStyles = [];\n      styleViewComponentObject.initializeConditionalStyles();\n      expect(styleViewComponentObject.conditionalStyles.length).toBe(2);\n\n      return nextTick().then(() => {\n        const hasNoOutput =\n          styleViewComponentObject.domainObject.configuration.objectStyles.styles.every((style) => {\n            return style.style.output === '' || style.style.output === undefined;\n          });\n\n        expect(hasNoOutput).toBeTrue();\n      });\n    });\n\n    it('includes the output label when the flag is enabled', () => {\n      styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;\n      styleViewComponentObject.conditionalStyles = [];\n      styleViewComponentObject.initializeConditionalStyles();\n      expect(styleViewComponentObject.conditionalStyles.length).toBe(2);\n\n      styleViewComponentObject.useConditionSetOutputAsLabel = true;\n      styleViewComponentObject.persistLabelConfiguration();\n\n      return nextTick().then(() => {\n        const outputs = styleViewComponentObject.domainObject.configuration.objectStyles.styles.map(\n          (style) => {\n            return style.style.output;\n          }\n        );\n        expect(outputs.join(',')).toEqual('Sine > 0,Default');\n      });\n    });\n  });\n\n  describe('the condition set usage for multiple display layout items', () => {\n    let displayLayoutItem;\n    let lineLayoutItem;\n    let boxLayoutItem;\n    let notCreatableObjectItem;\n    let notCreatableObject;\n    let selection;\n    let component;\n    let styleViewComponentObject;\n    let _destroy;\n    const conditionSetDomainObject = {\n      configuration: {\n        conditionTestData: [\n          {\n            telemetry: '',\n            metadata: '',\n            input: ''\n          }\n        ],\n        conditionCollection: [\n          {\n            id: '39584410-cbf9-499e-96dc-76f27e69885d',\n            configuration: {\n              name: 'Unnamed Condition',\n              output: 'Sine > 0',\n              trigger: 'all',\n              criteria: [\n                {\n                  id: '85fbb2f7-7595-42bd-9767-a932266c5225',\n                  telemetry: {\n                    namespace: '',\n                    key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a'\n                  },\n                  operation: 'greaterThan',\n                  input: ['0'],\n                  metadata: 'sin'\n                },\n                {\n                  id: '35400132-63b0-425c-ac30-8197df7d5862',\n                  telemetry: 'any',\n                  operation: 'enumValueIs',\n                  input: ['0'],\n                  metadata: 'state'\n                }\n              ]\n            },\n            summary:\n              'Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF '\n          },\n          {\n            isDefault: true,\n            id: '2532d90a-e0d6-4935-b546-3123522da2de',\n            configuration: {\n              name: 'Default',\n              output: 'Default',\n              trigger: 'all',\n              criteria: []\n            },\n            summary: ''\n          }\n        ]\n      },\n      composition: [\n        {\n          namespace: '',\n          key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a'\n        },\n        {\n          namespace: '',\n          key: '077ffa67-e78f-4e99-80e0-522ac33a3888'\n        }\n      ],\n      telemetry: {},\n      name: 'Condition Set',\n      type: 'conditionSet',\n      identifier: {\n        namespace: '',\n        key: '863012c1-f6ca-4ab0-aed7-fd43d5e4cd12'\n      }\n    };\n    const staticStyle = {\n      style: {\n        backgroundColor: '#666666',\n        border: '1px solid #00ffff'\n      }\n    };\n    const conditionalStyle = {\n      conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',\n      style: {\n        isStyleInvisible: '',\n        backgroundColor: '#666666',\n        border: '1px solid #ffff00'\n      }\n    };\n\n    beforeEach(() => {\n      displayLayoutItem = {\n        composition: [],\n        configuration: {\n          items: [\n            {\n              fill: '#666666',\n              stroke: '',\n              x: 1,\n              y: 1,\n              width: 10,\n              height: 5,\n              type: 'box-view',\n              id: '89b88746-d325-487b-aec4-11b79afff9e8'\n            },\n            {\n              fill: '#666666',\n              stroke: '',\n              x: 1,\n              y: 1,\n              width: 10,\n              height: 5,\n              type: 'ellipse-view',\n              id: '19b88746-d325-487b-aec4-11b79afff9z8'\n            },\n            {\n              x: 18,\n              y: 9,\n              x2: 23,\n              y2: 4,\n              stroke: '#666666',\n              type: 'line-view',\n              id: '57d49a28-7863-43bd-9593-6570758916f0'\n            },\n            {\n              width: 32,\n              height: 18,\n              x: 36,\n              y: 8,\n              identifier: {\n                key: '~TEST~image',\n                namespace: 'test-space'\n              },\n              hasFrame: true,\n              type: 'subobject-view',\n              id: '6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85'\n            }\n          ],\n          layoutGrid: [10, 10]\n        },\n        name: 'Display Layout',\n        type: 'layout',\n        identifier: {\n          namespace: '',\n          key: 'c5e636c1-6771-4c9c-b933-8665cab189b3'\n        }\n      };\n      lineLayoutItem = {\n        x: 18,\n        y: 9,\n        x2: 23,\n        y2: 4,\n        stroke: '#666666',\n        type: 'line-view',\n        id: '57d49a28-7863-43bd-9593-6570758916f0'\n      };\n      boxLayoutItem = {\n        fill: '#666666',\n        stroke: '',\n        x: 1,\n        y: 1,\n        width: 10,\n        height: 5,\n        type: 'box-view',\n        id: '89b88746-d325-487b-aec4-11b79afff9e8'\n      };\n      notCreatableObjectItem = {\n        width: 32,\n        height: 18,\n        x: 36,\n        y: 8,\n        identifier: {\n          key: '~TEST~image',\n          namespace: 'test-space'\n        },\n        hasFrame: true,\n        type: 'subobject-view',\n        id: '6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85'\n      };\n      notCreatableObject = {\n        identifier: {\n          key: '~TEST~image',\n          namespace: 'test-space'\n        },\n        name: 'test~image',\n        location: 'test-space:~TEST',\n        type: 'test.image',\n        telemetry: {\n          values: [\n            {\n              key: 'value',\n              name: 'Value',\n              hints: {\n                image: 1,\n                priority: 0\n              },\n              format: 'image',\n              source: 'value'\n            },\n            {\n              key: 'utc',\n              source: 'timestamp',\n              name: 'Timestamp',\n              format: 'iso',\n              hints: {\n                domain: 1,\n                priority: 1\n              }\n            }\n          ]\n        }\n      };\n      selection = [\n        [\n          {\n            context: {\n              layoutItem: lineLayoutItem,\n              index: 1\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        [\n          {\n            context: {\n              layoutItem: boxLayoutItem,\n              index: 0\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        [\n          {\n            context: {\n              item: notCreatableObject,\n              layoutItem: notCreatableObjectItem,\n              index: 2\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ]\n      ];\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount({\n        components: {\n          StylesView\n        },\n        provide: {\n          openmct: openmct,\n          selection: selection,\n          stylesManager\n        },\n        template: '<styles-view ref=\"root\"/>'\n      });\n\n      component = vNode.componentInstance;\n      _destroy = destroy;\n\n      return nextTick().then(() => {\n        styleViewComponentObject = component.$refs.root;\n        styleViewComponentObject.setEditState(true);\n      });\n    });\n\n    afterEach(() => {\n      _destroy();\n    });\n\n    it('initializes the items in the view', () => {\n      expect(styleViewComponentObject.items.length).toBe(3);\n    });\n\n    it('initializes conditional styles', () => {\n      styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;\n      styleViewComponentObject.conditionalStyles = [];\n      styleViewComponentObject.initializeConditionalStyles();\n      expect(styleViewComponentObject.conditionalStyles.length).toBe(2);\n    });\n\n    it('updates applicable conditional styles', () => {\n      styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;\n      styleViewComponentObject.conditionalStyles = [];\n      styleViewComponentObject.initializeConditionalStyles();\n      expect(styleViewComponentObject.conditionalStyles.length).toBe(2);\n      styleViewComponentObject.updateConditionalStyle(conditionalStyle, 'border');\n\n      return nextTick().then(() => {\n        expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();\n        [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {\n          const itemStyles =\n            styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;\n          expect(itemStyles.length).toBe(2);\n          const foundStyle = itemStyles.find((style) => {\n            return style.conditionId === conditionalStyle.conditionId;\n          });\n          expect(foundStyle).toBeDefined();\n          const applicableStyles = getApplicableStylesForItem(\n            styleViewComponentObject.domainObject,\n            item\n          );\n          const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']);\n          Object.keys(foundStyle.style).forEach((key) => {\n            if (key === 'output') {\n              return;\n            }\n\n            expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1);\n            expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]);\n          });\n        });\n      });\n    });\n\n    it('updates applicable static styles', () => {\n      styleViewComponentObject.updateStaticStyle(staticStyle, 'border');\n\n      return nextTick().then(() => {\n        expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();\n        [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {\n          const itemStyle =\n            styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;\n          expect(itemStyle).toBeDefined();\n          const applicableStyles = getApplicableStylesForItem(\n            styleViewComponentObject.domainObject,\n            item\n          );\n          const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']);\n          Object.keys(itemStyle.style).forEach((key) => {\n            expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1);\n            expect(itemStyle.style[key]).toEqual(staticStyle.style[key]);\n          });\n        });\n      });\n    });\n  });\n\n  describe('the condition check if old', () => {\n    let conditionSetDomainObject;\n\n    beforeEach(() => {\n      conditionSetDomainObject = {\n        configuration: {\n          conditionTestData: [\n            {\n              telemetry: '',\n              metadata: '',\n              input: ''\n            }\n          ],\n          conditionCollection: [\n            {\n              id: '39584410-cbf9-499e-96dc-76f27e69885d',\n              configuration: {\n                name: 'Unnamed Condition',\n                output: 'Any old telemetry',\n                trigger: 'all',\n                criteria: [\n                  {\n                    id: '35400132-63b0-425c-ac30-8197df7d5862',\n                    telemetry: 'any',\n                    operation: IS_OLD_KEY,\n                    input: ['0.2'],\n                    metadata: 'dataReceived'\n                  }\n                ]\n              },\n              summary: 'Match if all criteria are met: Any telemetry is old after 5 seconds'\n            },\n            {\n              isDefault: true,\n              id: '2532d90a-e0d6-4935-b546-3123522da2de',\n              configuration: {\n                name: 'Default',\n                output: 'Default',\n                trigger: 'all',\n                criteria: []\n              },\n              summary: ''\n            }\n          ]\n        },\n        composition: [\n          {\n            namespace: '',\n            key: 'test-object'\n          }\n        ],\n        telemetry: {},\n        name: 'Condition Set',\n        type: 'conditionSet',\n        identifier: {\n          namespace: '',\n          key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'\n        }\n      };\n    });\n\n    it('should evaluate as old when telemetry is not received in the allotted time', async () => {\n      let onAddResolve;\n      const onAddCalledPromise = new Promise((resolve) => {\n        onAddResolve = resolve;\n      });\n      const mockTelemetryCollection = {\n        load: jasmine.createSpy('load'),\n        on: jasmine.createSpy('on').and.callFake((event, callback) => {\n          if (event === 'add') {\n            onAddResolve();\n          }\n        })\n      };\n\n      openmct.telemetry = jasmine.createSpyObj('telemetry', [\n        'getMetadata',\n        'request',\n        'getValueFormatter',\n        'abortAllRequests',\n        'requestCollection'\n      ]);\n      openmct.telemetry.request.and.returnValue(Promise.resolve([]));\n      openmct.telemetry.getMetadata.and.returnValue({\n        ...testTelemetryObject.telemetry,\n        valueMetadatas: testTelemetryObject.telemetry.values,\n        valuesForHints: jasmine\n          .createSpy('valuesForHints')\n          .and.returnValue(testTelemetryObject.telemetry.values),\n        value: jasmine.createSpy('value').and.callFake((key) => {\n          return testTelemetryObject.telemetry.values.find((value) => value.key === key);\n        })\n      });\n      openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);\n      openmct.telemetry.getValueFormatter.and.returnValue({\n        parse: function (value) {\n          return value;\n        }\n      });\n\n      let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);\n      conditionMgr.on('conditionSetResultUpdated', mockListener);\n      conditionMgr.telemetryObjects = {\n        'test-object': testTelemetryObject\n      };\n      conditionMgr.updateConditionTelemetryObjects();\n      // Wait for the 'on' callback to be called\n      await onAddCalledPromise;\n\n      if (mockListener.calls.count() === 0) {\n        // Wait for the listener to be called, or timeout.\n        await new Promise((resolve) => mockListener.and.callFake(resolve));\n      }\n\n      expect(mockListener).toHaveBeenCalledWith({\n        output: 'Any old telemetry',\n        id: {\n          namespace: '',\n          key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'\n        },\n        conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',\n        utc: undefined\n      });\n    });\n\n    it('should not evaluate as old when telemetry is received in the allotted time', async () => {\n      const testDatum = {\n        'some-key2': '',\n        utc: 1,\n        testSource: '',\n        'some-key': null,\n        id: 'test-object'\n      };\n\n      let onAddResolve;\n      let onAddCallback;\n      const onAddCalledPromise = new Promise((resolve) => {\n        onAddResolve = resolve;\n      });\n\n      const mockTelemetryCollection = {\n        load: jasmine.createSpy('load'),\n        on: jasmine.createSpy('on').and.callFake((event, callback) => {\n          if (event === 'add') {\n            onAddCallback = callback;\n            onAddResolve();\n          }\n        })\n      };\n\n      openmct.telemetry = jasmine.createSpyObj('telemetry', [\n        'getMetadata',\n        'getValueFormatter',\n        'request',\n        'subscribe',\n        'requestCollection'\n      ]);\n      openmct.telemetry.subscribe.and.returnValue(function () {});\n      openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));\n      openmct.telemetry.getMetadata.and.returnValue({\n        ...testTelemetryObject.telemetry,\n        valueMetadatas: testTelemetryObject.telemetry.values,\n        valuesForHints: jasmine\n          .createSpy('valuesForHints')\n          .and.returnValue(testTelemetryObject.telemetry.values),\n        value: jasmine.createSpy('value').and.callFake((key) => {\n          return testTelemetryObject.telemetry.values.find((value) => value.key === key);\n        })\n      });\n      openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);\n      openmct.telemetry.getValueFormatter.and.returnValue({\n        parse: function (value) {\n          return value;\n        }\n      });\n\n      const date = 1;\n      conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =\n        ['0.4'];\n      let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);\n      conditionMgr.on('conditionSetResultUpdated', mockListener);\n      conditionMgr.telemetryObjects = {\n        'test-object': testTelemetryObject\n      };\n      conditionMgr.updateConditionTelemetryObjects();\n\n      // Wait for the 'on' callback to be called\n      await onAddCalledPromise;\n\n      // Simulate receiving telemetry data\n      onAddCallback([testDatum]);\n\n      if (mockListener.calls.count() === 0) {\n        // Wait for the listener to be called, or timeout.\n        await new Promise((resolve) => mockListener.and.callFake(resolve));\n      }\n\n      expect(mockListener).toHaveBeenCalledWith({\n        output: 'Default',\n        id: {\n          namespace: '',\n          key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'\n        },\n        conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',\n        utc: date\n      });\n    });\n  });\n\n  describe('the condition evaluation', () => {\n    let conditionSetDomainObject;\n\n    beforeEach(() => {\n      conditionSetDomainObject = {\n        configuration: {\n          conditionTestData: [\n            {\n              telemetry: '',\n              metadata: '',\n              input: ''\n            }\n          ],\n          conditionCollection: [\n            {\n              id: '39584410-cbf9-499e-96dc-76f27e69885f',\n              configuration: {\n                name: 'Unnamed Condition0',\n                output: 'Any telemetry less than 0',\n                trigger: 'all',\n                criteria: [\n                  {\n                    id: '35400132-63b0-425c-ac30-8197df7d5864',\n                    telemetry: 'any',\n                    operation: 'lessThan',\n                    input: ['0'],\n                    metadata: 'some-key'\n                  }\n                ]\n              },\n              summary: 'Match if all criteria are met: Any telemetry value is less than 0'\n            },\n            {\n              id: '39584410-cbf9-499e-96dc-76f27e69885d',\n              configuration: {\n                name: 'Unnamed Condition',\n                output: 'Any telemetry greater than 0',\n                trigger: 'all',\n                criteria: [\n                  {\n                    id: '35400132-63b0-425c-ac30-8197df7d5862',\n                    telemetry: 'any',\n                    operation: 'greaterThan',\n                    input: ['0'],\n                    metadata: 'some-key'\n                  }\n                ]\n              },\n              summary: 'Match if all criteria are met: Any telemetry value is greater than 0'\n            },\n            {\n              id: '39584410-cbf9-499e-96dc-76f27e69885e',\n              configuration: {\n                name: 'Unnamed Condition1',\n                output: 'Any telemetry greater than 1',\n                trigger: 'all',\n                criteria: [\n                  {\n                    id: '35400132-63b0-425c-ac30-8197df7d5863',\n                    telemetry: 'any',\n                    operation: 'greaterThan',\n                    input: ['1'],\n                    metadata: 'some-key'\n                  }\n                ]\n              },\n              summary: 'Match if all criteria are met: Any telemetry value is greater than 1'\n            },\n            {\n              isDefault: true,\n              id: '2532d90a-e0d6-4935-b546-3123522da2de',\n              configuration: {\n                name: 'Default',\n                output: 'Default',\n                trigger: 'all',\n                criteria: []\n              },\n              summary: ''\n            }\n          ]\n        },\n        composition: [\n          {\n            namespace: '',\n            key: 'test-object'\n          }\n        ],\n        telemetry: {},\n        name: 'Condition Set',\n        type: 'conditionSet',\n        identifier: {\n          namespace: '',\n          key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'\n        }\n      };\n    });\n\n    it('should stop evaluating conditions when a condition evaluates to true', () => {\n      const date = Date.now();\n      let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);\n\n      openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');\n      openmct.telemetry.getMetadata.and.returnValue({\n        ...testTelemetryObject.telemetry,\n        valueMetadatas: testTelemetryObject.telemetry.values,\n        valuesForHints: jasmine\n          .createSpy('valuesForHints')\n          .and.returnValue(testTelemetryObject.telemetry.values),\n        value: jasmine.createSpy('value').and.callFake((key) => {\n          return testTelemetryObject.telemetry.values.find((value) => value.key === key);\n        })\n      });\n      conditionMgr.on('conditionSetResultUpdated', mockListener);\n      conditionMgr.telemetryObjects = {\n        'test-object': testTelemetryObject\n      };\n      conditionMgr.updateConditionTelemetryObjects();\n      conditionMgr.telemetryReceived(testTelemetryObject, [\n        {\n          'some-key': 2,\n          utc: date\n        }\n      ]);\n      let result = conditionMgr.conditions.map((condition) => condition.result);\n      expect(result[2]).toBeUndefined();\n    });\n  });\n\n  describe('canView of ConditionSetViewProvider', () => {\n    let conditionSetView;\n    const testViewObject = {\n      id: 'test-object',\n      type: 'conditionSet',\n      configuration: {\n        conditionCollection: []\n      }\n    };\n\n    beforeEach(() => {\n      const applicableViews = openmct.objectViews.get(testViewObject, []);\n      conditionSetView = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'conditionSet.view'\n      );\n    });\n\n    it('provides a view', () => {\n      expect(conditionSetView).toBeDefined();\n    });\n\n    it('returns true for type `conditionSet` and is a navigated Object', () => {\n      openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);\n\n      const isCanView = conditionSetView.canView(testViewObject, []);\n\n      expect(isCanView).toBe(true);\n    });\n\n    it('returns false for type `conditionSet` and is not a navigated Object', () => {\n      openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);\n\n      const isCanView = conditionSetView.canView(testViewObject, []);\n\n      expect(isCanView).toBe(false);\n    });\n\n    it('returns false for type `notConditionSet` and is a navigated Object', () => {\n      openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);\n      testViewObject.type = 'notConditionSet';\n      const isCanView = conditionSetView.canView(testViewObject, []);\n\n      expect(isCanView).toBe(false);\n    });\n  });\n\n  describe('The Style Rule Manager', () => {\n    it('should subscribe to the conditionSet after the editor saves', async () => {\n      const stylesObject = {\n        styles: [\n          {\n            conditionId: 'a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd',\n            style: {\n              backgroundColor: '#38761d',\n              border: '',\n              color: '#073763',\n              isStyleInvisible: ''\n            }\n          },\n          {\n            conditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e',\n            style: {\n              backgroundColor: '#980000',\n              border: '',\n              color: '#ff9900',\n              isStyleInvisible: ''\n            }\n          }\n        ],\n        staticStyle: {\n          style: {\n            backgroundColor: '',\n            border: '',\n            color: ''\n          }\n        },\n        selectedConditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e',\n        defaultConditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e',\n        conditionSetIdentifier: {\n          namespace: '',\n          key: '035c589c-d98f-429e-8b89-d76bd8d22b29'\n        }\n      };\n      openmct.$injector = jasmine.createSpyObj('$injector', ['get']);\n      openmct.telemetry = jasmine.createSpyObj('telemetry', [\n        'isTelemetryObject',\n        'request',\n        'subscribe',\n        'getMetadata',\n        'getValueFormatter',\n        'requestCollection'\n      ]);\n      openmct.telemetry.subscribe.and.returnValue(function () {});\n      openmct.telemetry.request.and.returnValue(Promise.resolve([]));\n      openmct.telemetry.isTelemetryObject.and.returnValue(true);\n      openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);\n      openmct.telemetry.getMetadata.and.returnValue({\n        ...testTelemetryObject.telemetry,\n        valueMetadatas: testTelemetryObject.telemetry.values,\n        valuesForHints: jasmine\n          .createSpy('valuesForHints')\n          .and.returnValue(testTelemetryObject.telemetry.values),\n        value: jasmine.createSpy('value').and.callFake((key) => {\n          return testTelemetryObject.telemetry.values.find((value) => value.key === key);\n        })\n      });\n      openmct.telemetry.getValueFormatter.and.returnValue({\n        parse: function (value) {\n          return value;\n        }\n      });\n      openmct.telemetry.requestCollection.and.returnValue({\n        load: jasmine.createSpy('load'),\n        on: jasmine.createSpy('on')\n      });\n\n      const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);\n      spyOn(styleRuleManger, 'subscribeToConditionSet');\n      openmct.editor.edit();\n      await openmct.editor.save();\n      expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/utils/constants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const TRIGGER = {\n  ANY: 'any',\n  ALL: 'all',\n  NOT: 'not',\n  XOR: 'xor'\n};\n\nexport const TRIGGER_LABEL = {\n  any: 'any criteria are met',\n  all: 'all criteria are met',\n  not: 'no criteria are met',\n  xor: 'only one criterion is met'\n};\n\nexport const TRIGGER_CONJUNCTION = {\n  any: 'or',\n  all: 'and',\n  not: 'and',\n  xor: 'or'\n};\n\nexport const STYLE_CONSTANTS = {\n  isStyleInvisible: 'is-style-invisible',\n  borderColorTitle: 'Set border color',\n  textColorTitle: 'Set text color',\n  backgroundColorTitle: 'Set background color',\n  imagePropertiesTitle: 'Edit image properties',\n  visibilityHidden: 'Hidden',\n  visibilityVisible: 'Visible'\n};\n\nexport const ERROR = {\n  TELEMETRY_NOT_FOUND: {\n    errorText: 'Telemetry not found for criterion'\n  },\n  CONDITION_NOT_FOUND: {\n    errorText: 'Condition not found'\n  }\n};\n\nexport const IS_OLD_KEY = 'isStale';\nexport const IS_STALE_KEY = 'isStale.new';\n"
  },
  {
    "path": "src/plugins/condition/utils/evaluator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { TRIGGER } from './constants.js';\n\nexport function evaluateResults(results, trigger) {\n  if (trigger && trigger === TRIGGER.XOR) {\n    return matchExact(results, 1);\n  } else if (trigger && trigger === TRIGGER.NOT) {\n    return matchExact(results, 0);\n  } else if (trigger && trigger === TRIGGER.ALL) {\n    return matchAll(results);\n  } else {\n    return matchAny(results);\n  }\n}\n\nfunction matchAll(results) {\n  for (let result of results) {\n    if (result !== true) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction matchAny(results) {\n  for (let result of results) {\n    if (result === true) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nfunction matchExact(results, target) {\n  let matches = 0;\n  for (let result of results) {\n    if (result === true) {\n      matches++;\n    }\n\n    if (matches > target) {\n      return false;\n    }\n  }\n\n  return matches === target;\n}\n"
  },
  {
    "path": "src/plugins/condition/utils/evaluatorSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { TRIGGER } from './constants.js';\nimport { evaluateResults } from './evaluator.js';\n\ndescribe('evaluate results', () => {\n  // const allTrue = [true, true, true, true, true];\n  // const oneTrue = [false, false, false, false, true];\n  // const multipleTrue = [false, true, false, true, false];\n  // const noneTrue = [false, false, false, false, false];\n  // const allTrueWithUndefined = [true, true, true, undefined, true];\n  // const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];\n  // const multipleTrueWithUndefined = [true, undefined, true, undefined, true];\n  // const allUndefined = [undefined, undefined, undefined, undefined, undefined];\n  // const singleTrue = [true];\n  // const singleFalse = [false];\n  // const singleUndefined = [undefined];\n  // const empty = [];\n\n  const tests = [\n    {\n      name: 'allTrue',\n      values: [true, true, true, true, true],\n      any: true,\n      all: true,\n      not: false,\n      xor: false\n    },\n    {\n      name: 'oneTrue',\n      values: [false, false, false, false, true],\n      any: true,\n      all: false,\n      not: false,\n      xor: true\n    },\n    {\n      name: 'multipleTrue',\n      values: [false, true, false, true, false],\n      any: true,\n      all: false,\n      not: false,\n      xor: false\n    },\n    {\n      name: 'noneTrue',\n      values: [false, false, false, false, false],\n      any: false,\n      all: false,\n      not: true,\n      xor: false\n    },\n    {\n      name: 'allTrueWithUndefined',\n      values: [true, true, true, undefined, true],\n      any: true,\n      all: false,\n      not: false,\n      xor: false\n    },\n    {\n      name: 'oneTrueWithUndefined',\n      values: [undefined, undefined, undefined, undefined, true],\n      any: true,\n      all: false,\n      not: false,\n      xor: true\n    },\n    {\n      name: 'multipleTrueWithUndefined',\n      values: [true, undefined, true, undefined, true],\n      any: true,\n      all: false,\n      not: false,\n      xor: false\n    },\n    {\n      name: 'allUndefined',\n      values: [undefined, undefined, undefined, undefined, undefined],\n      any: false,\n      all: false,\n      not: true,\n      xor: false\n    },\n    {\n      name: 'singleTrue',\n      values: [true],\n      any: true,\n      all: true,\n      not: false,\n      xor: true\n    },\n    {\n      name: 'singleFalse',\n      values: [false],\n      any: false,\n      all: false,\n      not: true,\n      xor: false\n    },\n    {\n      name: 'singleUndefined',\n      values: [undefined],\n      any: false,\n      all: false,\n      not: true,\n      xor: false\n    }\n    // , {\n    //     name: 'empty',\n    //     values: [],\n    //     any: false,\n    //     all: false,\n    //     not: true,\n    //     xor: false\n    // }\n  ];\n\n  describe(`based on trigger ${TRIGGER.ANY}`, () => {\n    it('should evaluate to expected result', () => {\n      tests.forEach((test) => {\n        const result = evaluateResults(test.values, TRIGGER.ANY);\n        expect(result).toEqual(test[TRIGGER.ANY]);\n      });\n    });\n  });\n\n  describe(`based on trigger ${TRIGGER.ALL}`, () => {\n    it('should evaluate to expected result', () => {\n      tests.forEach((test) => {\n        const result = evaluateResults(test.values, TRIGGER.ALL);\n        expect(result).toEqual(test[TRIGGER.ALL]);\n      });\n    });\n  });\n\n  describe(`based on trigger ${TRIGGER.NOT}`, () => {\n    it('should evaluate to expected result', () => {\n      tests.forEach((test) => {\n        const result = evaluateResults(test.values, TRIGGER.NOT);\n        expect(result).toEqual(test[TRIGGER.NOT]);\n      });\n    });\n  });\n\n  describe(`based on trigger ${TRIGGER.XOR}`, () => {\n    it('should evaluate to expected result', () => {\n      tests.forEach((test) => {\n        const result = evaluateResults(test.values, TRIGGER.XOR);\n        expect(result).toEqual(test[TRIGGER.XOR]);\n      });\n    });\n  });\n\n  // it('should evaluate to true if trigger is NOT', () => {\n  //     const results = {\n  //         result: false,\n  //         result1: false,\n  //         result2: false\n  //     };\n  //     const result = computeConditionByLimit(results, 0);\n  //     expect(result).toBeTrue();\n  // });\n\n  // it('should evaluate to false if trigger is NOT', () => {\n  //     const results = {\n  //         result: true,\n  //         result1: false,\n  //         result2: false\n  //     };\n  //     const result = computeConditionByLimit(results, 0);\n  //     expect(result).toBeFalse();\n  // });\n\n  // it('should evaluate to true if trigger is XOR', () => {\n  //     const results = {\n  //         result: false,\n  //         result1: true,\n  //         result2: false\n  //     };\n  //     const result = computeConditionByLimit(results, 1);\n  //     expect(result).toBeTrue();\n  // });\n\n  // it('should evaluate to false if trigger is XOR', () => {\n  //     const results = {\n  //         result: false,\n  //         result1: true,\n  //         result2: true\n  //     };\n  //     const result = computeConditionByLimit(results, 1);\n  //     expect(result).toBeFalse();\n  // });\n});\n"
  },
  {
    "path": "src/plugins/condition/utils/operations.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { IS_OLD_KEY, IS_STALE_KEY } from './constants.js';\n\nfunction convertToNumbers(input) {\n  let numberInputs = [];\n  input.forEach((inputValue) => numberInputs.push(Number(inputValue)));\n\n  return numberInputs;\n}\n\nfunction convertToStrings(input) {\n  let stringInputs = [];\n  input.forEach((inputValue) =>\n    stringInputs.push(inputValue !== undefined ? inputValue.toString() : '')\n  );\n\n  return stringInputs;\n}\n\nfunction joinValues(values, length) {\n  return values.slice(0, length).join(', ');\n}\n\nexport const OPERATIONS = [\n  {\n    name: 'equalTo',\n    operation: function (input) {\n      return Number(input[0]) === Number(input[1]);\n    },\n    text: 'is equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'notEqualTo',\n    operation: function (input) {\n      return Number(input[0]) !== Number(input[1]);\n    },\n    text: 'is not equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is not ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'greaterThan',\n    operation: function (input) {\n      return Number(input[0]) > Number(input[1]);\n    },\n    text: 'is greater than',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' > ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'lessThan',\n    operation: function (input) {\n      return Number(input[0]) < Number(input[1]);\n    },\n    text: 'is less than',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' < ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'greaterThanOrEq',\n    operation: function (input) {\n      return Number(input[0]) >= Number(input[1]);\n    },\n    text: 'is greater than or equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' >= ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'lessThanOrEq',\n    operation: function (input) {\n      return Number(input[0]) <= Number(input[1]);\n    },\n    text: 'is less than or equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' <= ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'between',\n    operation: function (input) {\n      let numberInputs = convertToNumbers(input);\n      let larger = Math.max(...numberInputs.slice(1, 3));\n      let smaller = Math.min(...numberInputs.slice(1, 3));\n\n      return numberInputs[0] > smaller && numberInputs[0] < larger;\n    },\n    text: 'is between',\n    appliesTo: ['number'],\n    inputCount: 2,\n    getDescription: function (values) {\n      return ' is between ' + values[0] + ' and ' + values[1];\n    }\n  },\n  {\n    name: 'notBetween',\n    operation: function (input) {\n      let numberInputs = convertToNumbers(input);\n      let larger = Math.max(...numberInputs.slice(1, 3));\n      let smaller = Math.min(...numberInputs.slice(1, 3));\n\n      return numberInputs[0] < smaller || numberInputs[0] > larger;\n    },\n    text: 'is not between',\n    appliesTo: ['number'],\n    inputCount: 2,\n    getDescription: function (values) {\n      return ' is not between ' + values[0] + ' and ' + values[1];\n    }\n  },\n  {\n    name: 'textContains',\n    operation: function (input) {\n      return input[0] && input[1] && input[0].includes(input[1]);\n    },\n    text: 'text contains',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' contains ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'textDoesNotContain',\n    operation: function (input) {\n      return input[0] && input[1] && !input[0].includes(input[1]);\n    },\n    text: 'text does not contain',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' does not contain ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'textStartsWith',\n    operation: function (input) {\n      return input[0].startsWith(input[1]);\n    },\n    text: 'text starts with',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' starts with ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'textEndsWith',\n    operation: function (input) {\n      return input[0].endsWith(input[1]);\n    },\n    text: 'text ends with',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' ends with ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'textIsExactly',\n    operation: function (input) {\n      return input[0] === input[1];\n    },\n    text: 'text is exactly',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is exactly ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'isUndefined',\n    operation: function (input) {\n      return typeof input[0] === 'undefined';\n    },\n    text: 'is undefined',\n    appliesTo: ['string', 'number', 'enum'],\n    inputCount: 0,\n    getDescription: function () {\n      return ' is undefined';\n    }\n  },\n  {\n    name: 'isDefined',\n    operation: function (input) {\n      return typeof input[0] !== 'undefined';\n    },\n    text: 'is defined',\n    appliesTo: ['string', 'number', 'enum'],\n    inputCount: 0,\n    getDescription: function () {\n      return ' is defined';\n    }\n  },\n  {\n    name: 'enumValueIs',\n    operation: function (input) {\n      let stringInputs = convertToStrings(input);\n\n      return stringInputs[0] === stringInputs[1];\n    },\n    text: 'is',\n    appliesTo: ['enum'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'enumValueIsNot',\n    operation: function (input) {\n      let stringInputs = convertToStrings(input);\n\n      return stringInputs[0] !== stringInputs[1];\n    },\n    text: 'is not',\n    appliesTo: ['enum'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is not ' + joinValues(values, 1);\n    }\n  },\n  {\n    name: 'isOneOf',\n    operation: function (input) {\n      const lhsValue = input[0] !== undefined ? input[0].toString() : '';\n      if (input[1]) {\n        const values = input[1].split(',');\n\n        return values.some((value) => lhsValue === value.toString().trim());\n      }\n\n      return false;\n    },\n    text: 'is one of',\n    appliesTo: ['string', 'number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is one of ' + values[0];\n    }\n  },\n  {\n    name: 'isNotOneOf',\n    operation: function (input) {\n      const lhsValue = input[0] !== undefined ? input[0].toString() : '';\n      if (input[1]) {\n        const values = input[1].split(',');\n        const found = values.some((value) => lhsValue === value.toString().trim());\n\n        return !found;\n      }\n\n      return false;\n    },\n    text: 'is not one of',\n    appliesTo: ['string', 'number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is not one of ' + values[0];\n    }\n  },\n  {\n    name: IS_OLD_KEY,\n    operation: function () {\n      return false;\n    },\n    text: 'is older than',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ` is older than ${values[0] || ''} seconds`;\n    }\n  },\n  {\n    name: IS_STALE_KEY,\n    operation: function () {\n      return false;\n    },\n    text: 'is stale',\n    appliesTo: ['number'],\n    inputCount: 0,\n    getDescription: function () {\n      return ' is stale';\n    }\n  }\n];\n\nexport const INPUT_TYPES = {\n  string: 'text',\n  number: 'number'\n};\n\nexport function getOperatorText(operationName, values) {\n  const found = OPERATIONS.find((operation) => operation.name === operationName);\n\n  return found?.getDescription(values) ?? '';\n}\n"
  },
  {
    "path": "src/plugins/condition/utils/operationsSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { OPERATIONS } from './operations.js';\nlet isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isOneOf');\nlet isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isNotOneOf');\nlet isBetween = OPERATIONS.find((operation) => operation.name === 'between');\nlet isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');\nlet enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');\nlet enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');\n\ndescribe('operations', function () {\n  it('should evaluate isOneOf to true for number inputs', () => {\n    const inputs = [45, '5,6,45,8'];\n    expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isOneOf to true for string inputs', () => {\n    const inputs = ['45', ' 45, 645, 4,8 '];\n    expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isNotOneOf to true for number inputs', () => {\n    const inputs = [45, '5,6,4,8'];\n    expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isNotOneOf to true for string inputs', () => {\n    const inputs = ['45', ' 5,645, 4,8 '];\n    expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isOneOf to false for number inputs', () => {\n    const inputs = [4, '5, 6, 7, 8'];\n    expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate isOneOf to false for string inputs', () => {\n    const inputs = ['4', '5,645 ,7,8'];\n    expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate isNotOneOf to false for number inputs', () => {\n    const inputs = [4, '5,4, 7,8'];\n    expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate isNotOneOf to false for string inputs', () => {\n    const inputs = ['4', '5,46,4,8'];\n    expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate isBetween to true', () => {\n    const inputs = ['4', '3', '89'];\n    expect(Boolean(isBetween.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isNotBetween to true', () => {\n    const inputs = ['45', '100', '89'];\n    expect(Boolean(isNotBetween.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate isBetween to false', () => {\n    const inputs = ['4', '100', '89'];\n    expect(Boolean(isBetween.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate isNotBetween to false', () => {\n    const inputs = ['45', '30', '50'];\n    expect(Boolean(isNotBetween.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIs to true for number inputs', () => {\n    const inputs = [1, '1'];\n    expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate enumValueIs to true for string inputs', () => {\n    const inputs = ['45', '45'];\n    expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate enumValueIsNot to true for number inputs', () => {\n    const inputs = [45, '46'];\n    expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate enumValueIsNot to true for string inputs', () => {\n    const inputs = ['45', '46'];\n    expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue();\n  });\n\n  it('should evaluate enumValueIs to false for number inputs', () => {\n    const inputs = [1, '2'];\n    expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIs to false for string inputs', () => {\n    const inputs = ['45', '46'];\n    expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIsNot to false for number inputs', () => {\n    const inputs = [45, '45'];\n    expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIsNot to false for string inputs', () => {\n    const inputs = ['45', '45'];\n    expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIs to false for undefined input', () => {\n    const inputs = [undefined, '45'];\n    expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse();\n  });\n\n  it('should evaluate enumValueIsNot to true for undefined input', () => {\n    const inputs = [undefined, '45'];\n    expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue();\n  });\n});\n"
  },
  {
    "path": "src/plugins/condition/utils/styleUtils.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport isEmpty from 'lodash/isEmpty.js';\n\nconst NONE_VALUE = '__no_value';\n\nconst styleProps = {\n  backgroundColor: {\n    svgProperty: 'fill',\n    noneValue: NONE_VALUE,\n    applicableForType: (type) => {\n      return !type\n        ? true\n        : type === 'text-view' ||\n            type === 'telemetry-view' ||\n            type === 'box-view' ||\n            type === 'ellipse-view' ||\n            type === 'subobject-view';\n    }\n  },\n  border: {\n    svgProperty: 'stroke',\n    noneValue: NONE_VALUE,\n    applicableForType: (type) => {\n      return !type\n        ? true\n        : type === 'text-view' ||\n            type === 'telemetry-view' ||\n            type === 'box-view' ||\n            type === 'ellipse-view' ||\n            type === 'image-view' ||\n            type === 'line-view' ||\n            type === 'subobject-view';\n    }\n  },\n  color: {\n    svgProperty: 'color',\n    noneValue: NONE_VALUE,\n    applicableForType: (type) => {\n      return !type\n        ? true\n        : type === 'text-view' || type === 'telemetry-view' || type === 'subobject-view';\n    }\n  },\n  imageUrl: {\n    svgProperty: 'url',\n    noneValue: '',\n    applicableForType: (type) => {\n      return !type ? false : type === 'image-view';\n    }\n  }\n};\n\nfunction aggregateStyleValues(accumulator, currentStyle) {\n  const styleKeys = Object.keys(currentStyle);\n  const properties = Object.keys(styleProps);\n  properties.forEach((property) => {\n    if (!accumulator[property]) {\n      accumulator[property] = [];\n    }\n\n    const found = styleKeys.find((key) => key === property);\n    if (found) {\n      accumulator[property].push(currentStyle[found]);\n    }\n  });\n\n  return accumulator;\n}\n\nfunction getStaticStyleForItem(domainObject, id) {\n  let domainObjectStyles =\n    domainObject && domainObject.configuration && domainObject.configuration.objectStyles;\n  if (domainObjectStyles) {\n    if (id) {\n      if (domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {\n        return domainObjectStyles[id].staticStyle.style;\n      }\n    } else if (domainObjectStyles.staticStyle) {\n      return domainObjectStyles.staticStyle.style;\n    }\n  }\n}\n\n// Returns a union of styles used by multiple items.\n// Styles that are common to all items but don't have the same value are added to the mixedStyles list\nexport function getConsolidatedStyleValues(multipleItemStyles) {\n  let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});\n\n  let styleValues = {};\n  let mixedStyles = [];\n  const properties = Object.keys(styleProps);\n  properties.forEach((property) => {\n    const values = aggregatedStyleValues[property];\n    if (values && values.length) {\n      if (values.every((value) => value === values[0])) {\n        styleValues[property] = values[0];\n      } else {\n        styleValues[property] = '';\n        mixedStyles.push(property);\n      }\n    }\n  });\n\n  return {\n    styles: styleValues,\n    mixedStyles\n  };\n}\n\nexport function getConditionalStyleForItem(domainObject, id) {\n  let domainObjectStyles =\n    domainObject && domainObject.configuration && domainObject.configuration.objectStyles;\n  if (domainObjectStyles) {\n    if (id) {\n      if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {\n        return domainObjectStyles[id].styles;\n      }\n    } else if (domainObjectStyles.conditionSetIdentifier) {\n      return domainObjectStyles.styles;\n    }\n  }\n}\n\nexport function getConditionSetIdentifierForItem(domainObject, id) {\n  let domainObjectStyles =\n    domainObject && domainObject.configuration && domainObject.configuration.objectStyles;\n  if (domainObjectStyles) {\n    if (id) {\n      if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {\n        return domainObjectStyles[id].conditionSetIdentifier;\n      }\n    } else if (domainObjectStyles.conditionSetIdentifier) {\n      return domainObjectStyles.conditionSetIdentifier;\n    }\n  }\n}\n\n//Returns either existing static styles or uses SVG defaults if available\nexport function getApplicableStylesForItem(domainObject, item) {\n  const type = item && item.type;\n  const id = item && item.id;\n  let style = {};\n\n  let staticStyle = getStaticStyleForItem(domainObject, id);\n\n  const properties = Object.keys(styleProps);\n  properties.forEach((property) => {\n    const styleProp = styleProps[property];\n    if (styleProp.applicableForType(type)) {\n      let defaultValue;\n      if (staticStyle) {\n        defaultValue = staticStyle[property];\n      } else if (item) {\n        defaultValue = item[styleProp.svgProperty];\n      }\n\n      style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;\n    }\n  });\n\n  return style;\n}\n\nexport function getStylesWithoutNoneValue(style) {\n  if (isEmpty(style) || !style) {\n    return;\n  }\n\n  let styleObj = {};\n  const keys = Object.keys(style);\n  keys.forEach((key) => {\n    if (typeof style[key] === 'string') {\n      if (style[key].indexOf('__no_value') > -1) {\n        style[key] = '';\n      } else {\n        styleObj[key] = style[key];\n      }\n    }\n  });\n\n  return styleObj;\n}\n"
  },
  {
    "path": "src/plugins/condition/utils/time.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction updateLatestTimeStamp(timestamp, timeSystems) {\n  let latest = {};\n\n  timeSystems.forEach((timeSystem) => {\n    latest[timeSystem.key] = timestamp[timeSystem.key];\n  });\n\n  return latest;\n}\n\nexport function getLatestTimestamp(\n  currentTimestamp,\n  compareTimestamp,\n  timeSystems,\n  currentTimeSystem\n) {\n  let latest = { ...currentTimestamp };\n  const compare = { ...compareTimestamp };\n  const key = currentTimeSystem.key;\n\n  if (!latest || !latest[key]) {\n    latest = updateLatestTimeStamp(compare, timeSystems);\n  }\n\n  if (compare[key] > latest[key]) {\n    latest = updateLatestTimeStamp(compare, timeSystems);\n  }\n\n  return latest;\n}\n\nexport function checkIfOld(callback, timeout) {\n  let oldCheckTimer = setTimeout(() => {\n    clearTimeout(oldCheckTimer);\n    callback();\n  }, timeout);\n\n  return {\n    update: (data) => {\n      if (oldCheckTimer) {\n        clearTimeout(oldCheckTimer);\n      }\n\n      oldCheckTimer = setTimeout(() => {\n        clearTimeout(oldCheckTimer);\n        callback(data);\n      }, timeout);\n    },\n    clear: () => {\n      if (oldCheckTimer) {\n        clearTimeout(oldCheckTimer);\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/condition/utils/timeSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { checkIfOld } from './time.js';\n\ndescribe('time related utils', () => {\n  let subscription;\n  let mockListener;\n\n  beforeEach(() => {\n    mockListener = jasmine.createSpy('listener');\n    subscription = checkIfOld(mockListener, 100);\n  });\n\n  describe('check if old', () => {\n    it('should call listeners when old', (done) => {\n      setTimeout(() => {\n        expect(mockListener).toHaveBeenCalled();\n        done();\n      }, 200);\n    });\n\n    it('should update the subscription', (done) => {\n      function updated() {\n        setTimeout(() => {\n          expect(mockListener).not.toHaveBeenCalled();\n          done();\n        }, 50);\n      }\n\n      setTimeout(() => {\n        subscription.update();\n        updated();\n      }, 50);\n    });\n\n    it('should clear the subscription', (done) => {\n      subscription.clear();\n\n      setTimeout(() => {\n        expect(mockListener).not.toHaveBeenCalled();\n        done();\n      }, 200);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/conditionWidget/ConditionWidgetViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport ConditionWidgetComponent from './components/ConditionWidget.vue';\n\nexport default function ConditionWidget(openmct) {\n  return {\n    key: 'conditionWidget',\n    name: 'Condition Widget',\n    cssClass: 'icon-condition-widget',\n    canView: function (domainObject) {\n      return domainObject.type === 'conditionWidget';\n    },\n    canEdit: function (domainObject) {\n      return domainObject.type === 'conditionWidget';\n    },\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                ConditionWidgetComponent: ConditionWidgetComponent\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: '<condition-widget-component></condition-widget-component>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/conditionWidget/components/ConditionWidget.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    ref=\"conditionWidgetElement\"\n    class=\"c-condition-widget u-style-receiver js-style-receiver\"\n    @mouseover.ctrl=\"showToolTip\"\n    @mouseleave=\"hideToolTip\"\n  >\n    <component :is=\"urlDefined ? 'a' : 'div'\" class=\"c-condition-widget__label-wrapper\" :href=\"url\">\n      <div class=\"c-condition-widget__label\">{{ label }}</div>\n    </component>\n  </div>\n</template>\n\n<script>\nimport { sanitizeUrl } from '@braintree/sanitize-url';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\n\nexport default {\n  mixins: [tooltipHelpers],\n  inject: ['openmct', 'domainObject'],\n  data: function () {\n    return {\n      conditionalLabel: ''\n    };\n  },\n  computed: {\n    urlDefined() {\n      return this.domainObject.url?.length > 0;\n    },\n    url() {\n      return this.urlDefined ? sanitizeUrl(this.domainObject.url) : null;\n    },\n    useConditionSetOutputAsLabel() {\n      return (\n        this.conditionSetIdentifier && this.domainObject.configuration.useConditionSetOutputAsLabel\n      );\n    },\n    conditionSetIdentifier() {\n      return this.domainObject.configuration?.objectStyles?.conditionSetIdentifier;\n    },\n    label() {\n      return this.useConditionSetOutputAsLabel ? this.conditionalLabel : this.domainObject.label;\n    }\n  },\n  watch: {\n    conditionSetIdentifier: {\n      handler(newValue, oldValue) {\n        if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {\n          return;\n        }\n\n        this.listenToConditionSetChanges();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    if (this.domainObject) {\n      this.listenToConditionSetChanges();\n    }\n  },\n  unmounted() {\n    this.stopListeningToConditionSetChanges();\n  },\n  methods: {\n    async listenToConditionSetChanges() {\n      if (!this.conditionSetIdentifier) {\n        return;\n      }\n\n      const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);\n      this.stopListeningToConditionSetChanges();\n\n      if (!conditionSetDomainObject) {\n        this.openmct.notifications.alert('Unable to find condition set');\n      }\n\n      this.telemetryCollection = this.openmct.telemetry.requestCollection(\n        conditionSetDomainObject,\n        {\n          size: 1,\n          strategy: 'latest'\n        }\n      );\n\n      this.telemetryCollection.on('add', this.updateConditionLabel, this);\n      this.telemetryCollection.load();\n    },\n    stopListeningToConditionSetChanges() {\n      if (this.telemetryCollection) {\n        this.telemetryCollection.off('add', this.updateConditionLabel, this);\n        this.telemetryCollection.destroy();\n        this.telemetryCollection = null;\n      }\n    },\n    updateConditionLabel([latestDatum]) {\n      if (!this.conditionSetIdentifier) {\n        this.stopListeningToConditionSetChanges();\n\n        return;\n      }\n\n      this.conditionalLabel = latestDatum.output || '';\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'conditionWidgetElement');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/conditionWidget/components/condition-widget.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n.c-condition-widget {\n  $shdwSize: 3px;\n  @include userSelectNone();\n  background-color: rgba(\n    $colorBodyFg,\n    0.1\n  ); // Give a little presence if the user hasn't defined a fill color\n  border-radius: $basicCr;\n  border: 1px solid transparent;\n  display: block;\n  max-width: max-content;\n\n  a {\n    display: block;\n    color: inherit;\n  }\n}\n\n.c-condition-widget__label {\n  // Either a <div> or an <a> tag\n  padding: $interiorMargin $interiorMargin * 1.5;\n  text-align: center;\n  white-space: normal;\n}\n\n// Make Condition Widget expand when in a hidden frame Layout context\n// For both static and Flexible Layouts\n.c-so-view--conditionWidget.c-so-view--no-frame {\n  .c-condition-widget {\n    @include abs();\n    max-width: unset;\n\n    &__label-wrapper {\n      @include abs();\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n  }\n\n  .c-so-view__frame-controls {\n    display: none;\n  }\n}\n\n// Add some margin when a Condition Widget is in a Flexible Layout\n.c-fl .c-so-view--no-frame .c-condition-widget {\n  @include abs(1px);\n}\n\n// When the widget is in the main view, center it in the space\n.l-shell__main-container > * > .c-condition-widget {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n"
  },
  {
    "path": "src/plugins/conditionWidget/conditionWidgetStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function conditionWidgetStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return domainObject?.type === 'conditionWidget' && !domainObject.configuration?.objectStyles;\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      domainObject.configuration.objectStyles = {};\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/conditionWidget/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport conditionWidgetStylesInterceptor from './conditionWidgetStylesInterceptor.js';\nimport ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));\n    openmct.objects.addGetInterceptor(conditionWidgetStylesInterceptor(openmct));\n\n    openmct.types.addType('conditionWidget', {\n      key: 'conditionWidget',\n      name: 'Condition Widget',\n      description:\n        'A button that can be used on its own, or dynamically styled with a Condition Set.',\n      creatable: true,\n      cssClass: 'icon-condition-widget',\n      initialize(domainObject) {\n        domainObject.configuration = {\n          objectStyles: {}\n        };\n        domainObject.label = 'Condition Widget';\n        domainObject.conditionalLabel = '';\n        domainObject.url = '';\n      },\n      form: [\n        {\n          key: 'label',\n          name: 'Label',\n          control: 'textfield',\n          property: ['label'],\n          required: true,\n          cssClass: 'l-input'\n        },\n        {\n          key: 'url',\n          name: 'URL',\n          control: 'textfield',\n          required: false,\n          cssClass: 'l-input-lg'\n        }\n      ]\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/conditionWidget/pluginSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport ConditionWidgetPlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  const CONDITION_WIDGET_KEY = 'conditionWidget';\n  let objectDef;\n  let element;\n  let child;\n  let openmct;\n  let mockConditionObjectDefinition;\n  let mockConditionObject;\n  let mockConditionObjectPath;\n\n  beforeEach((done) => {\n    mockConditionObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'conditionWidget',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n\n    mockConditionObjectDefinition = {\n      name: 'Condition Widget',\n      key: 'conditionWidget',\n      creatable: true\n    };\n\n    mockConditionObject = {\n      conditionWidget: {\n        identifier: {\n          namespace: '',\n          key: 'condition-widget-object'\n        },\n        url: 'https://nasa.github.io/openmct/',\n        label: 'Foo Widget',\n        type: 'conditionWidget',\n        composition: []\n      },\n      telemetry: {\n        identifier: {\n          namespace: '',\n          key: 'telemetry-object'\n        },\n        type: 'test-telemetry-object',\n        name: 'Test Telemetry Object',\n        telemetry: {\n          values: [\n            {\n              key: 'name',\n              name: 'Name',\n              format: 'string'\n            },\n            {\n              key: 'utc',\n              name: 'Time',\n              format: 'utc',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              name: 'Some attribute 1',\n              key: 'some-key-1',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              name: 'Some attribute 2',\n              key: 'some-key-2'\n            }\n          ]\n        }\n      }\n    };\n\n    const timeSystem = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 1597160002854,\n        end: 1597181232854\n      }\n    };\n\n    openmct = createOpenMct(timeSystem);\n    openmct.install(new ConditionWidgetPlugin());\n\n    objectDef = openmct.types.get('conditionWidget').definition;\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('defines a conditionWidget object type with the correct key', () => {\n    expect(objectDef.key).toEqual(mockConditionObjectDefinition.key);\n  });\n\n  describe('the conditionWidget object', () => {\n    it('is creatable', () => {\n      expect(objectDef.creatable).toEqual(mockConditionObjectDefinition.creatable);\n    });\n  });\n\n  describe('the view', () => {\n    let conditionWidgetView;\n    let testViewObject;\n\n    beforeEach(() => {\n      testViewObject = {\n        id: 'test-object',\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: 'conditionWidget'\n      };\n\n      const applicableViews = openmct.objectViews.get(testViewObject, mockConditionObjectPath);\n      conditionWidgetView = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'conditionWidget'\n      );\n      let view = conditionWidgetView.view(testViewObject, element);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('provides a view', () => {\n      expect(conditionWidgetView).toBeDefined();\n    });\n  });\n\n  it('should have a view provider for condition widget objects', () => {\n    const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []);\n\n    const conditionWidgetViewProvider = applicableViews.find(\n      (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY\n    );\n\n    expect(applicableViews.length).toEqual(1);\n    expect(conditionWidgetViewProvider).toBeDefined();\n  });\n\n  it('should render a view with a URL and label', async () => {\n    const urlParent = document.createElement('div');\n    const urlChild = document.createElement('div');\n    urlParent.appendChild(urlChild);\n\n    const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []);\n\n    const conditionWidgetViewProvider = applicableViews.find(\n      (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY\n    );\n\n    const conditionWidgetView = conditionWidgetViewProvider.view(\n      mockConditionObject[CONDITION_WIDGET_KEY],\n      [mockConditionObject[CONDITION_WIDGET_KEY]]\n    );\n    conditionWidgetView.show(urlChild);\n\n    await nextTick();\n\n    const domainUrl = mockConditionObject[CONDITION_WIDGET_KEY].url;\n    expect(urlParent.innerHTML).toContain(\n      `<a class=\"c-condition-widget__label-wrapper\" href=\"${domainUrl}\"`\n    );\n\n    const conditionWidgetRender = urlParent.querySelector('.c-condition-widget');\n    expect(conditionWidgetRender).toBeDefined();\n    expect(conditionWidgetRender.innerHTML).toContain('<div class=\"c-condition-widget__label\">');\n\n    const conditionWidgetLabel = conditionWidgetRender.querySelector('.c-condition-widget__label');\n    expect(conditionWidgetLabel).toBeDefined();\n    const domainLabel = mockConditionObject[CONDITION_WIDGET_KEY].label;\n    expect(conditionWidgetLabel.textContent).toContain(domainLabel);\n  });\n});\n"
  },
  {
    "path": "src/plugins/correlationTelemetryPlugin/plugin.js",
    "content": "const CORRELATOR_TYPE = 'telemetry.correlator';\n\nexport default function CorrelationTelemetryPlugin() {\n  // eslint-disable-next-line no-shadow\n  return function install(openmct) {\n    function getTelemetryObject(idString) {\n      return openmct.objects.get(idString);\n    }\n\n    function getTelemetry(object, options) {\n      return openmct.telemetry.request(object, options);\n    }\n\n    openmct.types.addType(CORRELATOR_TYPE, {\n      name: 'Correlation Telemetry',\n      description: `Combines telemetry from multiple sources to produce telemetry correlated by timestamp with a given time tolerance.`,\n      cssClass: 'icon-object',\n      creatable: true,\n      initialize: function (obj) {\n        obj.telemetry = {};\n      },\n      form: [\n        {\n          key: 'xSource',\n          name: 'X Axis Source',\n          control: 'locator',\n          required: true,\n          cssClass: 'grows'\n        },\n        {\n          key: 'ySource',\n          name: 'Y Axis Source',\n          control: 'locator',\n          required: true,\n          cssClass: 'grows'\n        }\n      ]\n    });\n\n    openmct.telemetry.addProvider({\n      supportsMetadata: function (domainObject) {\n        return domainObject.type === CORRELATOR_TYPE;\n      },\n      getMetadata: function (domainObject) {\n        let metadata = {};\n        metadata.values = openmct.time.getAllTimeSystems().map(function (timeSystem, i) {\n          return {\n            name: timeSystem.name,\n            key: timeSystem.key,\n            source: timeSystem.source,\n            format: timeSystem.timeFormat,\n            hints: { domain: i }\n          };\n        });\n        metadata.values.push({\n          name: 'X',\n          key: 'x',\n          source: 'x',\n          hints: { xSource: 1, range: 1 }\n        });\n        metadata.values.push({\n          name: 'Y',\n          key: 'y',\n          source: 'y',\n          hints: { ySource: 1, range: 2 }\n        });\n        return metadata;\n      },\n      supportsRequest: function (domainObject) {\n        return domainObject.type === CORRELATOR_TYPE;\n      },\n      request: function (domainObject, options) {\n        let telemResults = {};\n        let telemObject;\n\n        const xSourceIdentifier = openmct.objects.makeKeyString(domainObject.xSource[0].identifier);\n        let xPromise = getTelemetryObject(xSourceIdentifier)\n          .then((object) => {\n            telemObject = object;\n            return getTelemetry(object, options);\n          })\n          .then((data) => {\n            let source = 'x';\n            telemResults[source] = {\n              object: telemObject\n            };\n            let metadata = openmct.telemetry.getMetadata(telemObject);\n            let valueMeta = metadata.valuesForHints(['range'])[0];\n            telemResults[source].correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n            telemResults[source].correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n            telemResults[source].timestampFormat = openmct.telemetry.getValueFormatter(\n              metadata.value(options.domain)\n            );\n            telemResults[source].data = data;\n          });\n\n        const ySourceIdentifier = openmct.objects.makeKeyString(domainObject.ySource[0].identifier);\n        let yPromise = getTelemetryObject(ySourceIdentifier)\n          .then((object) => {\n            telemObject = object;\n            return getTelemetry(object, options);\n          })\n          .then((data) => {\n            let source = 'y';\n            telemResults[source] = {\n              object: telemObject\n            };\n            let metadata = openmct.telemetry.getMetadata(telemObject);\n            let valueMeta = metadata.valuesForHints(['range'])[0];\n            telemResults[source].correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n            telemResults[source].correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n            telemResults[source].timestampFormat = openmct.telemetry.getValueFormatter(\n              metadata.value(options.domain)\n            );\n            telemResults[source].data = data;\n          });\n\n        return Promise.all([xPromise, yPromise]).then(function () {\n          let results = [];\n          let xByTime = telemResults.x.data.reduce(function (m, datum) {\n            m[telemResults.x.timestampFormat.parse(datum)] =\n              telemResults.x.correlatorFormat.parse(datum);\n            return m;\n          }, {});\n          telemResults.y.data.forEach(function (datum) {\n            let timestamp = telemResults.y.timestampFormat.parse(datum);\n            if (xByTime[timestamp] !== undefined) {\n              let resultDatum = {\n                x: xByTime[timestamp],\n                y: telemResults.y.correlatorFormat.parse(datum)\n              };\n              resultDatum[options.domain] = timestamp;\n              results.push(resultDatum);\n            }\n          });\n          return results;\n        });\n      },\n      supportsSubscribe: function (domainObject) {\n        return domainObject.type === CORRELATOR_TYPE;\n      },\n      subscribe: function (domainObject, callback) {\n        let telem = {};\n        let done = false;\n        let unsubscribes = [];\n\n        function sendUpdate() {\n          if (done) {\n            return;\n          }\n          if (!telem.y.latest || !telem.x.latest) {\n            return;\n          }\n          if (telem.y.latestTimestamp !== telem.x.latestTimestamp) {\n            return;\n          }\n          let datum = {\n            x: telem.x.correlatorFormat.parse(telem.x.latest),\n            y: telem.y.correlatorFormat.parse(telem.y.latest)\n          };\n          datum[openmct.time.timeSystem().key] = Math.max(\n            telem.x.latestTimestamp,\n            telem.y.latestTimestamp\n          );\n          delete telem.x.latest;\n          delete telem.y.latest;\n          delete telem.x.latestTimestamp;\n          delete telem.y.latestTimestamp;\n          callback(datum);\n        }\n\n        const xSourceIdentifier = openmct.objects.makeKeyString(domainObject.xSource[0].identifier);\n        getTelemetryObject(xSourceIdentifier).then(function (xObject) {\n          if (done) {\n            return;\n          }\n          telem.x = {\n            object: xObject\n          };\n          let metadata = openmct.telemetry.getMetadata(xObject);\n          let valueMeta = metadata.valuesForHints(['range'])[0];\n          telem.x.correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n          telem.x.timestampFormat = openmct.telemetry.getValueFormatter(\n            metadata.value(openmct.time.timeSystem().key)\n          );\n          unsubscribes.push(\n            openmct.telemetry.subscribe(xObject, function (datum) {\n              telem.x.latest = datum;\n              telem.x.latestTimestamp = telem.x.timestampFormat.parse(datum);\n              requestAnimationFrame(sendUpdate);\n            })\n          );\n        });\n\n        const ySourceIdentifier = openmct.objects.makeKeyString(domainObject.ySource[0].identifier);\n        getTelemetryObject(ySourceIdentifier).then(function (yObject) {\n          if (done) {\n            return;\n          }\n          telem.y = {\n            object: yObject\n          };\n          let metadata = openmct.telemetry.getMetadata(yObject);\n          let valueMeta = metadata.valuesForHints(['range'])[0];\n          telem.y.correlatorFormat = openmct.telemetry.getValueFormatter(valueMeta);\n          telem.y.timestampFormat = openmct.telemetry.getValueFormatter(\n            metadata.value(openmct.time.timeSystem().key)\n          );\n          unsubscribes.push(\n            openmct.telemetry.subscribe(yObject, function (datum) {\n              telem.y.latest = datum;\n              telem.y.latestTimestamp = telem.y.timestampFormat.parse(datum);\n              requestAnimationFrame(sendUpdate);\n            })\n          );\n        });\n\n        return function unsubscribe() {\n          done = true;\n          unsubscribes.forEach(function (u) {\n            u();\n          });\n          unsubscribes = undefined;\n        };\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/defaultRootName/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport RootObjectProvider from '../../api/objects/RootObjectProvider.js';\n\nexport default function (name) {\n  return function (openmct) {\n    let rootObjectProvider = new RootObjectProvider();\n    rootObjectProvider.updateName(name);\n  };\n}\n"
  },
  {
    "path": "src/plugins/defaultRootName/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nconst OLD_ROOT_NAME = 'Open MCT';\nconst NEW_ROOT_NAME = 'not_a_root';\n\nlet openmct;\n\ndescribe('the DefaultRootNamePlugin', () => {\n  describe('without DefaultRootNamePlugin', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('does not changes root name', (done) => {\n      openmct.objects.getRoot().then((object) => {\n        expect(object.name).toEqual(OLD_ROOT_NAME);\n\n        done();\n      });\n    });\n  });\n\n  describe('with DefaultRootNamePlugin', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n\n      openmct.install(openmct.plugins.DefaultRootName(NEW_ROOT_NAME));\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('changes root name', (done) => {\n      openmct.objects.getRoot().then((object) => {\n        expect(object.name).toEqual(NEW_ROOT_NAME);\n\n        done();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/displayLayout/AlphanumericFormatViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport AlphanumericFormat from './components/AlphanumericFormat.vue';\n\nclass AlphanumericFormatView {\n  constructor(openmct, domainObject, objectPath) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this._destroy = null;\n    this.component = null;\n  }\n\n  show(element) {\n    const { vNode, destroy } = mount(\n      {\n        el: element,\n        components: {\n          AlphanumericFormat\n        },\n        provide: {\n          openmct: this.openmct,\n          objectPath: this.objectPath,\n          currentView: this\n        },\n        template: '<AlphanumericFormat ref=\"alphanumericFormat\" />'\n      },\n      {\n        app: this.openmct.app,\n        element\n      }\n    );\n    this.component = vNode.componentInstance;\n    this._destroy = destroy;\n  }\n\n  getViewContext() {\n    if (this.component) {\n      return {};\n    }\n\n    return this.component.$refs.alphanumericFormat.getViewContext();\n  }\n\n  priority() {\n    return this.openmct.editor.isEditing()\n      ? this.openmct.priority.DEFAULT\n      : this.openmct.priority.LOW;\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n    }\n  }\n}\n\nexport default function AlphanumericFormatViewProvider(openmct, options) {\n  function isTelemetryObject(selectionPath) {\n    let selectedObject = selectionPath[0].context.item;\n    let parentObject = selectionPath[1].context.item;\n    let selectedLayoutItem = selectionPath[0].context.layoutItem;\n\n    return (\n      parentObject &&\n      parentObject.type === 'layout' &&\n      selectedObject &&\n      selectedLayoutItem &&\n      selectedLayoutItem.type === 'telemetry-view' &&\n      openmct.telemetry.isTelemetryObject(selectedObject) &&\n      !options.showAsView.includes(selectedObject.type)\n    );\n  }\n\n  return {\n    key: 'alphanumeric-format',\n    name: 'Format',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 1) {\n        return false;\n      }\n\n      return selection.every(isTelemetryObject);\n    },\n    view: function (domainObject, objectPath) {\n      return new AlphanumericFormatView(openmct, domainObject, objectPath);\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/CustomStringFormatter.js",
    "content": "import { sprintf } from 'printj';\n\nexport default class CustomStringFormatter {\n  constructor(openmct, valueMetadata, itemFormat) {\n    this.openmct = openmct;\n\n    this.itemFormat = itemFormat;\n    this.valueMetadata = valueMetadata;\n  }\n\n  format(datum) {\n    if (!this.itemFormat) {\n      return;\n    }\n\n    if (!this.itemFormat.startsWith('&')) {\n      return sprintf(this.itemFormat, datum[this.valueMetadata.key]);\n    }\n\n    try {\n      const key = this.itemFormat.slice(1);\n      const customFormatter = this.openmct.telemetry.getFormatter(key);\n      if (!customFormatter) {\n        throw new Error('Custom Formatter not found');\n      }\n\n      return customFormatter.format(datum[this.valueMetadata.key]);\n    } catch (e) {\n      console.error(e);\n\n      return datum[this.valueMetadata.key];\n    }\n  }\n\n  setFormat(itemFormat) {\n    this.itemFormat = itemFormat;\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/CustomStringFormatterSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport CustomStringFormatter from './CustomStringFormatter.js';\n\nconst CUSTOM_FORMATS = [\n  {\n    key: 'sclk',\n    format: (value) => 2 * value\n  },\n  {\n    key: 'lts',\n    format: (value) => 3 * value\n  }\n];\n\nconst valueMetadata = {\n  key: 'sin',\n  name: 'Sine',\n  unit: 'Hz',\n  formatString: '%0.2f',\n  hints: {\n    range: 1,\n    priority: 3\n  },\n  source: 'sin'\n};\n\nconst datum = {\n  name: '1 Sine Wave Generator',\n  utc: 1603930354000,\n  yesterday: 1603843954000,\n  sin: 0.587785209686822,\n  cos: -0.8090170253297632\n};\n\ndescribe('CustomStringFormatter', function () {\n  let element;\n  let child;\n  let openmct;\n  let customStringFormatter;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n    CUSTOM_FORMATS.forEach((formatter) => {\n      openmct.telemetry.addFormat(formatter);\n    });\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('adds custom format sclk', () => {\n    const format = openmct.telemetry.getFormatter('sclk');\n    expect(format.key).toEqual('sclk');\n  });\n\n  it('adds custom format lts', () => {\n    const format = openmct.telemetry.getFormatter('lts');\n    expect(format.key).toEqual('lts');\n  });\n\n  it('returns correct value for custom format sclk', () => {\n    customStringFormatter.setFormat('&sclk');\n    const value = customStringFormatter.format(datum, valueMetadata);\n    expect(datum.sin * 2).toEqual(value);\n  });\n\n  it('returns correct value for custom format lts', () => {\n    customStringFormatter.setFormat('&lts');\n    const value = customStringFormatter.format(datum, valueMetadata);\n    expect(datum.sin * 3).toEqual(value);\n  });\n});\n"
  },
  {
    "path": "src/plugins/displayLayout/DisplayLayoutToolbar.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\n\nconst CONTEXT_ACTION = 'contextAction';\nconst CONTEXT_ACTIONS = Object.freeze({\n  ADD_ELEMENT: 'addElement',\n  REMOVE_ITEM: 'removeItem',\n  DUPLICATE_ITEM: 'duplicateItem',\n  ORDER_ITEM: 'orderItem',\n  SWITCH_VIEW_TYPE: 'switchViewType',\n  MERGE_MULTIPLE_TELEMETRY_VIEWS: 'mergeMultipleTelemetryViews',\n  MERGE_MULTIPLE_OVERLAY_PLOTS: 'mergeMultipleOverlayPlots',\n  TOGGLE_GRID: 'toggleGrid'\n});\n\nconst DIALOG_FORM = {\n  text: {\n    title: 'Text Element Properties',\n    sections: [\n      {\n        rows: [\n          {\n            key: 'text',\n            control: 'textfield',\n            name: 'Text',\n            required: true,\n            cssClass: 'l-input-lg'\n          }\n        ]\n      }\n    ]\n  },\n  image: {\n    title: 'Image Properties',\n    sections: [\n      {\n        rows: [\n          {\n            key: 'url',\n            control: 'textfield',\n            name: 'Image URL',\n            cssClass: 'l-input-lg',\n            required: true\n          }\n        ]\n      }\n    ]\n  }\n};\nconst VIEW_TYPES = {\n  'telemetry-view': {\n    value: 'telemetry-view',\n    name: 'Alphanumeric',\n    class: 'icon-alphanumeric'\n  },\n  'telemetry.plot.overlay': {\n    value: 'telemetry.plot.overlay',\n    name: 'Overlay Plot',\n    class: 'icon-plot-overlay'\n  },\n  'telemetry.plot.stacked': {\n    value: 'telemetry.plot.stacked',\n    name: 'Stacked Plot',\n    class: 'icon-plot-stacked'\n  },\n  table: {\n    value: 'table',\n    name: 'Table',\n    class: 'icon-tabular-scrolling'\n  }\n};\nconst APPLICABLE_VIEWS = {\n  'telemetry-view': [\n    VIEW_TYPES['telemetry.plot.overlay'],\n    VIEW_TYPES['telemetry.plot.stacked'],\n    VIEW_TYPES.table\n  ],\n  'telemetry.plot.overlay': [\n    VIEW_TYPES['telemetry.plot.stacked'],\n    VIEW_TYPES.table,\n    VIEW_TYPES['telemetry-view']\n  ],\n  'telemetry.plot.stacked': [\n    VIEW_TYPES['telemetry.plot.overlay'],\n    VIEW_TYPES.table,\n    VIEW_TYPES['telemetry-view']\n  ],\n  table: [\n    VIEW_TYPES['telemetry.plot.overlay'],\n    VIEW_TYPES['telemetry.plot.stacked'],\n    VIEW_TYPES['telemetry-view']\n  ],\n  'telemetry-view-multi': [\n    VIEW_TYPES['telemetry.plot.overlay'],\n    VIEW_TYPES['telemetry.plot.stacked'],\n    VIEW_TYPES.table\n  ],\n  'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']]\n};\n\nexport default class DisplayLayoutToolbar {\n  #openmct;\n\n  constructor(openmct) {\n    this.#openmct = openmct;\n    this.name = 'Display Layout Toolbar';\n    this.key = 'layout';\n    this.description = 'A toolbar for objects inside a display layout.';\n  }\n\n  forSelection(selection) {\n    if (!selection || selection.length === 0) {\n      return false;\n    }\n\n    let selectionPath = selection[0];\n    let selectedObject = selectionPath[0];\n    let selectedParent = selectionPath[1];\n\n    // Apply the layout toolbar if the selected object is inside a layout, or the main layout is selected.\n    return (\n      (selectedParent &&\n        selectedParent.context.item &&\n        selectedParent.context.item.type === 'layout') ||\n      (selectedObject.context.item && selectedObject.context.item.type === 'layout')\n    );\n  }\n\n  #getPath(selectionPath) {\n    return `configuration.items[${selectionPath[0].context.index}]`;\n  }\n\n  #getAllOfType(selection, specificType) {\n    return selection.filter((selectionPath) => {\n      let type = selectionPath[0].context.layoutItem.type;\n\n      return type === specificType;\n    });\n  }\n\n  #getAllTypes(selection) {\n    return selection.filter((selectionPath) => {\n      let type = selectionPath[0].context.layoutItem.type;\n\n      return (\n        type === 'text-view' ||\n        type === 'telemetry-view' ||\n        type === 'box-view' ||\n        type === 'ellipse-view' ||\n        type === 'image-view' ||\n        type === 'line-view' ||\n        type === 'subobject-view'\n      );\n    });\n  }\n\n  #getAddButton(selection, selectionPath) {\n    if (selection.length === 1) {\n      selectionPath = selectionPath || selection[0];\n      const domainObject = selectionPath[0].context.item;\n      const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n\n      return {\n        control: 'menu',\n        domainObject,\n        method: (option) => {\n          let name = option.name.toLowerCase();\n          let form = DIALOG_FORM[name];\n          if (form) {\n            this.#showForm(form, name, selection);\n          } else {\n            this.#openmct.objectViews.emit(\n              `${CONTEXT_ACTION}:${keyString}`,\n              CONTEXT_ACTIONS.ADD_ELEMENT,\n              name,\n              selection\n            );\n          }\n        },\n        key: 'add',\n        icon: 'icon-plus',\n        label: 'Add Drawing Object',\n        options: [\n          {\n            name: 'Box',\n            class: 'icon-box-round-corners'\n          },\n          {\n            name: 'Ellipse',\n            class: 'icon-circle'\n          },\n          {\n            name: 'Line',\n            class: 'icon-line-horz'\n          },\n          {\n            name: 'Text',\n            class: 'icon-font'\n          },\n          {\n            name: 'Image',\n            class: 'icon-image'\n          }\n        ]\n      };\n    }\n  }\n\n  #getToggleFrameButton(selectedParent, selection) {\n    return {\n      control: 'toggle-button',\n      domainObject: selectedParent,\n      applicableSelectedItems: selection.filter(\n        (selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view'\n      ),\n      property: (selectionPath) => {\n        return this.#getPath(selectionPath) + '.hasFrame';\n      },\n      options: [\n        {\n          value: false,\n          icon: 'icon-frame-hide',\n          title: 'Frame visible',\n          label: 'Hide frame'\n        },\n        {\n          value: true,\n          icon: 'icon-frame-show',\n          title: 'Frame hidden',\n          label: 'Show frame'\n        }\n      ]\n    };\n  }\n\n  #getRemoveButton(selectedParent, selectionPath, selection) {\n    const domainObject = selectedParent;\n    const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n    return {\n      control: 'button',\n      domainObject,\n      icon: 'icon-trash',\n      title: 'Delete the selected object',\n      method: () => {\n        let prompt = this.#openmct.overlays.dialog({\n          iconClass: 'alert',\n          message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,\n          buttons: [\n            {\n              label: 'Ok',\n              emphasis: 'true',\n              callback: () => {\n                this.#openmct.objectViews.emit(\n                  `${CONTEXT_ACTION}:${keyString}`,\n                  CONTEXT_ACTIONS.REMOVE_ITEM,\n                  this.#getAllTypes(selection),\n                  selection\n                );\n                prompt.dismiss();\n              }\n            },\n            {\n              label: 'Cancel',\n              callback: function () {\n                prompt.dismiss();\n              }\n            }\n          ]\n        });\n      }\n    };\n  }\n\n  #getStackOrder(selectedParent, selectionPath, selectedObjects) {\n    const domainObject = selectedParent;\n    const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n    return {\n      control: 'menu',\n      domainObject,\n      icon: 'icon-layers',\n      title: 'Move the selected object above or below other objects',\n      options: [\n        {\n          name: 'Move to Top',\n          value: 'top',\n          class: 'icon-arrow-double-up'\n        },\n        {\n          name: 'Move Up',\n          value: 'up',\n          class: 'icon-arrow-up'\n        },\n        {\n          name: 'Move Down',\n          value: 'down',\n          class: 'icon-arrow-down'\n        },\n        {\n          name: 'Move to Bottom',\n          value: 'bottom',\n          class: 'icon-arrow-double-down'\n        }\n      ],\n      method: (option) => {\n        this.#openmct.objectViews.emit(\n          `${CONTEXT_ACTION}:${keyString}`,\n          CONTEXT_ACTIONS.ORDER_ITEM,\n          option.value,\n          this.#getAllTypes(selectedObjects)\n        );\n      }\n    };\n  }\n\n  #getXInput(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: this.#getAllTypes(selection),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.x';\n        },\n        label: 'X:',\n        title: 'X position'\n      };\n    }\n  }\n\n  #getYInput(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: this.#getAllTypes(selection),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.y';\n        },\n        label: 'Y:',\n        title: 'Y position'\n      };\n    }\n  }\n\n  #getWidthInput(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: this.#getAllTypes(selection),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.width';\n        },\n        label: 'W:',\n        title: 'Resize object width'\n      };\n    }\n  }\n\n  #getHeightInput(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: this.#getAllTypes(selection),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.height';\n        },\n        label: 'H:',\n        title: 'Resize object height'\n      };\n    }\n  }\n\n  #getX2Input(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: selection.filter((selectionPath) => {\n          return selectionPath[0].context.layoutItem.type === 'line-view';\n        }),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.x2';\n        },\n        label: 'X2:',\n        title: 'X2 position'\n      };\n    }\n  }\n\n  #getY2Input(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'input',\n        type: 'number',\n        domainObject: selectedParent,\n        applicableSelectedItems: selection.filter((selectionPath) => {\n          return selectionPath[0].context.layoutItem.type === 'line-view';\n        }),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.y2';\n        },\n        label: 'Y2:',\n        title: 'Y2 position'\n      };\n    }\n  }\n\n  #getTextButton(selectedParent, selection) {\n    return {\n      control: 'button',\n      domainObject: selectedParent,\n      applicableSelectedItems: selection.filter((selectionPath) => {\n        return selectionPath[0].context.layoutItem.type === 'text-view';\n      }),\n      property: (selectionPath) => {\n        return this.#getPath(selectionPath);\n      },\n      icon: 'icon-pencil',\n      title: 'Edit text properties',\n      label: 'Edit text',\n      dialog: DIALOG_FORM.text\n    };\n  }\n\n  #getTelemetryValueMenu(selectionPath, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'select-menu',\n        domainObject: selectionPath[1].context.item,\n        applicableSelectedItems: selection.filter((path) => {\n          return path[0].context.layoutItem.type === 'telemetry-view';\n        }),\n        property: (path) => {\n          return this.#getPath(path) + '.value';\n        },\n        title: 'Set value',\n        options: this.#openmct.telemetry\n          .getMetadata(selectionPath[0].context.item)\n          .values()\n          .map((value) => {\n            return {\n              name: value.name,\n              value: value.key\n            };\n          })\n      };\n    }\n  }\n\n  #getDisplayModeMenu(selectedParent, selection) {\n    if (selection.length === 1) {\n      return {\n        control: 'select-menu',\n        domainObject: selectedParent,\n        applicableSelectedItems: selection.filter((selectionPath) => {\n          return selectionPath[0].context.layoutItem.type === 'telemetry-view';\n        }),\n        property: (selectionPath) => {\n          return this.#getPath(selectionPath) + '.displayMode';\n        },\n        title: 'Set display mode',\n        options: [\n          {\n            name: 'Label + Value',\n            value: 'all'\n          },\n          {\n            name: 'Label only',\n            value: 'label'\n          },\n          {\n            name: 'Value only',\n            value: 'value'\n          }\n        ]\n      };\n    }\n  }\n\n  #getDuplicateButton(selectedParent, selectionPath, selection) {\n    const domainObject = selectedParent;\n    const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n    return {\n      control: 'button',\n      domainObject,\n      icon: 'icon-duplicate',\n      title: 'Duplicate the selected object',\n      method: () => {\n        this.#openmct.objectViews.emit(\n          `${CONTEXT_ACTION}:${keyString}`,\n          CONTEXT_ACTIONS.DUPLICATE_ITEM,\n          selection\n        );\n      }\n    };\n  }\n\n  #getPropertyFromPath(object, path) {\n    let splitPath = path.split('.');\n    let property = Object.assign({}, object);\n\n    while (splitPath.length && property) {\n      property = property[splitPath.shift()];\n    }\n\n    return property;\n  }\n\n  #areAllViews(type, path, selection) {\n    let allTelemetry = true;\n\n    selection.forEach((selectedItem) => {\n      let selectedItemContext = selectedItem[0].context;\n\n      if (this.#getPropertyFromPath(selectedItemContext, path) !== type) {\n        allTelemetry = false;\n      }\n    });\n\n    return allTelemetry;\n  }\n\n  #getToggleUnitsButton(selectedParent, selection) {\n    let applicableItems = this.#getAllOfType(selection, 'telemetry-view');\n    applicableItems = this.#unitsOnly(applicableItems);\n    if (!applicableItems.length) {\n      return;\n    }\n\n    return {\n      control: 'toggle-button',\n      domainObject: selectedParent,\n      applicableSelectedItems: applicableItems,\n      property: (selectionPath) => {\n        return this.#getPath(selectionPath) + '.showUnits';\n      },\n      options: [\n        {\n          value: true,\n          icon: 'icon-eye-open',\n          title: 'Show units',\n          label: 'Show units'\n        },\n        {\n          value: false,\n          icon: 'icon-eye-disabled',\n          title: 'Hide units',\n          label: 'Hide units'\n        }\n      ]\n    };\n  }\n\n  #unitsOnly(items) {\n    let results = items.filter((item) => {\n      let currentItem = item[0];\n      let metadata = this.#openmct.telemetry.getMetadata(currentItem.context.item);\n      if (!metadata) {\n        return false;\n      }\n\n      let hasUnits = metadata.valueMetadatas.filter((metadatum) => metadatum.unit).length;\n\n      return hasUnits > 0;\n    });\n\n    return results;\n  }\n\n  #getViewSwitcherMenu(selectedParent, selectionPath, selection) {\n    if (selection.length === 1) {\n      let selectedItemContext = selectionPath[0].context;\n      let selectedItemType = selectedItemContext.item.type;\n\n      if (selectedItemContext.layoutItem.type === 'telemetry-view') {\n        selectedItemType = 'telemetry-view';\n      }\n\n      let viewOptions = APPLICABLE_VIEWS[selectedItemType];\n\n      if (viewOptions) {\n        const domainObject = selectedParent;\n        const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n        return {\n          control: 'menu',\n          domainObject,\n          icon: 'icon-object',\n          title: 'Switch the way this telemetry is displayed',\n          label: 'View type',\n          options: viewOptions,\n          method: (option) => {\n            this.#openmct.objectViews.emit(\n              `${CONTEXT_ACTION}:${keyString}`,\n              CONTEXT_ACTIONS.SWITCH_VIEW_TYPE,\n              selectedItemContext,\n              option.value,\n              selection\n            );\n          }\n        };\n      }\n    } else if (selection.length > 1) {\n      const domainObject = selectedParent;\n      const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n      if (this.#areAllViews('telemetry-view', 'layoutItem.type', selection)) {\n        return {\n          control: 'menu',\n          domainObject,\n          icon: 'icon-object',\n          title: 'Merge into a telemetry table or plot',\n          label: 'View type',\n          options: APPLICABLE_VIEWS['telemetry-view-multi'],\n          method: (option) => {\n            this.#openmct.objectViews.emit(\n              `${CONTEXT_ACTION}:${keyString}`,\n              CONTEXT_ACTIONS.MERGE_MULTIPLE_TELEMETRY_VIEWS,\n              selection,\n              option.value\n            );\n          }\n        };\n      } else if (this.#areAllViews('telemetry.plot.overlay', 'item.type', selection)) {\n        return {\n          control: 'menu',\n          domainObject,\n          icon: 'icon-object',\n          title: 'Merge into a stacked plot',\n          options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],\n          method: (option) => {\n            this.#openmct.objectViews.emit(\n              `${CONTEXT_ACTION}:${keyString}`,\n              CONTEXT_ACTIONS.MERGE_MULTIPLE_OVERLAY_PLOTS,\n              selection,\n              option.value\n            );\n          }\n        };\n      }\n    }\n  }\n\n  #getToggleGridButton(selection, selectionPath) {\n    const ICON_GRID_SHOW = 'icon-grid-on';\n    const ICON_GRID_HIDE = 'icon-grid-off';\n\n    let displayLayoutContext;\n\n    if (selection.length === 1 && selectionPath === undefined) {\n      displayLayoutContext = selection[0][0].context;\n    } else {\n      displayLayoutContext = selectionPath[1].context;\n    }\n\n    const domainObject = displayLayoutContext.item;\n    const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n\n    return {\n      control: 'button',\n      domainObject,\n      icon: ICON_GRID_SHOW,\n      method: () => {\n        this.#openmct.objectViews.emit(\n          `${CONTEXT_ACTION}:${keyString}`,\n          CONTEXT_ACTIONS.TOGGLE_GRID\n        );\n\n        this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW;\n      },\n      secondary: true\n    };\n  }\n\n  #getSeparator() {\n    return {\n      control: 'separator'\n    };\n  }\n\n  #isMainLayoutSelected(selectionPath) {\n    let selectedObject = selectionPath[0].context.item;\n\n    return (\n      selectedObject && selectedObject.type === 'layout' && !selectionPath[0].context.layoutItem\n    );\n  }\n\n  #showForm(formStructure, name, selection) {\n    const domainObject = selection[0][0].context.item;\n    const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);\n    this.#openmct.forms.showForm(formStructure).then((changes) => {\n      this.#openmct.objectViews.emit(\n        `${CONTEXT_ACTION}:${keyString}`,\n        CONTEXT_ACTIONS.ADD_ELEMENT,\n        name,\n        changes,\n        selection\n      );\n    });\n  }\n\n  toolbar(selectedObjects) {\n    if (this.#isMainLayoutSelected(selectedObjects[0])) {\n      return [this.#getToggleGridButton(selectedObjects), this.#getAddButton(selectedObjects)];\n    }\n\n    let toolbar = {\n      'add-menu': [],\n      text: [],\n      url: [],\n      viewSwitcher: [],\n      'toggle-frame': [],\n      'display-mode': [],\n      'telemetry-value': [],\n      style: [],\n      'unit-toggle': [],\n      position: [],\n      duplicate: [],\n      remove: [],\n      'toggle-grid': []\n    };\n\n    selectedObjects.forEach((selectionPath) => {\n      let selectedParent = selectionPath[1].context.item;\n      let layoutItem = selectionPath[0].context.layoutItem;\n\n      if (!layoutItem || selectedParent.locked) {\n        return;\n      }\n\n      if (layoutItem.type === 'subobject-view') {\n        if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') {\n          toolbar['add-menu'] = [this.#getAddButton(selectedObjects, selectionPath)];\n        }\n\n        if (toolbar['toggle-frame'].length === 0) {\n          toolbar['toggle-frame'] = [this.#getToggleFrameButton(selectedParent, selectedObjects)];\n        }\n\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath, selectedObjects),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getHeightInput(selectedParent, selectedObjects),\n            this.#getWidthInput(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n\n        if (toolbar.viewSwitcher.length === 0) {\n          toolbar.viewSwitcher = [\n            this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)\n          ];\n        }\n      } else if (layoutItem.type === 'telemetry-view') {\n        if (toolbar['display-mode'].length === 0) {\n          toolbar['display-mode'] = [this.#getDisplayModeMenu(selectedParent, selectedObjects)];\n        }\n\n        if (toolbar['telemetry-value'].length === 0) {\n          toolbar['telemetry-value'] = [\n            this.#getTelemetryValueMenu(selectionPath, selectedObjects)\n          ];\n        }\n\n        if (toolbar['unit-toggle'].length === 0) {\n          let toggleUnitsButton = this.#getToggleUnitsButton(selectedParent, selectedObjects);\n          if (toggleUnitsButton) {\n            toolbar['unit-toggle'] = [toggleUnitsButton];\n          }\n        }\n\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath, selectedObjects),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getHeightInput(selectedParent, selectedObjects),\n            this.#getWidthInput(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n\n        if (toolbar.viewSwitcher.length === 0) {\n          toolbar.viewSwitcher = [\n            this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)\n          ];\n        }\n      } else if (layoutItem.type === 'text-view') {\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath, selectedObjects),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getHeightInput(selectedParent, selectedObjects),\n            this.#getWidthInput(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.text.length === 0) {\n          toolbar.text = [this.#getTextButton(selectedParent, selectedObjects)];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n      } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') {\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath, selectedObjects),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getHeightInput(selectedParent, selectedObjects),\n            this.#getWidthInput(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n      } else if (layoutItem.type === 'image-view') {\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath, selectedObjects),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getHeightInput(selectedParent, selectedObjects),\n            this.#getWidthInput(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n      } else if (layoutItem.type === 'line-view') {\n        if (toolbar.position.length === 0) {\n          toolbar.position = [\n            this.#getStackOrder(selectedParent, selectionPath),\n            this.#getSeparator(),\n            this.#getXInput(selectedParent, selectedObjects),\n            this.#getYInput(selectedParent, selectedObjects),\n            this.#getX2Input(selectedParent, selectedObjects),\n            this.#getY2Input(selectedParent, selectedObjects)\n          ];\n        }\n\n        if (toolbar.remove.length === 0) {\n          toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];\n        }\n      }\n\n      if (toolbar.duplicate.length === 0) {\n        toolbar.duplicate = [\n          this.#getDuplicateButton(selectedParent, selectionPath, selectedObjects)\n        ];\n      }\n\n      if (toolbar['toggle-grid'].length === 0) {\n        toolbar['toggle-grid'] = [this.#getToggleGridButton(selectedObjects, selectionPath)];\n      }\n    });\n\n    let toolbarArray = Object.values(toolbar);\n\n    return _.flatten(\n      toolbarArray.reduce((accumulator, group, index) => {\n        group = group.filter((control) => control !== undefined);\n\n        if (group.length > 0) {\n          accumulator.push(group);\n\n          if (index < toolbarArray.length - 1) {\n            accumulator.push(this.#getSeparator());\n          }\n        }\n\n        return accumulator;\n      }, [])\n    );\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/DisplayLayoutType.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function DisplayLayoutType() {\n  return {\n    name: 'Display Layout',\n    creatable: true,\n    description:\n      'Assemble other objects and components together into a reusable screen layout. Simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.',\n    cssClass: 'icon-layout',\n    initialize(domainObject) {\n      domainObject.composition = [];\n      domainObject.configuration = {\n        items: [],\n        layoutGrid: [10, 10],\n        objectStyles: {}\n      };\n    },\n    form: [\n      {\n        name: 'Horizontal grid (px)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        property: ['configuration', 'layoutGrid', 0],\n        required: true\n      },\n      {\n        name: 'Vertical grid (px)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        property: ['configuration', 'layoutGrid', 1],\n        required: true\n      },\n      {\n        name: 'Horizontal size (px)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        property: ['configuration', 'layoutDimensions', 0],\n        required: false\n      },\n      {\n        name: 'Vertical size (px)',\n        control: 'numberfield',\n        cssClass: 'l-input-sm l-numeric',\n        property: ['configuration', 'layoutDimensions', 1],\n        required: false\n      }\n    ]\n  };\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/DrawingObjectTypes.js",
    "content": "const displayLayoutDrawingObjectTypes = {\n  'box-view': {\n    name: 'Box',\n    creatable: false,\n    description: 'A rectangle shape.',\n    cssClass: 'icon-box-round-corners'\n  },\n  'ellipse-view': {\n    name: 'Ellipse',\n    creatable: false,\n    description: 'A ellipse shape.',\n    cssClass: 'icon-circle'\n  },\n  'line-view': {\n    name: 'Line',\n    creatable: false,\n    description: 'A line.',\n    cssClass: 'icon-line-horz'\n  },\n  'text-view': {\n    name: 'Text',\n    creatable: false,\n    description: 'An editable text box.',\n    cssClass: 'icon-font'\n  },\n  'image-view': {\n    name: 'Image',\n    creatable: false,\n    description: 'An image.',\n    cssClass: 'icon-image'\n  }\n};\n\nexport default displayLayoutDrawingObjectTypes;\n"
  },
  {
    "path": "src/plugins/displayLayout/LayoutDrag.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Handles drag interactions on frames in layouts. This will\n * provides new positions/dimensions for frames based on\n * relative pixel positions provided; these will take into account\n * the grid size (in a snap-to sense) and will enforce some minimums\n * on both position and dimensions.\n *\n * The provided position and dimensions factors will determine\n * whether this is a move or a resize, and what type of resize it\n * will be. For instance, a position factor of [1, 1]\n * will move a frame along with the mouse as the drag\n * proceeds, while a dimension factor of [0, 0] will leave\n * dimensions unchanged. Combining these in different\n * ways results in different handles; a position factor of\n * [1, 0] and a dimensions factor of [-1, 0] will implement\n * a left-edge resize, as the horizontal position will move\n * with the mouse while the horizontal dimensions shrink in\n * kind (and vertical properties remain unmodified.)\n *\n * @param {Object} rawPosition the initial position/dimensions\n *                 of the frame being interacted with\n * @param {number[]} posFactor the position factor\n * @param {number[]} dimFactor the dimensions factor\n * @param {number[]} the size of each grid element, in pixels\n * @constructor\n */\nexport default function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) {\n  this.rawPosition = rawPosition;\n  this.posFactor = posFactor;\n  this.dimFactor = dimFactor;\n  this.gridSize = gridSize;\n}\n\n// Convert a delta from pixel coordinates to grid coordinates,\n// rounding to whole-number grid coordinates.\nfunction toGridDelta(gridSize, pixelDelta) {\n  return pixelDelta.map(function (v, i) {\n    return Math.round(v / gridSize[i]);\n  });\n}\n\n// Utility function to perform element-by-element multiplication\nfunction multiply(array, factors) {\n  return array.map(function (v, i) {\n    return v * factors[i];\n  });\n}\n\n// Utility function to perform element-by-element addition\nfunction add(array, other) {\n  return array.map(function (v, i) {\n    return v + other[i];\n  });\n}\n\n// Utility function to perform element-by-element max-choosing\nfunction max(array, other) {\n  return array.map(function (v, i) {\n    return Math.max(v, other[i]);\n  });\n}\n\n/**\n * Get a new position object in grid coordinates, with\n * position and dimensions both offset appropriately\n * according to the factors supplied in the constructor.\n * @param {number[]} pixelDelta the offset from the\n *        original position, in pixels\n */\nLayoutDrag.prototype.getAdjustedPositionAndDimensions = function (pixelDelta) {\n  const gridDelta = toGridDelta(this.gridSize, pixelDelta);\n\n  return {\n    position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]),\n    dimensions: max(add(this.rawPosition.dimensions, multiply(gridDelta, this.dimFactor)), [1, 1])\n  };\n};\n\nLayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) {\n  const gridDelta = toGridDelta(this.gridSize, pixelDelta);\n\n  return {\n    position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0])\n  };\n};\n"
  },
  {
    "path": "src/plugins/displayLayout/actions/CopyToClipboardAction.js",
    "content": "import clipboard from '@/utils/clipboard';\n\nconst COPY_TO_CLIPBOARD_ACTION_KEY = 'copyToClipboard';\n\nclass CopyToClipboardAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-duplicate';\n    this.description = 'Copy value to clipboard';\n    this.group = 'action';\n    this.key = COPY_TO_CLIPBOARD_ACTION_KEY;\n    this.name = 'Copy to Clipboard';\n    this.priority = 1;\n  }\n\n  invoke(objectPath, view = {}) {\n    const viewContext = view.getViewContext && view.getViewContext();\n    const formattedValue = viewContext.row.formattedValueForCopy();\n\n    clipboard\n      .updateClipboard(formattedValue)\n      .then(() => {\n        this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `);\n      })\n      .catch(() => {\n        this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `);\n      });\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const viewContext = view.getViewContext && view.getViewContext();\n    const row = viewContext && viewContext.row;\n    if (!row) {\n      return false;\n    }\n\n    return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function';\n  }\n}\n\nexport { COPY_TO_CLIPBOARD_ACTION_KEY };\n\nexport default CopyToClipboardAction;\n"
  },
  {
    "path": "src/plugins/displayLayout/components/AlphanumericFormat.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspect-properties\">\n    <ul class=\"c-inspect-properties__section\">\n      <li class=\"c-inspect-properties__row\">\n        <div\n          class=\"c-inspect-properties__label\"\n          title=\"Printf formatting for the selected telemetry\"\n        >\n          <label for=\"telemetryPrintfFormat\">Format</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            id=\"telemetryPrintfFormat\"\n            type=\"text\"\n            :value=\"telemetryFormat\"\n            :placeholder=\"nonMixedFormat ? '' : 'Mixed'\"\n            @change=\"formatTelemetry\"\n          />\n          <template v-if=\"!isEditing && telemetryFormat?.length\">\n            {{ telemetryFormat }}\n          </template>\n        </div>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'AlphanumericFormat',\n  inject: ['openmct', 'objectPath'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing(),\n      telemetryFormat: undefined,\n      nonMixedFormat: false\n    };\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.toggleEdit);\n    this.openmct.selection.on('change', this.handleSelection);\n    this.handleSelection(this.openmct.selection.get());\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.toggleEdit);\n    this.openmct.selection.off('change', this.handleSelection);\n  },\n  methods: {\n    toggleEdit(isEditing) {\n      this.isEditing = isEditing;\n    },\n    formatTelemetry(event) {\n      const newFormat = event.currentTarget.value;\n      this.openmct.selection.get().forEach((selectionPath) => {\n        selectionPath[0].context.updateTelemetryFormat(newFormat);\n      });\n      this.telemetryFormat = newFormat;\n    },\n    handleSelection(selection) {\n      if (selection.length === 0 || selection[0].length < 2) {\n        return;\n      }\n\n      let layoutItem = selection[0][0].context.layoutItem;\n\n      if (!layoutItem) {\n        return;\n      }\n\n      let format = layoutItem.format;\n      this.nonMixedFormat = selection.every((selectionPath) => {\n        return selectionPath[0].context.layoutItem.format === format;\n      });\n\n      this.telemetryFormat = this.nonMixedFormat ? format : '';\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/BoxView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <div\n        class=\"c-box-view u-style-receiver js-style-receiver\"\n        :class=\"[styleClass]\"\n        :style=\"style\"\n        role=\"application\"\n        aria-roledescription=\"draggable box\"\n        aria-label=\"Box\"\n      ></div>\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\nimport LayoutFrame from './LayoutFrame.vue';\n\nexport default {\n  makeDefinition() {\n    return {\n      fill: '#666666',\n      stroke: '',\n      x: 1,\n      y: 1,\n      width: 10,\n      height: 5\n    };\n  },\n  components: {\n    LayoutFrame\n  },\n  mixins: [conditionalStylesMixin],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    initSelect: Boolean,\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  computed: {\n    style() {\n      if (this.itemStyle) {\n        return this.itemStyle;\n      } else {\n        return {\n          backgroundColor: this.item.fill,\n          border: this.item.stroke ? '1px solid ' + this.item.stroke : ''\n        };\n      }\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.context = {\n      layoutItem: this.item,\n      index: this.index\n    };\n    this.removeSelectable = this.openmct.selection.selectable(\n      this.$el,\n      this.context,\n      this.initSelect\n    );\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n  },\n  methods: {\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/DisplayLayout.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    id=\"display-layout-drop-area\"\n    class=\"l-layout u-style-receiver js-style-receiver\"\n    :class=\"{\n      'is-multi-selected': selectedLayoutItems.length > 1,\n      'allow-editing': isEditing\n    }\"\n    :aria-label=\"`${domainObject.name} Layout`\"\n    @dragover=\"handleDragOver\"\n    @click.capture=\"bypassSelection\"\n    @drop=\"handleDrop\"\n  >\n    <display-layout-grid\n      v-if=\"isEditing\"\n      :grid-size=\"gridSize\"\n      :show-grid=\"showGrid\"\n      :grid-dimensions=\"gridDimensions\"\n      :aria-label=\"`${domainObject.name} Layout Grid`\"\n      :aria-hidden=\"showGrid ? 'false' : 'true'\"\n    />\n    <div\n      v-if=\"shouldDisplayLayoutDimensions\"\n      class=\"l-layout__dimensions\"\n      :style=\"layoutDimensionsStyle\"\n    >\n      <div class=\"l-layout__dimensions-vals\">\n        {{ layoutDimensions[0] }},{{ layoutDimensions[1] }}\n      </div>\n    </div>\n    <component\n      :is=\"item.type\"\n      v-for=\"(item, index) in layoutItems\"\n      :key=\"item.id\"\n      :ref=\"`layout-item-${item.id}`\"\n      :item=\"item\"\n      :grid-size=\"gridSize\"\n      :init-select=\"initSelectIndex === index\"\n      :index=\"index\"\n      :multi-select=\"selectedLayoutItems.length > 1 || null\"\n      :is-editing=\"isEditing\"\n      @context-click=\"updateViewContext\"\n      @move=\"move\"\n      @end-move=\"endMove\"\n      @end-line-resize=\"endLineResize\"\n      @format-changed=\"updateTelemetryFormat\"\n    />\n    <edit-marquee\n      v-if=\"showMarquee\"\n      :grid-size=\"gridSize\"\n      :selected-layout-items=\"selectedLayoutItems\"\n      @end-resize=\"endResize\"\n    />\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { v4 as uuid } from 'uuid';\n\nimport BoxView from './BoxView.vue';\nimport DisplayLayoutGrid from './DisplayLayoutGrid.vue';\nimport EditMarquee from './EditMarquee.vue';\nimport EllipseView from './EllipseView.vue';\nimport ImageView from './ImageView.vue';\nimport LineView from './LineView.vue';\nimport SubobjectView from './SubobjectView.vue';\nimport TelemetryView from './TelemetryView.vue';\nimport TextView from './TextView.vue';\n\nconst TELEMETRY_IDENTIFIER_FUNCTIONS = {\n  table: (domainObject) => {\n    return Promise.resolve(domainObject.composition);\n  },\n  'telemetry.plot.overlay': (domainObject) => {\n    return Promise.resolve(domainObject.composition);\n  },\n  'telemetry.plot.stacked': (domainObject, openmct) => {\n    let composition = openmct.composition.get(domainObject);\n\n    return composition.load().then((objects) => {\n      let identifiers = [];\n      objects.forEach((object) => {\n        if (object.type === 'telemetry.plot.overlay') {\n          identifiers.push(...object.composition);\n        } else {\n          identifiers.push(object.identifier);\n        }\n      });\n\n      return Promise.resolve(identifiers);\n    });\n  }\n};\n\nconst ITEM_TYPE_VIEW_MAP = {\n  'subobject-view': SubobjectView,\n  'telemetry-view': TelemetryView,\n  'box-view': BoxView,\n  'ellipse-view': EllipseView,\n  'line-view': LineView,\n  'text-view': TextView,\n  'image-view': ImageView\n};\nconst ORDERS = {\n  top: Number.POSITIVE_INFINITY,\n  up: 1,\n  down: -1,\n  bottom: Number.NEGATIVE_INFINITY\n};\nconst DRAG_OBJECT_TRANSFER_PREFIX = 'openmct/domain-object/';\nconst DUPLICATE_OFFSET = 3;\n\nlet components = ITEM_TYPE_VIEW_MAP;\ncomponents['edit-marquee'] = EditMarquee;\ncomponents['display-layout-grid'] = DisplayLayoutGrid;\n\nfunction getItemDefinition(itemType, ...options) {\n  let itemView = ITEM_TYPE_VIEW_MAP[itemType];\n\n  if (!itemView) {\n    throw `Invalid itemType: ${itemType}`;\n  }\n\n  return itemView.makeDefinition(...options);\n}\n\nexport default {\n  components: components,\n  inject: ['openmct', 'objectPath', 'options', 'currentView'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  data() {\n    return {\n      initSelectIndex: undefined,\n      selection: [],\n      showGrid: true,\n      viewContext: {},\n      gridDimensions: [0, 0],\n      layoutItems: this.domainObject.configuration.items || []\n    };\n  },\n  computed: {\n    gridSize() {\n      return this.domainObject.configuration.layoutGrid.map(Number);\n    },\n    selectedLayoutItems() {\n      return this.layoutItems.filter((item) => {\n        return this.itemIsInCurrentSelection(item);\n      });\n    },\n    layoutDimensions() {\n      return this.domainObject.configuration.layoutDimensions;\n    },\n    shouldDisplayLayoutDimensions() {\n      return this.layoutDimensions && this.layoutDimensions[0] > 0 && this.layoutDimensions[1] > 0;\n    },\n    layoutDimensionsStyle() {\n      const width = `${this.layoutDimensions[0]}px`;\n      const height = `${this.layoutDimensions[1]}px`;\n\n      return {\n        width,\n        height\n      };\n    },\n    showMarquee() {\n      let selectionPath = this.selection[0];\n      let singleSelectedLine =\n        this.selection.length === 1 &&\n        selectionPath[0].context.layoutItem &&\n        selectionPath[0].context.layoutItem.type === 'line-view';\n\n      return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;\n    }\n  },\n  watch: {\n    isEditing(value) {\n      if (value) {\n        this.showGrid = value;\n      }\n    },\n    layoutItems: {\n      handler(value) {\n        this.updateGrid();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.setSelection);\n    this.initializeItems();\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('add', this.addChild);\n    this.composition.on('remove', this.removeChild);\n    this.composition.load();\n    this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];\n\n    this.unObserveItems = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.items',\n      (items) => {\n        this.layoutItems = [...items];\n      }\n    );\n\n    this.watchDisplayResize();\n  },\n  beforeUnmount() {\n    if (this.unObserveItems) {\n      this.unObserveItems();\n    }\n    this.unwatchDisplayResize();\n    this.openmct.selection.off('change', this.setSelection);\n    this.composition.off('add', this.addChild);\n    this.composition.off('remove', this.removeChild);\n  },\n  methods: {\n    updateGrid() {\n      let wMax = this.$el.clientWidth / this.gridSize[0];\n      let hMax = this.$el.clientHeight / this.gridSize[1];\n      this.layoutItems.forEach((item) => {\n        if (item.x + item.width > wMax) {\n          wMax = item.x + item.width + 2;\n        }\n\n        if (item.y + item.height > hMax) {\n          hMax = item.y + item.height + 2;\n        }\n      });\n      this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];\n    },\n    clearSelection() {\n      this.$el.click();\n    },\n    watchDisplayResize() {\n      this.unwatchDisplayResize();\n      this.resizeObserver = new ResizeObserver(this.updateGrid);\n\n      this.resizeObserver.observe(this.$el);\n    },\n    unwatchDisplayResize() {\n      if (this.resizeObserver) {\n        this.resizeObserver.disconnect();\n      }\n    },\n    addElement(itemType, element) {\n      this.addItem(itemType + '-view', element);\n    },\n    setSelection(selection) {\n      this.selection = [...selection];\n    },\n    itemIsInCurrentSelection(item) {\n      return this.selection.some(\n        (selectionPath) =>\n          selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.id === item.id\n      );\n    },\n    bypassSelection($event) {\n      if (this.dragInProgress) {\n        if ($event) {\n          $event.stopImmediatePropagation();\n        }\n\n        this.dragInProgress = false;\n\n        return;\n      }\n    },\n    endLineResize(item, updates) {\n      this.dragInProgress = true;\n      let index = this.layoutItems.indexOf(item);\n      Object.assign(item, updates);\n      this.mutate(`configuration.items[${index}]`, item);\n    },\n    endResize(scaleWidth, scaleHeight, marqueeStart, marqueeOffset) {\n      this.dragInProgress = true;\n      this.layoutItems.forEach((item) => {\n        if (this.itemIsInCurrentSelection(item)) {\n          let itemXInMarqueeSpace = item.x - marqueeStart.x;\n          let itemXInMarqueeSpaceAfterScale = Math.round(itemXInMarqueeSpace * scaleWidth);\n          item.x = itemXInMarqueeSpaceAfterScale + marqueeOffset.x + marqueeStart.x;\n\n          let itemYInMarqueeSpace = item.y - marqueeStart.y;\n          let itemYInMarqueeSpaceAfterScale = Math.round(itemYInMarqueeSpace * scaleHeight);\n          item.y = itemYInMarqueeSpaceAfterScale + marqueeOffset.y + marqueeStart.y;\n\n          if (item.x2) {\n            let itemX2InMarqueeSpace = item.x2 - marqueeStart.x;\n            let itemX2InMarqueeSpaceAfterScale = Math.round(itemX2InMarqueeSpace * scaleWidth);\n            item.x2 = itemX2InMarqueeSpaceAfterScale + marqueeOffset.x + marqueeStart.x;\n          } else {\n            item.width = Math.round(item.width * scaleWidth);\n          }\n\n          if (item.y2) {\n            let itemY2InMarqueeSpace = item.y2 - marqueeStart.y;\n            let itemY2InMarqueeSpaceAfterScale = Math.round(itemY2InMarqueeSpace * scaleHeight);\n            item.y2 = itemY2InMarqueeSpaceAfterScale + marqueeOffset.y + marqueeStart.y;\n          } else {\n            item.height = Math.round(item.height * scaleHeight);\n          }\n        }\n      });\n      this.mutate('configuration.items', this.layoutItems);\n    },\n    move(gridDelta) {\n      this.dragInProgress = true;\n\n      if (!this.initialPositions) {\n        this.initialPositions = {};\n        _.cloneDeep(this.selectedLayoutItems).forEach((selectedItem) => {\n          if (selectedItem.type === 'line-view') {\n            this.initialPositions[selectedItem.id] = [\n              selectedItem.x,\n              selectedItem.y,\n              selectedItem.x2,\n              selectedItem.y2\n            ];\n            this.startingMinX2 =\n              this.startingMinX2 !== undefined\n                ? Math.min(this.startingMinX2, selectedItem.x2)\n                : selectedItem.x2;\n            this.startingMinY2 =\n              this.startingMinY2 !== undefined\n                ? Math.min(this.startingMinY2, selectedItem.y2)\n                : selectedItem.y2;\n          } else {\n            this.initialPositions[selectedItem.id] = [selectedItem.x, selectedItem.y];\n          }\n\n          this.startingMinX =\n            this.startingMinX !== undefined\n              ? Math.min(this.startingMinX, selectedItem.x)\n              : selectedItem.x;\n          this.startingMinY =\n            this.startingMinY !== undefined\n              ? Math.min(this.startingMinY, selectedItem.y)\n              : selectedItem.y;\n        });\n      }\n\n      this.layoutItems.forEach((item) => {\n        if (this.initialPositions[item.id]) {\n          this.updateItemPosition(item, gridDelta);\n        }\n      });\n    },\n    updateItemPosition(item, gridDelta) {\n      let startingPosition = this.initialPositions[item.id];\n      let [startingX, startingY, startingX2, startingY2] = startingPosition;\n\n      if (this.startingMinX + gridDelta[0] >= 0) {\n        if (item.x2 !== undefined) {\n          if (this.startingMinX2 + gridDelta[0] >= 0) {\n            item.x = startingX + gridDelta[0];\n          }\n        } else {\n          item.x = startingX + gridDelta[0];\n        }\n      }\n\n      if (this.startingMinY + gridDelta[1] >= 0) {\n        if (item.y2 !== undefined) {\n          if (this.startingMinY2 + gridDelta[1] >= 0) {\n            item.y = startingY + gridDelta[1];\n          }\n        } else {\n          item.y = startingY + gridDelta[1];\n        }\n      }\n\n      if (\n        item.x2 !== undefined &&\n        this.startingMinX2 + gridDelta[0] >= 0 &&\n        this.startingMinX + gridDelta[0] >= 0\n      ) {\n        item.x2 = startingX2 + gridDelta[0];\n      }\n\n      if (\n        item.y2 !== undefined &&\n        this.startingMinY2 + gridDelta[1] >= 0 &&\n        this.startingMinY + gridDelta[1] >= 0\n      ) {\n        item.y2 = startingY2 + gridDelta[1];\n      }\n    },\n    endMove() {\n      this.mutate('configuration.items', this.layoutItems);\n      this.initialPositions = undefined;\n      this.startingMinX = undefined;\n      this.startingMinY = undefined;\n      this.startingMinX2 = undefined;\n      this.startingMinY2 = undefined;\n    },\n    mutate(path, value) {\n      this.openmct.objects.mutate(this.domainObject, path, value);\n    },\n    handleDrop($event) {\n      if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {\n        return;\n      }\n\n      $event.preventDefault();\n\n      let domainObject = JSON.parse($event.dataTransfer.getData('openmct/domain-object-path'))[0];\n      let elementRect = this.$el.getBoundingClientRect();\n      let droppedObjectPosition = [\n        Math.floor(($event.pageX - elementRect.left) / this.gridSize[0]),\n        Math.floor(($event.pageY - elementRect.top) / this.gridSize[1])\n      ];\n\n      if (this.isTelemetry(domainObject)) {\n        this.addItem('telemetry-view', domainObject, droppedObjectPosition);\n      } else {\n        let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n        if (!this.objectViewMap[keyString]) {\n          this.addItem('subobject-view', domainObject, droppedObjectPosition);\n        } else {\n          let prompt = this.openmct.overlays.dialog({\n            iconClass: 'alert',\n            message: 'This item is already in layout and will not be added again.',\n            buttons: [\n              {\n                label: 'Ok',\n                callback: function () {\n                  prompt.dismiss();\n                }\n              }\n            ]\n          });\n        }\n      }\n    },\n    containsObject(identifier) {\n      if ('composition' in this.domainObject) {\n        return this.domainObject.composition.some((childId) =>\n          this.openmct.objects.areIdsEqual(childId, identifier)\n        );\n      }\n\n      return false;\n    },\n    handleDragOver($event) {\n      if (this.domainObject.locked) {\n        return;\n      }\n\n      // Get the ID of the dragged object\n      let draggedKeyString = $event.dataTransfer.types\n        .filter((type) => type.startsWith(DRAG_OBJECT_TRANSFER_PREFIX))\n        .map((type) => type.substring(DRAG_OBJECT_TRANSFER_PREFIX.length))[0];\n\n      // If the layout already contains the given object, then shortcut the default dragover behavior and\n      // potentially allow drop. Display layouts allow drag drop of duplicate telemetry objects.\n      if (this.containsObject(draggedKeyString)) {\n        $event.preventDefault();\n      }\n    },\n    isTelemetry(domainObject) {\n      if (\n        this.openmct.telemetry.isTelemetryObject(domainObject) &&\n        !this.options.showAsView.includes(domainObject.type)\n      ) {\n        return true;\n      } else {\n        return false;\n      }\n    },\n    addItem(itemType, ...options) {\n      let item = getItemDefinition(itemType, this.openmct, this.gridSize, ...options);\n      item.type = itemType;\n      item.id = uuid();\n      this.trackItem(item);\n      this.layoutItems.push(item);\n      this.openmct.objects.mutate(this.domainObject, 'configuration.items', this.layoutItems);\n      this.initSelectIndex = this.layoutItems.length - 1;\n    },\n    trackItem(item) {\n      if (!item.identifier) {\n        return;\n      }\n\n      let keyString = this.openmct.objects.makeKeyString(item.identifier);\n\n      if (item.type === 'telemetry-view') {\n        let count = this.telemetryViewMap[keyString] || 0;\n        this.telemetryViewMap[keyString] = ++count;\n      } else if (item.type === 'subobject-view') {\n        let count = this.objectViewMap[keyString] || 0;\n        this.objectViewMap[keyString] = ++count;\n      }\n    },\n    removeItem(selectedItems) {\n      let indices = [];\n      this.initSelectIndex = -1;\n      selectedItems.forEach((selectedItem) => {\n        indices.push(selectedItem[0].context.index);\n        this.untrackItem(selectedItem[0].context.layoutItem);\n      });\n      _.pullAt(this.layoutItems, indices);\n      this.mutate('configuration.items', this.layoutItems);\n      this.clearSelection();\n    },\n    untrackItem(item) {\n      if (!item.identifier) {\n        return;\n      }\n\n      let keyString = this.openmct.objects.makeKeyString(item.identifier);\n      let telemetryViewCount = this.telemetryViewMap[keyString];\n      let objectViewCount = this.objectViewMap[keyString];\n\n      if (item.type === 'telemetry-view') {\n        telemetryViewCount = --this.telemetryViewMap[keyString];\n\n        if (telemetryViewCount === 0) {\n          delete this.telemetryViewMap[keyString];\n        }\n      } else if (item.type === 'subobject-view') {\n        objectViewCount = --this.objectViewMap[keyString];\n\n        if (objectViewCount === 0) {\n          delete this.objectViewMap[keyString];\n        }\n      }\n\n      if (!telemetryViewCount && !objectViewCount) {\n        this.removeFromComposition(item);\n      }\n    },\n    removeFromComposition(item) {\n      this.composition.remove(item);\n    },\n    initializeItems() {\n      this.telemetryViewMap = {};\n      this.objectViewMap = {};\n\n      let removedItems = [];\n      this.layoutItems.forEach((item) => {\n        if (item.identifier) {\n          if (this.containsObject(item.identifier)) {\n            this.trackItem(item);\n          } else {\n            removedItems.push(this.openmct.objects.makeKeyString(item.identifier));\n          }\n        }\n      });\n\n      this.startTransaction();\n      removedItems.forEach(this.removeFromConfiguration);\n\n      return this.endTransaction();\n    },\n    isItemAlreadyTracked(child) {\n      let found = false;\n      let keyString = this.openmct.objects.makeKeyString(child.identifier);\n\n      this.layoutItems.forEach((item) => {\n        if (item.identifier) {\n          let itemKeyString = this.openmct.objects.makeKeyString(item.identifier);\n\n          if (itemKeyString === keyString) {\n            found = true;\n\n            return;\n          }\n        }\n      });\n\n      if (found) {\n        return true;\n      } else if (this.isTelemetry(child)) {\n        return this.telemetryViewMap[keyString] && this.objectViewMap[keyString];\n      } else {\n        return this.objectViewMap[keyString];\n      }\n    },\n    addChild(child) {\n      if (this.isItemAlreadyTracked(child)) {\n        return;\n      }\n\n      let type;\n\n      if (this.isTelemetry(child)) {\n        type = 'telemetry-view';\n      } else {\n        type = 'subobject-view';\n      }\n\n      this.addItem(type, child);\n    },\n    removeChild(identifier) {\n      let keyString = this.openmct.objects.makeKeyString(identifier);\n\n      if (this.objectViewMap[keyString]) {\n        delete this.objectViewMap[keyString];\n        this.removeFromConfiguration(keyString);\n      } else if (this.telemetryViewMap[keyString]) {\n        delete this.telemetryViewMap[keyString];\n        this.removeFromConfiguration(keyString);\n      }\n    },\n    removeFromConfiguration(keyString) {\n      let layoutItems = this.layoutItems.filter((item) => {\n        if (!item.identifier) {\n          return true;\n        } else {\n          return this.openmct.objects.makeKeyString(item.identifier) !== keyString;\n        }\n      });\n      this.layoutItems = [...layoutItems];\n      this.mutate('configuration.items', layoutItems);\n      this.clearSelection();\n    },\n    orderItem(position, selectedItems) {\n      let delta = ORDERS[position];\n      let indices = [];\n      let items = [];\n\n      Object.assign(items, this.layoutItems);\n      this.selectedLayoutItems.forEach((selectedItem) => {\n        indices.push(this.layoutItems.indexOf(selectedItem));\n      });\n      indices.sort((a, b) => a - b);\n\n      if (position === 'top' || position === 'up') {\n        indices.reverse();\n      }\n\n      if (position === 'top' || position === 'bottom') {\n        this.moveToTopOrBottom(position, indices, items, delta);\n      } else {\n        this.moveUpOrDown(position, indices, items, delta);\n      }\n\n      this.mutate('configuration.items', this.layoutItems);\n    },\n    moveUpOrDown(position, indices, items, delta) {\n      let previousItemIndex = -1;\n      let newIndex = -1;\n\n      indices.forEach((itemIndex, index) => {\n        let isAdjacentItemSelected =\n          position === 'up'\n            ? itemIndex + 1 === previousItemIndex\n            : itemIndex - 1 === previousItemIndex;\n\n        if (index > 0 && isAdjacentItemSelected) {\n          if (position === 'up') {\n            newIndex -= 1;\n          } else {\n            newIndex += 1;\n          }\n        } else {\n          newIndex = Math.max(Math.min(itemIndex + delta, this.layoutItems.length - 1), 0);\n        }\n\n        previousItemIndex = itemIndex;\n        this.updateItemOrder(newIndex, itemIndex, items);\n      });\n    },\n    moveToTopOrBottom(position, indices, items, delta) {\n      let newIndex = -1;\n\n      indices.forEach((itemIndex, index) => {\n        if (index === 0) {\n          newIndex = Math.max(Math.min(itemIndex + delta, this.layoutItems.length - 1), 0);\n        } else {\n          if (position === 'top') {\n            newIndex -= 1;\n          } else {\n            newIndex += 1;\n          }\n        }\n\n        this.updateItemOrder(newIndex, itemIndex, items);\n      });\n    },\n    updateItemOrder(newIndex, itemIndex, items) {\n      if (newIndex !== itemIndex) {\n        this.layoutItems.splice(itemIndex, 1);\n        this.layoutItems.splice(newIndex, 0, items[itemIndex]);\n      }\n    },\n    updateTelemetryFormat(item, format) {\n      let index = this.layoutItems.findIndex((layoutItem) => {\n        return layoutItem.id === item.id;\n      });\n\n      item.format = format;\n      this.mutate(`configuration.items[${index}]`, item);\n    },\n    createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {\n      let identifier = {\n        key: uuid(),\n        namespace: this.domainObject.identifier.namespace\n      };\n      let type = this.openmct.types.get(viewType);\n      let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;\n      let object = {};\n\n      if (model) {\n        object = _.cloneDeep(model);\n      } else {\n        object.type = viewType;\n        type.definition.initialize(object);\n        object.composition.push(...composition);\n      }\n\n      if (object.modified || object.persisted) {\n        object.modified = undefined;\n        object.persisted = undefined;\n        delete object.modified;\n        delete object.persisted;\n      }\n\n      object.name = objectName;\n      object.identifier = identifier;\n      object.location = parentKeyString;\n\n      let savedResolve;\n      this.openmct.objects.save(object).then(() => {\n        savedResolve(object);\n      });\n\n      return new Promise((resolve) => {\n        savedResolve = resolve;\n      });\n    },\n    convertToTelemetryView(identifier, position) {\n      this.openmct.objects.get(identifier).then((domainObject) => {\n        this.composition.add(domainObject);\n        this.addItem('telemetry-view', domainObject, position);\n      });\n    },\n    dispatchMultipleSelection(selectItemsArray) {\n      let event = new MouseEvent('click', {\n        bubbles: true,\n        shiftKey: true,\n        cancelable: true,\n        view: window\n      });\n\n      selectItemsArray.forEach((id) => {\n        let refId = `layout-item-${id}`;\n        let component = this.$refs[refId] && this.$refs[refId][0];\n\n        if (component) {\n          component.immediatelySelect = event;\n          component.$el.dispatchEvent(event);\n        }\n      });\n    },\n    duplicateItem(selectedItems) {\n      let objectStyles = this.domainObject.configuration.objectStyles || {};\n      let selectItemsArray = [];\n      let newDomainObjectsArray = [];\n\n      selectedItems.forEach((selectedItem) => {\n        let layoutItem = selectedItem[0].context.layoutItem;\n        let domainObject = selectedItem[0].context.item;\n        let layoutItemStyle = objectStyles[layoutItem.id];\n        let copy = _.cloneDeep(layoutItem);\n\n        copy.id = uuid();\n        selectItemsArray.push(copy.id);\n\n        let offsetKeys = ['x', 'y'];\n\n        if (copy.type === 'line-view') {\n          offsetKeys = offsetKeys.concat(['x2', 'y2']);\n        }\n\n        if (copy.type === 'subobject-view') {\n          this.createNewDomainObject(\n            domainObject,\n            domainObject.composition,\n            domainObject.type,\n            'duplicate',\n            domainObject\n          ).then((newDomainObject) => {\n            newDomainObjectsArray.push(newDomainObject);\n            copy.identifier = newDomainObject.identifier;\n          });\n        }\n\n        offsetKeys.forEach((key) => {\n          copy[key] += DUPLICATE_OFFSET;\n        });\n\n        if (layoutItemStyle) {\n          objectStyles[copy.id] = layoutItemStyle;\n        }\n\n        this.trackItem(copy);\n        this.layoutItems.push(copy);\n      });\n\n      this.$nextTick(() => {\n        this.openmct.objects.mutate(this.domainObject, 'configuration.items', this.layoutItems);\n        this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', objectStyles);\n        this.clearSelection();\n\n        newDomainObjectsArray.forEach((domainObject) => {\n          this.composition.add(domainObject);\n        });\n        this.dispatchMultipleSelection(selectItemsArray);\n      });\n    },\n    mergeMultipleTelemetryViews(selection, viewType) {\n      let identifiers = selection.map((selectedItem) => {\n        return selectedItem[0].context.layoutItem.identifier;\n      });\n      let firstDomainObject = selection[0][0].context.item;\n      let firstLayoutItem = selection[0][0].context.layoutItem;\n      let position = [firstLayoutItem.x, firstLayoutItem.y];\n      let mockDomainObject = {\n        name: 'Merged Telemetry Views',\n        identifier: firstDomainObject.identifier\n      };\n      this.createNewDomainObject(mockDomainObject, identifiers, viewType).then(\n        (newDomainObject) => {\n          this.composition.add(newDomainObject);\n          this.addItem('subobject-view', newDomainObject, position);\n          this.removeItem(selection);\n          this.initSelectIndex = this.layoutItems.length - 1;\n        }\n      );\n    },\n    mergeMultipleOverlayPlots(selection, viewType) {\n      let overlayPlots = selection.map((selectedItem) => selectedItem[0].context.item);\n      let overlayPlotIdentifiers = overlayPlots.map((overlayPlot) => overlayPlot.identifier);\n      let firstOverlayPlot = overlayPlots[0];\n      let firstLayoutItem = selection[0][0].context.layoutItem;\n      let position = [firstLayoutItem.x, firstLayoutItem.y];\n      let mockDomainObject = {\n        name: 'Merged Overlay Plots',\n        identifier: firstOverlayPlot.identifier\n      };\n      this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then(\n        (newDomainObject) => {\n          let newDomainObjectKeyString = this.openmct.objects.makeKeyString(\n            newDomainObject.identifier\n          );\n          let domainObjectKeyString = this.openmct.objects.makeKeyString(\n            this.domainObject.identifier\n          );\n\n          this.composition.add(newDomainObject);\n          this.addItem('subobject-view', newDomainObject, position);\n\n          overlayPlots.forEach((overlayPlot) => {\n            if (overlayPlot.location === domainObjectKeyString) {\n              this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);\n            }\n          });\n\n          this.removeItem(selection);\n          this.initSelectIndex = this.layoutItems.length - 1;\n        }\n      );\n    },\n    getTelemetryIdentifiers(domainObject) {\n      let method = TELEMETRY_IDENTIFIER_FUNCTIONS[domainObject.type];\n\n      if (method) {\n        return method(domainObject, this.openmct);\n      } else {\n        throw 'No method identified for domainObject type';\n      }\n    },\n    switchViewType(context, viewType, selection) {\n      let domainObject = context.item;\n      let layoutItem = context.layoutItem;\n      let position = [layoutItem.x, layoutItem.y];\n      let layoutType = 'subobject-view';\n\n      if (layoutItem.type === 'telemetry-view') {\n        this.createNewDomainObject(domainObject, [domainObject.identifier], viewType).then(\n          (newDomainObject) => {\n            this.composition.add(newDomainObject);\n            this.addItem(layoutType, newDomainObject, position);\n          }\n        );\n      } else {\n        this.getTelemetryIdentifiers(domainObject).then((identifiers) => {\n          if (viewType === 'telemetry-view') {\n            identifiers.forEach((identifier, index) => {\n              let positionX = position[0] + index * DUPLICATE_OFFSET;\n              let positionY = position[1] + index * DUPLICATE_OFFSET;\n\n              this.convertToTelemetryView(identifier, [positionX, positionY]);\n            });\n          } else {\n            this.createNewDomainObject(domainObject, identifiers, viewType).then(\n              (newDomainObject) => {\n                this.composition.add(newDomainObject);\n                this.addItem(layoutType, newDomainObject, position);\n              }\n            );\n          }\n        });\n      }\n\n      this.removeItem(selection);\n      this.initSelectIndex = this.layoutItems.length - 1; //restore selection\n    },\n    startTransaction() {\n      if (!this.openmct.objects.isTransactionActive()) {\n        this.transaction = this.openmct.objects.startTransaction();\n      }\n    },\n    async endTransaction() {\n      if (!this.transaction) {\n        return;\n      }\n\n      await this.transaction.commit();\n      this.openmct.objects.endTransaction();\n      this.transaction = null;\n    },\n    toggleGrid() {\n      this.showGrid = !this.showGrid;\n    },\n    updateViewContext(viewContext) {\n      this.viewContext.row = viewContext;\n    },\n    getViewContext() {\n      return this.viewContext;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/DisplayLayoutGrid.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"l-layout__grid-holder\"\n    :class=\"{ 'c-grid': showGrid }\"\n    role=\"grid\"\n    aria-label=\"Layout Grid\"\n    :aria-hidden=\"showGrid ? 'false' : 'true'\"\n    :aria-live=\"showGrid ? 'polite' : 'off'\"\n  >\n    <div\n      v-if=\"gridSize[0] >= 3\"\n      class=\"c-grid__x l-grid l-grid-x\"\n      :style=\"[\n        {\n          backgroundSize: gridSize[0] + 'px 100%',\n          width: gridDimensions[0] + 'px',\n          height: gridDimensions[1] + 'px'\n        }\n      ]\"\n    ></div>\n    <div\n      v-if=\"gridSize[1] >= 3\"\n      class=\"c-grid__y l-grid l-grid-y\"\n      :style=\"[\n        {\n          backgroundSize: '100%' + gridSize[1] + 'px',\n          width: gridDimensions[0] + 'px',\n          height: gridDimensions[1] + 'px'\n        }\n      ]\"\n    ></div>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    showGrid: {\n      type: Boolean,\n      required: true\n    },\n    gridDimensions: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/EditMarquee.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <!-- Resize handles -->\n  <div class=\"c-frame-edit\" :style=\"marqueeStyle\">\n    <div\n      class=\"c-frame-edit__handle c-frame-edit__handle--nw\"\n      @mousedown.left=\"startResize([1, 1], [-1, -1], $event)\"\n    ></div>\n    <div\n      class=\"c-frame-edit__handle c-frame-edit__handle--ne\"\n      @mousedown.left=\"startResize([0, 1], [1, -1], $event)\"\n    ></div>\n    <div\n      class=\"c-frame-edit__handle c-frame-edit__handle--sw\"\n      @mousedown.left=\"startResize([1, 0], [-1, 1], $event)\"\n    ></div>\n    <div\n      class=\"c-frame-edit__handle c-frame-edit__handle--se\"\n      @mousedown.left=\"startResize([0, 0], [1, 1], $event)\"\n    ></div>\n  </div>\n</template>\n\n<script>\nimport LayoutDrag from './../LayoutDrag.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    selectedLayoutItems: {\n      type: Array,\n      default: undefined\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    }\n  },\n  emits: ['end-resize'],\n  data() {\n    return {\n      dragPosition: undefined\n    };\n  },\n  computed: {\n    marqueePosition() {\n      let x = Number.POSITIVE_INFINITY;\n      let y = Number.POSITIVE_INFINITY;\n      let width = Number.NEGATIVE_INFINITY;\n      let height = Number.NEGATIVE_INFINITY;\n\n      this.selectedLayoutItems.forEach((item) => {\n        if (item.x2 !== undefined) {\n          let lineWidth = Math.abs(item.x - item.x2);\n          let lineMinX = Math.min(item.x, item.x2);\n          x = Math.min(lineMinX, x);\n          width = Math.max(lineWidth + lineMinX, width);\n        } else {\n          x = Math.min(item.x, x);\n          width = Math.max(item.width + item.x, width);\n        }\n\n        if (item.y2 !== undefined) {\n          let lineHeight = Math.abs(item.y - item.y2);\n          let lineMinY = Math.min(item.y, item.y2);\n          y = Math.min(lineMinY, y);\n          height = Math.max(lineHeight + lineMinY, height);\n        } else {\n          y = Math.min(item.y, y);\n          height = Math.max(item.height + item.y, height);\n        }\n      });\n\n      if (this.dragPosition) {\n        [x, y] = this.dragPosition.position;\n        [width, height] = this.dragPosition.dimensions;\n      } else {\n        width = width - x;\n        height = height - y;\n      }\n\n      return {\n        x: x,\n        y: y,\n        width: width,\n        height: height\n      };\n    },\n    marqueeStyle() {\n      return {\n        left: this.gridSize[0] * this.marqueePosition.x + 'px',\n        top: this.gridSize[1] * this.marqueePosition.y + 'px',\n        width: this.gridSize[0] * this.marqueePosition.width + 'px',\n        height: this.gridSize[1] * this.marqueePosition.height + 'px'\n      };\n    }\n  },\n  methods: {\n    updatePosition(event) {\n      let currentPosition = [event.pageX, event.pageY];\n      this.initialPosition = this.initialPosition || currentPosition;\n      this.delta = currentPosition.map(\n        function (value, index) {\n          return value - this.initialPosition[index];\n        }.bind(this)\n      );\n    },\n    startResize(posFactor, dimFactor, event) {\n      document.body.addEventListener('mousemove', this.continueResize);\n      document.body.addEventListener('mouseup', this.endResize);\n      this.marqueeStartPosition = {\n        position: [this.marqueePosition.x, this.marqueePosition.y],\n        dimensions: [this.marqueePosition.width, this.marqueePosition.height]\n      };\n      this.updatePosition(event);\n      this.activeDrag = new LayoutDrag(\n        this.marqueeStartPosition,\n        posFactor,\n        dimFactor,\n        this.gridSize\n      );\n      event.preventDefault();\n    },\n    continueResize(event) {\n      event.preventDefault();\n      this.updatePosition(event);\n      this.dragPosition = this.activeDrag.getAdjustedPositionAndDimensions(this.delta);\n    },\n    endResize(event) {\n      document.body.removeEventListener('mousemove', this.continueResize);\n      document.body.removeEventListener('mouseup', this.endResize);\n      this.continueResize(event);\n\n      let marqueeStartWidth = this.marqueeStartPosition.dimensions[0];\n      let marqueeStartHeight = this.marqueeStartPosition.dimensions[1];\n      let marqueeStartX = this.marqueeStartPosition.position[0];\n      let marqueeStartY = this.marqueeStartPosition.position[1];\n\n      let marqueeEndX = this.dragPosition.position[0];\n      let marqueeEndY = this.dragPosition.position[1];\n      let marqueeEndWidth = this.dragPosition.dimensions[0];\n      let marqueeEndHeight = this.dragPosition.dimensions[1];\n\n      let scaleWidth = marqueeEndWidth / marqueeStartWidth;\n      let scaleHeight = marqueeEndHeight / marqueeStartHeight;\n\n      let marqueeStart = {\n        x: marqueeStartX,\n        y: marqueeStartY,\n        height: marqueeStartWidth,\n        width: marqueeStartHeight\n      };\n      let marqueeEnd = {\n        x: marqueeEndX,\n        y: marqueeEndY,\n        width: marqueeEndWidth,\n        height: marqueeEndHeight\n      };\n      let marqueeOffset = {\n        x: marqueeEnd.x - marqueeStart.x,\n        y: marqueeEnd.y - marqueeStart.y\n      };\n\n      this.$emit('end-resize', scaleWidth, scaleHeight, marqueeStart, marqueeOffset);\n      this.dragPosition = undefined;\n      this.initialPosition = undefined;\n      this.marqueeStartPosition = undefined;\n      this.delta = undefined;\n      event.preventDefault();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/EllipseView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <div\n        class=\"c-ellipse-view u-style-receiver js-style-receiver\"\n        :class=\"[styleClass]\"\n        :style=\"style\"\n        role=\"application\"\n        aria-roledescription=\"draggable ellipse\"\n        aria-label=\"Ellipse\"\n      ></div>\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\nimport LayoutFrame from './LayoutFrame.vue';\n\nexport default {\n  makeDefinition() {\n    return {\n      fill: '#666666',\n      stroke: '',\n      x: 1,\n      y: 1,\n      width: 10,\n      height: 10\n    };\n  },\n  components: {\n    LayoutFrame\n  },\n  mixins: [conditionalStylesMixin],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    initSelect: Boolean,\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  computed: {\n    style() {\n      if (this.itemStyle) {\n        return this.itemStyle;\n      } else {\n        return {\n          backgroundColor: this.item.fill,\n          border: this.item.stroke ? '1px solid ' + this.item.stroke : ''\n        };\n      }\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.context = {\n      layoutItem: this.item,\n      index: this.index\n    };\n    this.removeSelectable = this.openmct.selection.selectable(\n      this.$el,\n      this.context,\n      this.initSelect\n    );\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n  },\n  methods: {\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/ImageView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <div v-show=\"showImage\" aria-label=\"Image View\" class=\"c-image-view\" :style=\"style\"></div>\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport { encode_url } from '../../../utils/encoding';\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\nimport LayoutFrame from './LayoutFrame.vue';\n\nexport default {\n  makeDefinition(openmct, gridSize, element) {\n    return {\n      stroke: 'transparent',\n      x: 1,\n      y: 1,\n      width: 10,\n      height: 5,\n      url: element.url\n    };\n  },\n  components: {\n    LayoutFrame\n  },\n  mixins: [conditionalStylesMixin],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    initSelect: Boolean,\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  computed: {\n    showImage() {\n      return this.isEditing || !this.itemStyle?.isStyleInvisible;\n    },\n    style() {\n      let backgroundImage = `url('${encode_url(this.item.url)}')`;\n      let border = '1px solid ' + this.item.stroke;\n\n      if (this.itemStyle) {\n        if (this.itemStyle.imageUrl !== undefined) {\n          backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`;\n        }\n\n        border = this.itemStyle.border;\n      }\n\n      return {\n        backgroundImage,\n        border\n      };\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.context = {\n      layoutItem: this.item,\n      index: this.index\n    };\n    this.removeSelectable = this.openmct.selection.selectable(\n      this.$el,\n      this.context,\n      this.initSelect\n    );\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n  },\n  methods: {\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/LayoutFrame.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    aria-label=\"sub object frame\"\n    class=\"l-layout__frame c-frame\"\n    :class=\"{\n      'no-frame': !item.hasFrame,\n      'u-inspectable': inspectable\n    }\"\n    :style=\"style\"\n  >\n    <slot name=\"content\"></slot>\n    <div\n      class=\"c-frame__move-bar\"\n      :aria-label=\"`Move ${typeName} Frame`\"\n      @mousedown.left=\"startMove($event)\"\n    ></div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport DRAWING_OBJECT_TYPES from '../DrawingObjectTypes.js';\nimport LayoutDrag from './../LayoutDrag.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  computed: {\n    typeName() {\n      const { type } = this.item;\n      if (DRAWING_OBJECT_TYPES[type]) {\n        return DRAWING_OBJECT_TYPES[type].name;\n      }\n      return 'Sub-object';\n    },\n    size() {\n      let { width, height } = this.item;\n\n      return {\n        width: this.gridSize[0] * width,\n        height: this.gridSize[1] * height\n      };\n    },\n    style() {\n      let { x, y, width, height } = this.item;\n\n      return {\n        left: this.gridSize[0] * x + 'px',\n        top: this.gridSize[1] * y + 'px',\n        width: this.gridSize[0] * width + 'px',\n        height: this.gridSize[1] * height + 'px',\n        minWidth: this.gridSize[0] * width + 'px',\n        minHeight: this.gridSize[1] * height + 'px'\n      };\n    },\n    inspectable() {\n      return this.item.type === 'subobject-view' || this.item.type === 'telemetry-view';\n    }\n  },\n  methods: {\n    updatePosition(event) {\n      let currentPosition = [event.pageX, event.pageY];\n      this.initialPosition = this.initialPosition || currentPosition;\n      this.delta = currentPosition.map(\n        function (value, index) {\n          return value - this.initialPosition[index];\n        }.bind(this)\n      );\n    },\n    startMove(event, posFactor = [1, 1], dimFactor = [0, 0]) {\n      if (!this.isEditing) {\n        return;\n      }\n\n      document.body.addEventListener('mousemove', this.continueMove);\n      document.body.addEventListener('mouseup', this.endMove);\n      this.dragPosition = {\n        position: [this.item.x, this.item.y]\n      };\n      this.updatePosition(event);\n      this.activeDrag = new LayoutDrag(this.dragPosition, posFactor, dimFactor, this.gridSize);\n      event.preventDefault();\n    },\n    continueMove(event) {\n      event.preventDefault();\n      this.updatePosition(event);\n      let newPosition = this.activeDrag.getAdjustedPosition(this.delta);\n\n      if (!_.isEqual(newPosition, this.dragPosition)) {\n        this.dragPosition = newPosition;\n        this.$emit('move', this.toGridDelta(this.delta));\n      }\n    },\n    endMove(event) {\n      document.body.removeEventListener('mousemove', this.continueMove);\n      document.body.removeEventListener('mouseup', this.endMove);\n      this.continueMove(event);\n      this.$emit('end-move');\n      this.dragPosition = undefined;\n      this.initialPosition = undefined;\n      this.delta = undefined;\n      event.preventDefault();\n    },\n    toGridDelta(pixelDelta) {\n      return pixelDelta.map((v, i) => {\n        return Math.round(v / this.gridSize[i]);\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/LineView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"l-layout__frame c-frame no-frame c-line-view\"\n    :class=\"[styleClass]\"\n    :style=\"style\"\n    aria-role=\"application\"\n    aria-roledescription=\"draggable line\"\n    aria-label=\"Line\"\n  >\n    <svg width=\"100%\" height=\"100%\">\n      <line\n        v-bind=\"linePosition\"\n        class=\"c-line-view__hover-indicator\"\n        vector-effect=\"non-scaling-stroke\"\n      />\n      <line\n        v-bind=\"linePosition\"\n        class=\"c-line-view__line\"\n        :stroke=\"stroke\"\n        vector-effect=\"non-scaling-stroke\"\n      />\n    </svg>\n\n    <div class=\"c-frame__move-bar\" @mousedown.left=\"startDrag($event)\"></div>\n    <div v-if=\"showFrameEdit\" class=\"c-frame-edit\">\n      <div\n        class=\"c-frame-edit__handle\"\n        :class=\"startHandleClass\"\n        @mousedown=\"startDrag($event, 'start')\"\n      ></div>\n      <div\n        class=\"c-frame-edit__handle\"\n        :class=\"endHandleClass\"\n        @mousedown=\"startDrag($event, 'end')\"\n      ></div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\n\nconst START_HANDLE_QUADRANTS = {\n  1: 'c-frame-edit__handle--sw',\n  2: 'c-frame-edit__handle--se',\n  3: 'c-frame-edit__handle--ne',\n  4: 'c-frame-edit__handle--nw',\n  5: 'c-frame-edit__handle--nw',\n  6: 'c-frame-edit__handle--ne'\n};\n\nconst END_HANDLE_QUADRANTS = {\n  1: 'c-frame-edit__handle--ne',\n  2: 'c-frame-edit__handle--nw',\n  3: 'c-frame-edit__handle--sw',\n  4: 'c-frame-edit__handle--se',\n  5: 'c-frame-edit__handle--sw',\n  6: 'c-frame-edit__handle--nw'\n};\n\nexport default {\n  makeDefinition() {\n    return {\n      x: 5,\n      y: 10,\n      x2: 10,\n      y2: 5,\n      stroke: '#666666'\n    };\n  },\n  mixins: [conditionalStylesMixin],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    initSelect: Boolean,\n    index: {\n      type: Number,\n      required: true\n    },\n    multiSelect: Boolean,\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move', 'end-line-resize'],\n  data() {\n    return {\n      dragPosition: undefined,\n      dragging: undefined,\n      selection: []\n    };\n  },\n  computed: {\n    showFrameEdit() {\n      let layoutItem = this.selection.length > 0 && this.selection[0][0].context.layoutItem;\n\n      return this.isEditing && !this.multiSelect && layoutItem && layoutItem.id === this.item.id;\n    },\n    position() {\n      let { x, y, x2, y2 } = this.item;\n      if (this.dragging && this.dragPosition) {\n        ({ x, y, x2, y2 } = this.dragPosition);\n      }\n\n      return {\n        x,\n        y,\n        x2,\n        y2\n      };\n    },\n    stroke() {\n      if (this.itemStyle) {\n        if (this.itemStyle.border) {\n          return this.itemStyle.border.replace('1px solid ', '');\n        }\n\n        return '';\n      } else {\n        return this.item.stroke;\n      }\n    },\n    style() {\n      let { x, y, x2, y2 } = this.position;\n      let width = Math.max(this.gridSize[0] * Math.abs(x - x2), 1);\n      let height = Math.max(this.gridSize[1] * Math.abs(y - y2), 1);\n      let left = this.gridSize[0] * Math.min(x, x2);\n      let top = this.gridSize[1] * Math.min(y, y2);\n\n      return {\n        left: `${left}px`,\n        top: `${top}px`,\n        width: `${width}px`,\n        height: `${height}px`\n      };\n    },\n    startHandleClass() {\n      return START_HANDLE_QUADRANTS[this.vectorQuadrant];\n    },\n    endHandleClass() {\n      return END_HANDLE_QUADRANTS[this.vectorQuadrant];\n    },\n    vectorQuadrant() {\n      let { x, y, x2, y2 } = this.position;\n      if (x2 === x) {\n        return 5; // Vertical line\n      }\n\n      if (y2 === y) {\n        return 6; // Horizontal line\n      }\n\n      if (x2 > x) {\n        if (y2 < y) {\n          return 1;\n        }\n\n        return 4;\n      }\n\n      if (y2 < y) {\n        return 2;\n      }\n\n      return 3;\n    },\n    linePosition() {\n      let pos = {};\n      switch (this.vectorQuadrant) {\n        case 1:\n        case 3:\n          // slopes up\n          pos = {\n            x1: '0%',\n            y1: '100%',\n            x2: '100%',\n            y2: '0%'\n          };\n          break;\n        case 5:\n          // vertical\n          pos = {\n            x1: '0%',\n            y1: '0%',\n            x2: '0%',\n            y2: '100%'\n          };\n          break;\n        case 6:\n          // horizontal\n          pos = {\n            x1: '0%',\n            y1: '0%',\n            x2: '100%',\n            y2: '0%'\n          };\n          break;\n        default:\n          // slopes down\n          pos = {\n            x1: '0%',\n            y1: '0%',\n            x2: '100%',\n            y2: '100%'\n          };\n          break;\n      }\n\n      return pos;\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.setSelection);\n    this.context = {\n      layoutItem: this.item,\n      index: this.index\n    };\n    this.removeSelectable = this.openmct.selection.selectable(\n      this.$el,\n      this.context,\n      this.initSelect\n    );\n  },\n  unmounted() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n\n    this.openmct.selection.off('change', this.setSelection);\n  },\n  methods: {\n    startDrag(event, position) {\n      this.dragging = position;\n      document.body.addEventListener('mousemove', this.continueDrag);\n      document.body.addEventListener('mouseup', this.endDrag);\n      this.startPosition = [event.pageX, event.pageY];\n      let { x, y, x2, y2 } = this.item;\n      this.dragPosition = {\n        x,\n        y,\n        x2,\n        y2\n      };\n      if (x === x2 || y === y2) {\n        if (y > y2 || x < x2) {\n          if (this.dragging === 'start') {\n            this.dragging = 'end';\n          } else if (this.dragging === 'end') {\n            this.dragging = 'start';\n          }\n        }\n      }\n\n      event.preventDefault();\n    },\n    continueDrag(event) {\n      event.preventDefault();\n      let pxDeltaX = this.startPosition[0] - event.pageX;\n      let pxDeltaY = this.startPosition[1] - event.pageY;\n      let newPosition = this.calculateDragPosition(pxDeltaX, pxDeltaY);\n\n      if (!this.dragging) {\n        if (!_.isEqual(newPosition, this.dragPosition)) {\n          let gridDelta = [\n            event.pageX - this.startPosition[0],\n            event.pageY - this.startPosition[1]\n          ];\n          this.dragPosition = newPosition;\n          this.$emit('move', this.toGridDelta(gridDelta));\n        }\n      } else {\n        this.dragPosition = newPosition;\n      }\n    },\n    endDrag(event) {\n      document.body.removeEventListener('mousemove', this.continueDrag);\n      document.body.removeEventListener('mouseup', this.endDrag);\n      let { x, y, x2, y2 } = this.dragPosition;\n      if (!this.dragging) {\n        this.$emit('end-move');\n      } else {\n        this.$emit('end-line-resize', this.item, {\n          x,\n          y,\n          x2,\n          y2\n        });\n      }\n\n      this.dragPosition = undefined;\n      this.dragging = undefined;\n      event.preventDefault();\n    },\n    calculateDragPosition(pxDeltaX, pxDeltaY) {\n      let gridDeltaX = Math.round(pxDeltaX / this.gridSize[0]);\n      let gridDeltaY = Math.round(pxDeltaY / this.gridSize[1]);\n      let { x, y, x2, y2 } = this.item;\n      let dragPosition = {\n        x,\n        y,\n        x2,\n        y2\n      };\n\n      if (this.dragging === 'start') {\n        dragPosition.x -= gridDeltaX;\n        dragPosition.y -= gridDeltaY;\n      } else if (this.dragging === 'end') {\n        dragPosition.x2 -= gridDeltaX;\n        dragPosition.y2 -= gridDeltaY;\n      } else {\n        // dragging entire line.\n        dragPosition.x -= gridDeltaX;\n        dragPosition.y -= gridDeltaY;\n        dragPosition.x2 -= gridDeltaX;\n        dragPosition.y2 -= gridDeltaY;\n      }\n\n      return dragPosition;\n    },\n    setSelection(selection) {\n      this.selection = selection;\n    },\n    toGridDelta(pixelDelta) {\n      return pixelDelta.map((v, i) => {\n        return Math.round(v / this.gridSize[i]);\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/SubobjectView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <ObjectFrame\n        v-if=\"domainObject\"\n        ref=\"objectFrame\"\n        :domain-object=\"domainObject\"\n        :object-path=\"currentObjectPath\"\n        :has-frame=\"item.hasFrame\"\n        :show-edit-view=\"false\"\n        :layout-font-size=\"item.fontSize\"\n        :layout-font=\"item.font\"\n      />\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport ObjectFrame from '../../../ui/components/ObjectFrame.vue';\nimport LayoutFrame from './LayoutFrame.vue';\n\nconst MINIMUM_FRAME_SIZE = [320, 180];\nconst DEFAULT_DIMENSIONS = [10, 10];\nconst DEFAULT_POSITION = [1, 1];\nconst DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget', 'conditionWidget'];\n\nfunction getDefaultDimensions(gridSize) {\n  return MINIMUM_FRAME_SIZE.map((min, index) => {\n    return Math.max(Math.ceil(min / gridSize[index]), DEFAULT_DIMENSIONS[index]);\n  });\n}\n\nfunction hasFrameByDefault(type) {\n  return DEFAULT_HIDDEN_FRAME_TYPES.indexOf(type) === -1;\n}\n\nexport default {\n  makeDefinition(openmct, gridSize, domainObject, position, viewKey) {\n    let defaultDimensions = getDefaultDimensions(gridSize);\n    position = position || DEFAULT_POSITION;\n\n    return {\n      width: defaultDimensions[0],\n      height: defaultDimensions[1],\n      x: position[0],\n      y: position[1],\n      identifier: domainObject.identifier,\n      hasFrame: hasFrameByDefault(domainObject.type),\n      fontSize: 'default',\n      font: 'default',\n      viewKey\n    };\n  },\n  components: {\n    ObjectFrame,\n    LayoutFrame\n  },\n  inject: ['openmct', 'objectPath'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    initSelect: Boolean,\n    index: {\n      type: Number,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  data() {\n    return {\n      domainObject: undefined,\n      currentObjectPath: []\n    };\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    if (this.openmct.objects.supportsMutation(this.item.identifier)) {\n      this.mutablePromise = this.openmct.objects\n        .getMutable(this.item.identifier)\n        .then(this.setObject);\n    } else {\n      this.openmct.objects.get(this.item.identifier).then(this.setObject);\n    }\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n\n    if (this.mutablePromise) {\n      this.mutablePromise.then(() => {\n        this.openmct.objects.destroyMutable(this.domainObject);\n      });\n    } else if (this?.domainObject?.isMutable) {\n      this.openmct.objects.destroyMutable(this.domainObject);\n    }\n  },\n  methods: {\n    setObject(domainObject) {\n      this.domainObject = domainObject;\n      this.mutablePromise = undefined;\n      this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());\n      this.$nextTick(() => {\n        let reference = this.$refs.objectFrame;\n\n        if (reference) {\n          let childContext = this.$refs.objectFrame.getSelectionContext();\n          childContext.item = domainObject;\n          childContext.layoutItem = this.item;\n          childContext.index = this.index;\n          this.context = childContext;\n          this.removeSelectable = this.openmct.selection.selectable(\n            this.$el,\n            this.context,\n            this.immediatelySelect || this.initSelect\n          );\n          delete this.immediatelySelect;\n        }\n      });\n    },\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/TelemetryView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <div\n        v-if=\"domainObject\"\n        v-show=\"showTelemetry\"\n        ref=\"telemetryViewWrapper\"\n        class=\"c-telemetry-view u-style-receiver\"\n        :class=\"classNames\"\n        :style=\"styleObject\"\n        :data-font-size=\"item.fontSize\"\n        :data-font=\"item.font\"\n        aria-label=\"Alpha-numeric telemetry\"\n        @contextmenu.prevent=\"showContextMenu\"\n        @mouseover.ctrl=\"showToolTip\"\n        @mouseleave=\"hideToolTip\"\n      >\n        <div class=\"is-status__indicator\"></div>\n        <div v-if=\"showLabel\" class=\"c-telemetry-view__label\">\n          <div\n            class=\"c-telemetry-view__label-text\"\n            :aria-label=\"`Alpha-numeric telemetry name for ${domainObject.name}`\"\n          >\n            {{ domainObject.name }}\n          </div>\n        </div>\n\n        <div\n          v-if=\"showValue\"\n          :aria-label=\"`Alpha-numeric telemetry value of ${telemetryValue}`\"\n          :title=\"fieldName\"\n          class=\"c-telemetry-view__value\"\n          :class=\"[telemetryClass]\"\n        >\n          <div class=\"c-telemetry-view__value-text\">\n            {{ telemetryValue }}\n            <span v-if=\"unit && item.showUnits\" class=\"c-telemetry-view__value-text__unit\">\n              {{ unit }}\n            </span>\n          </div>\n        </div>\n      </div>\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport { COPY_TO_CLIPBOARD_ACTION_KEY } from '@/plugins/displayLayout/actions/CopyToClipboardAction.js';\nimport { COPY_TO_NOTEBOOK_ACTION_KEY } from '@/plugins/notebook/actions/CopyToNotebookAction.js';\nimport {\n  getDefaultNotebook,\n  getNotebookSectionAndPage\n} from '@/plugins/notebook/utils/notebook-storage.js';\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\nimport { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\nimport LayoutFrame from './LayoutFrame.vue';\n\nconst DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];\nconst DEFAULT_POSITION = [1, 1];\nconst CONTEXT_MENU_ACTIONS = [\n  COPY_TO_CLIPBOARD_ACTION_KEY,\n  COPY_TO_NOTEBOOK_ACTION_KEY,\n  VIEW_HISTORICAL_DATA_ACTION_KEY\n];\n\nexport default {\n  makeDefinition(openmct, gridSize, domainObject, position) {\n    let metadata = openmct.telemetry.getMetadata(domainObject);\n    position = position || DEFAULT_POSITION;\n\n    return {\n      identifier: domainObject.identifier,\n      x: position[0],\n      y: position[1],\n      width: DEFAULT_TELEMETRY_DIMENSIONS[0],\n      height: DEFAULT_TELEMETRY_DIMENSIONS[1],\n      displayMode: 'all',\n      value: metadata.getDefaultDisplayValue()?.key,\n      stroke: '',\n      fill: '',\n      color: '',\n      fontSize: 'default',\n      font: 'default'\n    };\n  },\n  components: {\n    LayoutFrame\n  },\n  mixins: [conditionalStylesMixin, stalenessMixin, tooltipHelpers],\n  inject: ['openmct', 'objectPath', 'currentView', 'renderWhenVisible'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    initSelect: Boolean,\n    index: {\n      type: Number,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move', 'format-changed', 'context-click'],\n  data() {\n    return {\n      currentObjectPath: undefined,\n      datum: undefined,\n      domainObject: undefined,\n      formats: undefined,\n      viewKey: `alphanumeric-format-${Math.random()}`,\n      status: '',\n      mutablePromise: undefined\n    };\n  },\n  computed: {\n    showTelemetry() {\n      return this.isEditing || !this.itemStyle?.isStyleInvisible;\n    },\n    classNames() {\n      let classes = [];\n\n      if (this.status) {\n        classes.push(`is-status--${this.status}`);\n      }\n\n      if (this.isStale) {\n        classes.push('is-stale');\n      }\n\n      return classes;\n    },\n    showLabel() {\n      let displayMode = this.item.displayMode;\n\n      return displayMode === 'all' || displayMode === 'label';\n    },\n    showValue() {\n      let displayMode = this.item.displayMode;\n\n      return displayMode === 'all' || displayMode === 'value';\n    },\n    unit() {\n      let value = this.item.value;\n      let unit = this.metadata ? this.metadata.value(value).unit : '';\n\n      return unit;\n    },\n    styleObject() {\n      let size;\n      //for legacy size support\n      if (!this.item.fontSize) {\n        size = this.item.size;\n      }\n\n      return Object.assign(\n        {},\n        {\n          size\n        },\n        this.itemStyle\n      );\n    },\n    fieldName() {\n      return this.valueMetadata && this.valueMetadata.name;\n    },\n    valueMetadata() {\n      return this.datum && this.metadata.value(this.item.value);\n    },\n    formatter() {\n      if (this.item.format) {\n        return this.customStringformatter;\n      }\n\n      return this.formats[this.item.value];\n    },\n    telemetryValue() {\n      if (!this.datum) {\n        return '---';\n      }\n\n      return this.formatter && this.formatter.format(this.datum);\n    },\n    telemetryClass() {\n      if (!this.datum) {\n        return;\n      }\n\n      let alarm =\n        this.limitEvaluator && this.limitEvaluator.evaluate(this.datum, this.valueMetadata);\n\n      return alarm && alarm.cssClass;\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.getAndSetObject();\n\n    this.status = this.openmct.status.get(this.item.identifier);\n    this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);\n\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  async beforeUnmount() {\n    this.removeStatusListener();\n\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n\n    if (this.telemetryCollection) {\n      this.telemetryCollection.off('add', this.setLatestValues);\n      this.telemetryCollection.off('clear', this.refreshData);\n\n      this.telemetryCollection.destroy();\n    }\n\n    if (this.mutablePromise) {\n      await this.mutablePromise();\n      this.openmct.objects.destroyMutable(this.domainObject);\n    } else if (this?.domainObject?.isMutable) {\n      this.openmct.objects.destroyMutable(this.domainObject);\n    }\n  },\n  methods: {\n    async getAndSetObject() {\n      let foundObject = null;\n      if (this.openmct.objects.supportsMutation(this.item.identifier)) {\n        this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier);\n        foundObject = await this.mutablePromise;\n      } else {\n        foundObject = await this.openmct.objects.get(this.item.identifier);\n      }\n      this.setObject(foundObject);\n      await this.$nextTick();\n    },\n    formattedValueForCopy() {\n      const timeFormatterKey = this.openmct.time.getTimeSystem().key;\n      const timeFormatter = this.formats[timeFormatterKey];\n      const unit = this.unit ? ` ${this.unit}` : '';\n\n      return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${\n        this.telemetryValue\n      }${unit}`;\n    },\n    setLatestValues(data) {\n      this.latestDatum = data[data.length - 1];\n      this.updateView();\n    },\n    updateView() {\n      if (!this.updatingView) {\n        this.updatingView = this.renderWhenVisible(() => {\n          this.datum = this.latestDatum;\n          this.updatingView = false;\n        });\n      }\n    },\n    refreshData(bounds, isTick) {\n      if (!isTick) {\n        this.latestDatum = undefined;\n        this.updateView();\n      }\n    },\n    setObject(domainObject) {\n      this.domainObject = domainObject;\n      this.mutablePromise = undefined;\n      this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n      this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);\n      this.formats = this.openmct.telemetry.getFormatMap(this.metadata);\n\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n\n      const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};\n      this.customStringformatter = this.openmct.telemetry.customStringFormatter(\n        valueMetadata,\n        this.item.format\n      );\n\n      this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {\n        size: 1,\n        strategy: 'latest',\n        timeContext: this.timeContext\n      });\n      this.telemetryCollection.on('add', this.setLatestValues);\n      this.telemetryCollection.on('clear', this.refreshData);\n      this.telemetryCollection.load();\n\n      this.currentObjectPath = this.objectPath.slice();\n      this.currentObjectPath.unshift(this.domainObject);\n\n      this.context = {\n        item: domainObject,\n        layoutItem: this.item,\n        index: this.index,\n        updateTelemetryFormat: this.updateTelemetryFormat,\n        toggleUnits: this.toggleUnits,\n        showUnits: this.showUnits\n      };\n      this.removeSelectable = this.openmct.selection.selectable(\n        this.$el,\n        this.context,\n        this.immediatelySelect || this.initSelect\n      );\n      delete this.immediatelySelect;\n      this.subscribeToStaleness(this.domainObject);\n    },\n    updateTelemetryFormat(format) {\n      this.customStringformatter.setFormat(format);\n\n      this.$emit('format-changed', this.item, format);\n    },\n    updateViewContext() {\n      this.$emit('context-click', {\n        viewHistoricalData: true,\n        formattedValueForCopy: this.formattedValueForCopy\n      });\n    },\n    async getContextMenuActions() {\n      const defaultNotebook = getDefaultNotebook();\n\n      let defaultNotebookName;\n      if (defaultNotebook) {\n        const domainObject = await this.openmct.objects.get(defaultNotebook.identifier);\n        const { section, page } = getNotebookSectionAndPage(\n          domainObject,\n          defaultNotebook.defaultSectionId,\n          defaultNotebook.defaultPageId\n        );\n        if (section && page) {\n          const defaultPath =\n            domainObject && `${domainObject.name} - ${section.name} - ${page.name}`;\n          defaultNotebookName = `Copy to Notebook ${defaultPath}`;\n        }\n      }\n\n      return CONTEXT_MENU_ACTIONS.map((actionKey) => {\n        const action = this.openmct.actions.getAction(actionKey);\n        if (action.key === COPY_TO_NOTEBOOK_ACTION_KEY) {\n          action.name = defaultNotebookName;\n        }\n\n        return action;\n      }).filter((action) => action.name !== undefined);\n    },\n    async showContextMenu(event) {\n      this.updateViewContext();\n      const contextMenuActions = await this.getContextMenuActions();\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        contextMenuActions,\n        this.currentObjectPath,\n        this.currentView\n      );\n      this.openmct.menus.showMenu(event.x, event.y, menuItems);\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'telemetryViewWrapper');\n    },\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/TextView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <LayoutFrame\n    :item=\"item\"\n    :grid-size=\"gridSize\"\n    :is-editing=\"isEditing\"\n    @move=\"move\"\n    @end-move=\"endMove\"\n  >\n    <template #content>\n      <div\n        class=\"c-text-view u-style-receiver js-style-receiver\"\n        :data-font-size=\"item.fontSize\"\n        :data-font=\"item.font\"\n        :class=\"[styleClass]\"\n        :style=\"style\"\n        role=\"application\"\n        aria-roledescription=\"draggable text\"\n        aria-label=\"Text\"\n      >\n        <div class=\"c-text-view__text\">{{ item.text }}</div>\n      </div>\n    </template>\n  </LayoutFrame>\n</template>\n\n<script>\nimport conditionalStylesMixin from '../mixins/objectStyles-mixin.js';\nimport LayoutFrame from './LayoutFrame.vue';\n\nexport default {\n  makeDefinition(openmct, gridSize, element) {\n    return {\n      fill: '',\n      stroke: '',\n      color: '',\n      x: 1,\n      y: 1,\n      width: 10,\n      height: 5,\n      text: element.text,\n      fontSize: 'default',\n      font: 'default'\n    };\n  },\n  components: {\n    LayoutFrame\n  },\n  mixins: [conditionalStylesMixin],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    gridSize: {\n      type: Array,\n      required: true,\n      validator: (arr) => arr && arr.length === 2 && arr.every((el) => typeof el === 'number')\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    initSelect: Boolean,\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['move', 'end-move'],\n  computed: {\n    style() {\n      let size;\n      //legacy size support\n      if (!this.item.fontSize) {\n        size = this.item.size;\n      }\n\n      return Object.assign(\n        {\n          size\n        },\n        this.itemStyle\n      );\n    }\n  },\n  watch: {\n    index(newIndex) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.index = newIndex;\n    },\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.layoutItem = newItem;\n    }\n  },\n  mounted() {\n    this.context = {\n      layoutItem: this.item,\n      index: this.index\n    };\n    this.removeSelectable = this.openmct.selection.selectable(\n      this.$el,\n      this.context,\n      this.initSelect\n    );\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n  },\n  methods: {\n    move(gridDelta) {\n      this.$emit('move', gridDelta);\n    },\n    endMove() {\n      this.$emit('end-move');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/displayLayout/components/box-and-line-views.scss",
    "content": ".c-box-view,\n.c-ellipse-view {\n  border-width: $drawingObjBorderW !important;\n  display: flex;\n  align-items: stretch;\n\n  .c-frame & {\n    @include abs();\n  }\n}\n\n.c-ellipse-view {\n  border-radius: 50%;\n}\n\n.c-line-view {\n  &.c-frame {\n    box-shadow: none !important;\n  }\n\n  .c-frame-edit {\n    border: none;\n  }\n\n  .c-handle-info {\n    background: rgba(#999, 0.2);\n    padding: 2px;\n    position: absolute;\n    top: 5px;\n    left: 5px;\n    white-space: nowrap;\n  }\n\n  svg {\n    // Prevent clipping when line is horizontal and vertical\n    min-height: 1px;\n    min-width: 1px;\n    // Must use !important to counteract setting in normalize.min.css\n    overflow: visible;\n  }\n\n  &__line {\n    stroke-linecap: round;\n    stroke-width: $drawingObjBorderW;\n  }\n\n  &__hover-indicator {\n    display: none;\n    opacity: 0.5;\n    stroke: $editFrameColorHov;\n    stroke-width: $drawingObjBorderW + 4;\n  }\n\n  .is-editing & {\n    // Needed to allow line to be moved\n    $w: 4px;\n    min-width: $w;\n    min-height: $w;\n\n    &:hover {\n      [class*='__hover-indicator'] {\n        display: inline;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/display-layout.scss",
    "content": "@mixin displayMarquee() {\n  > .c-frame-edit {\n    // All other frames\n    //@include test($c, 0.4);\n    display: block;\n  }\n  > .c-frame > .c-frame-edit {\n    // Line object frame\n    //@include test($c, 0.4);\n    display: block;\n  }\n}\n\n@mixin displayGrid() {\n  background: $editUIGridColorBg;\n\n  > [class*='__dimensions'] {\n    display: block;\n  }\n\n  > [class*='grid-holder'] {\n    display: block;\n  }\n}\n\n.l-layout {\n  @include abs();\n  display: flex;\n  flex-direction: column;\n  overflow: auto;\n\n  &__grid-holder,\n  &__dimensions {\n    display: none;\n  }\n\n  &__dimensions {\n    $b: 1px dashed $editDimensionsColor;\n    border-right: $b;\n    border-bottom: $b;\n    pointer-events: none;\n    position: absolute;\n\n    &-vals {\n      $p: 2px;\n      color: $editDimensionsColor;\n      display: inline-block;\n      font-style: italic;\n      position: absolute;\n      bottom: $p;\n      right: $p;\n      opacity: 0.7;\n    }\n  }\n\n  &__frame {\n    position: absolute;\n  }\n}\n\n.is-editing {\n  .l-shell__main-container {\n    [s-selected],\n    [s-selected-parent] {\n      // Display grid in main layout holder when editing\n      > .l-layout {\n        @include displayGrid();\n      }\n    }\n  }\n\n  .l-layout__frame {\n    &[s-selected-parent] {\n      // Display grid in nested layouts when editing\n      > * > * > * > .l-layout.allow-editing {\n        box-shadow: inset $editUIGridColorFg 0 0 2px 1px;\n      \n        @include displayGrid();\n      }\n    }\n  }\n\n  /*********************** EDIT MARQUEE CONTROL */\n  *[s-selected-parent] {\n    > .l-layout {\n      // When main shell layout is the parent\n      @include displayMarquee();\n    }\n    > * > * > * > * {\n      // When a sub-layout is the parent\n      @include displayMarquee();\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/edit-marquee.scss",
    "content": ".c-frame-edit {\n  // In Layouts, this is the editing rect and handles\n  display: none; // Set to display: block in display-layout.scss\n  pointer-events: none;\n  @include abs();\n  border: $editMarqueeBorder;\n\n  &__handle {\n    $d: 6px;\n    $o: floor($d * -0.5);\n    background: $editFrameColorHandleFg;\n    box-shadow: $editFrameColorHandleBg 0 0 0 2px;\n    pointer-events: all;\n    position: absolute;\n    width: $d;\n    height: $d;\n    top: auto;\n    right: auto;\n    bottom: auto;\n    left: auto;\n\n    &:before {\n      // Extended hit area\n      @include abs(-10px);\n      content: '';\n      display: block;\n      z-index: 0;\n    }\n\n    &:hover {\n      background: $editUIColor;\n    }\n\n    &--nwse {\n      cursor: nwse-resize;\n    }\n\n    &--nw {\n      cursor: nw-resize;\n      left: $o;\n      top: $o;\n    }\n\n    &--ne {\n      cursor: ne-resize;\n      right: $o;\n      top: $o;\n    }\n\n    &--se {\n      cursor: se-resize;\n      right: $o;\n      bottom: $o;\n    }\n\n    &--sw {\n      cursor: sw-resize;\n      left: $o;\n      bottom: $o;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/image-view.scss",
    "content": ".c-image-view {\n  background-size: cover;\n  background-repeat: no-repeat;\n  background-position: center;\n\n  .c-frame & {\n    @include abs();\n    border: 1px solid transparent;\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/layout-frame.scss",
    "content": "@use 'sass:math';\n\n/******************* FRAME */\n.c-frame {\n  display: flex;\n  flex-direction: column;\n\n  // Whatever is placed into the slot, make it fill the entirety of the space, obeying padding\n  > *:first-child {\n    flex: 1 1 auto;\n  }\n}\n\n.c-frame__move-bar {\n  display: none;\n}\n\n.is-editing {\n  /******************* STYLES FOR C-FRAME WHILE EDITING */\n  .c-frame {\n    border: 1px solid rgba($editFrameColorHov, 0.3);\n\n    &:not([s-selected]) {\n      &:hover {\n        border: $editFrameBorderHov;\n      }\n    }\n\n    &[s-selected] {\n      // All frames selected while editing\n      box-shadow: $editFrameSelectedShdw;\n\n      .c-frame__move-bar {\n        cursor: move;\n      }\n    }\n  }\n\n  /******************* DEFAULT STYLES FOR -EDIT__MOVE */\n  // All object types\n  .c-frame__move-bar {\n    @include abs();\n    display: block;\n  }\n\n  // Has-complex-content objects\n  .c-so-view.has-complex-content {\n    @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay);\n\n    > .c-so-view__local-controls {\n      @include transition($prop: transform, $dur: 250ms, $delay: $moveBarOutDelay);\n    }\n\n    + .c-frame__move-bar {\n      display: none;\n    }\n  }\n\n  .l-layout {\n    /******************* 0 - 1 ITEM SELECTED */\n    &:not(.is-multi-selected) {\n      > .l-layout__frame {\n        > .c-so-view.has-complex-content {\n          > .c-so-view__local-controls {\n            @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay);\n          }\n\n          + .c-frame__move-bar {\n            @include transition($prop: height, $delay: $moveBarOutDelay);\n            @include userSelectNone();\n            background: $editFrameMovebarColorBg;\n            box-shadow: rgba(black, 0.3) 0 2px;\n            bottom: auto;\n            display: block;\n            height: 0; // Height is set on hover below\n            opacity: 0.9;\n            max-height: 100%;\n            overflow: hidden;\n            text-align: center;\n            z-index: 10;\n\n            &:before {\n              // Grippy\n              $h: 4px;\n              $tbOffset: math.div($editFrameMovebarH - $h, 2);\n              $lrOffset: 25%;\n              @include grippy($editFrameMovebarColorFg);\n              content: '';\n              display: none;\n              position: absolute;\n              top: $tbOffset;\n              right: $lrOffset;\n              bottom: $tbOffset;\n              left: $lrOffset;\n            }\n          }\n        }\n\n        &:hover {\n          > .c-so-view.has-complex-content {\n            transition: $transInTransform;\n            transition-delay: 0s;\n\n            > .c-so-view__local-controls {\n              transform: translateY($editFrameMovebarH);\n              @include transition(height, $transOutTime);\n              transition-delay: 0s;\n            }\n\n            + .c-frame__move-bar {\n              @include transition(height);\n              height: $editFrameMovebarH;\n            }\n          }\n        }\n      }\n      > .l-layout__frame[s-selected] {\n        > .c-so-view.has-complex-content {\n          + .c-frame__move-bar:before {\n            display: block;\n          }\n        }\n      }\n    }\n\n    /******************* > 1 ITEMS SELECTED */\n    &.is-multi-selected {\n      .l-layout__frame[s-selected] {\n        > .c-so-view.has-complex-content + .c-frame__move-bar {\n          display: block;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/telemetry-view.scss",
    "content": ".c-telemetry-view {\n  display: flex;\n  align-items: stretch;\n\n  > * {\n    // Label and value holders\n    flex: 1 1 50%;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    overflow: hidden;\n    padding: $interiorMargin;\n\n    > * {\n      // Text elements\n      @include ellipsize();\n    }\n  }\n\n  &__value {\n    @include telemetryView();\n    @include isLimit();\n  }\n\n  &__label {\n    margin-right: $interiorMargin;\n  }\n\n  &.is-stale {\n    .c-telemetry-view__value {\n      @include isStaleElement();\n    }\n  }\n\n  .c-frame & {\n    @include abs();\n    border: 1px solid transparent;\n  }\n\n  .is-status__indicator {\n    position: absolute;\n    top: 0;\n    left: 0;\n  }\n\n  &[class*='is-status'] {\n    border: $borderMissing;\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/components/text-view.scss",
    "content": ".c-text-view {\n  display: flex;\n  align-items: center; // Vertically center text\n  overflow: hidden;\n  padding: $interiorMargin;\n\n  .c-frame & {\n    @include abs();\n    border: 1px solid transparent;\n  }\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/displayLayoutStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function displayLayoutStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return domainObject?.type === 'layout';\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      if (!domainObject.configuration.objectStyles) {\n        domainObject.configuration.objectStyles = {};\n      }\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/mixins/objectStyles-mixin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport StyleRuleManager from '@/plugins/condition/StyleRuleManager';\nimport { getStylesWithoutNoneValue } from '@/plugins/condition/utils/styleUtils';\n\nexport default {\n  inject: ['openmct'],\n  data() {\n    return {\n      objectStyle: undefined,\n      itemStyle: undefined,\n      styleClass: ''\n    };\n  },\n  mounted() {\n    this.parentDomainObject = this.$parent.domainObject;\n    this.itemId = this.item.id;\n    this.objectStyle = this.getObjectStyleForItem(\n      this.parentDomainObject.configuration.objectStyles\n    );\n    this.initObjectStyles();\n  },\n  beforeUnmount() {\n    if (this.stopListeningObjectStyles) {\n      this.stopListeningObjectStyles();\n    }\n\n    if (this.styleRuleManager) {\n      this.styleRuleManager.destroy();\n    }\n  },\n  methods: {\n    getObjectStyleForItem(objectStyle) {\n      if (objectStyle) {\n        return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined;\n      } else {\n        return undefined;\n      }\n    },\n    initObjectStyles() {\n      if (!this.styleRuleManager) {\n        this.styleRuleManager = new StyleRuleManager(\n          this.objectStyle,\n          this.openmct,\n          this.updateStyle.bind(this),\n          true\n        );\n      } else {\n        this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);\n      }\n\n      if (this.stopListeningObjectStyles) {\n        this.stopListeningObjectStyles();\n      }\n\n      this.stopListeningObjectStyles = this.openmct.objects.observe(\n        this.parentDomainObject,\n        'configuration.objectStyles',\n        (newObjectStyle) => {\n          //Updating object styles in the inspector view will trigger this so that the changes are reflected immediately\n          let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);\n          if (this.objectStyle !== newItemObjectStyle) {\n            this.objectStyle = newItemObjectStyle;\n            this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);\n          }\n        }\n      );\n    },\n    updateStyle(style) {\n      this.itemStyle = getStylesWithoutNoneValue(style);\n      this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/displayLayout/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport CopyToClipboardAction from './actions/CopyToClipboardAction.js';\nimport AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';\nimport DisplayLayout from './components/DisplayLayout.vue';\nimport displayLayoutStylesInterceptor from './displayLayoutStylesInterceptor.js';\nimport DisplayLayoutToolbar from './DisplayLayoutToolbar.js';\nimport DisplayLayoutType from './DisplayLayoutType.js';\nimport DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';\n\nclass DisplayLayoutView {\n  constructor(openmct, domainObject, objectPath, options) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this.options = options;\n\n    this.component = null;\n  }\n\n  show(container, isEditing, { renderWhenVisible }) {\n    const { vNode, destroy } = mount(\n      {\n        el: container,\n        components: {\n          DisplayLayout\n        },\n        provide: {\n          openmct: this.openmct,\n          objectPath: this.objectPath,\n          options: this.options,\n          currentView: this,\n          renderWhenVisible\n        },\n        data: () => {\n          return {\n            domainObject: this.domainObject,\n            isEditing\n          };\n        },\n        template:\n          '<display-layout ref=\"displayLayout\" :domain-object=\"domainObject\" :is-editing=\"isEditing\"></display-layout>'\n      },\n      {\n        app: this.openmct.app,\n        element: container\n      }\n    );\n    this._destroy = destroy;\n    this.component = vNode.componentInstance;\n  }\n\n  getViewContext() {\n    if (!this.component) {\n      return {};\n    }\n\n    return this.component.$refs.displayLayout.getViewContext();\n  }\n\n  getSelectionContext() {\n    return {\n      item: this.domainObject,\n      supportsMultiSelect: true\n    };\n  }\n\n  contextAction(action, ...rest) {\n    if (this?.component.$refs.displayLayout[action]) {\n      this.component.$refs.displayLayout[action](...rest);\n    }\n  }\n\n  onEditModeChange(isEditing) {\n    this.component.isEditing = isEditing;\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n      this.component = null;\n    }\n  }\n}\n\nexport default function DisplayLayoutPlugin(options) {\n  return function (openmct) {\n    openmct.actions.register(new CopyToClipboardAction(openmct));\n\n    openmct.objectViews.addProvider({\n      key: 'layout.view',\n      canView: function (domainObject) {\n        return domainObject.type === 'layout';\n      },\n      canEdit: function (domainObject) {\n        return domainObject.type === 'layout';\n      },\n      view: function (domainObject, objectPath) {\n        return new DisplayLayoutView(openmct, domainObject, objectPath, options);\n      }\n    });\n    openmct.objects.addGetInterceptor(displayLayoutStylesInterceptor(openmct));\n    openmct.types.addType('layout', DisplayLayoutType());\n    openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));\n    openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));\n    openmct.composition.addPolicy((parent, child) => {\n      if (parent.type === 'layout' && child.type === 'folder') {\n        return false;\n      } else {\n        return true;\n      }\n    });\n\n    for (const [type, definition] of Object.entries(DisplayLayoutDrawingObjectTypes)) {\n      openmct.types.addType(type, definition);\n    }\n\n    DisplayLayoutPlugin._installed = true;\n  };\n}\n"
  },
  {
    "path": "src/plugins/displayLayout/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport DisplayLayoutPlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let displayLayoutDefinition;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(\n      new DisplayLayoutPlugin({\n        showAsView: []\n      })\n    );\n    displayLayoutDefinition = openmct.types.get('layout');\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.start(child);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('defines a display layout object type with the correct key', () => {\n    expect(displayLayoutDefinition.definition.name).toEqual('Display Layout');\n  });\n\n  it('provides a view', () => {\n    const testViewObject = {\n      id: 'test-object',\n      type: 'layout',\n      configuration: {\n        items: [\n          {\n            identifier: {\n              namespace: '',\n              key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n            },\n            x: 8,\n            y: 3,\n            width: 10,\n            height: 5,\n            displayMode: 'all',\n            value: 'sin',\n            stroke: '',\n            fill: '',\n            color: '',\n            size: '13px',\n            type: 'telemetry-view',\n            id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e'\n          }\n        ],\n        layoutGrid: [10, 10]\n      }\n    };\n\n    const applicableViews = openmct.objectViews.get(testViewObject, []);\n    let displayLayoutViewProvider = applicableViews.find(\n      (viewProvider) => viewProvider.key === 'layout.view'\n    );\n    expect(displayLayoutViewProvider).toBeDefined();\n  });\n\n  it('renders a display layout view without errors', () => {\n    const testViewObject = {\n      identifier: {\n        namespace: 'test-namespace',\n        key: 'test-key'\n      },\n      type: 'layout',\n      configuration: {\n        items: [],\n        layoutGrid: [10, 10]\n      },\n      composition: []\n    };\n\n    const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);\n    let displayLayoutViewProvider = applicableViews.find(\n      (viewProvider) => viewProvider.key === 'layout.view'\n    );\n    let view = displayLayoutViewProvider.view(testViewObject, [testViewObject]);\n    let error;\n\n    try {\n      view.show(child, false, { renderWhenVisible });\n    } catch (e) {\n      error = e;\n    }\n\n    expect(error).toBeUndefined();\n  });\n\n  describe('on load', () => {\n    let displayLayoutItem;\n    let item;\n\n    beforeEach((done) => {\n      item = {\n        width: 32,\n        height: 18,\n        x: 78,\n        y: 8,\n        identifier: {\n          namespace: '',\n          key: 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'\n        },\n        hasFrame: true,\n        type: 'line-view', // so no telemetry functionality is triggered, just want to test the sync\n        id: 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'\n      };\n      displayLayoutItem = {\n        composition: [\n          // no item in composition, but item in configuration items\n        ],\n        configuration: {\n          items: [item],\n          layoutGrid: [10, 10]\n        },\n        name: 'Display Layout',\n        type: 'layout',\n        identifier: {\n          namespace: '',\n          key: 'c5e636c1-6771-4c9c-b933-8665cab189b3'\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(displayLayoutItem, []);\n      const displayLayoutViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'layout.view'\n      );\n      const view = displayLayoutViewProvider.view(displayLayoutItem, displayLayoutItem);\n      view.show(child, false, { renderWhenVisible });\n\n      nextTick(done);\n    });\n\n    it('will sync composition and layout items', () => {\n      expect(displayLayoutItem.configuration.items.length).toBe(0);\n    });\n  });\n\n  describe('the alpha numeric format view', () => {\n    let displayLayoutItem;\n    let telemetryItem;\n    let selection;\n\n    beforeEach(() => {\n      displayLayoutItem = {\n        composition: [],\n        configuration: {\n          items: [\n            {\n              identifier: {\n                namespace: '',\n                key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n              },\n              x: 8,\n              y: 3,\n              width: 10,\n              height: 5,\n              displayMode: 'all',\n              value: 'sin',\n              stroke: '',\n              fill: '',\n              color: '',\n              size: '13px',\n              type: 'telemetry-view',\n              id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e'\n            }\n          ],\n          layoutGrid: [10, 10]\n        },\n        name: 'Display Layout',\n        type: 'layout',\n        identifier: {\n          namespace: '',\n          key: 'c5e636c1-6771-4c9c-b933-8665cab189b3'\n        }\n      };\n      telemetryItem = {\n        telemetry: {\n          period: 5,\n          amplitude: 5,\n          offset: 5,\n          dataRateInHz: 5,\n          phase: 5,\n          randomness: 0\n        },\n        name: 'Sine Wave Generator',\n        type: 'generator',\n        modified: 1592851063871,\n        location: 'mine',\n        persisted: 1592851063871,\n        id: '55122607-e65e-44d5-9c9d-9c31a914ca89',\n        identifier: {\n          namespace: '',\n          key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n        }\n      };\n      selection = [\n        [\n          {\n            context: {\n              layoutItem: displayLayoutItem.configuration.items[0],\n              item: telemetryItem,\n              index: 1\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ]\n      ];\n    });\n\n    it('provides an alphanumeric format view', () => {\n      const displayLayoutAlphaNumFormatView = openmct.inspectorViews.get(selection);\n      expect(displayLayoutAlphaNumFormatView.length).toBeDefined();\n    });\n  });\n\n  describe('the toolbar', () => {\n    let displayLayoutItem;\n    let selection;\n\n    beforeEach(() => {\n      displayLayoutItem = {\n        composition: [],\n        configuration: {\n          items: [\n            {\n              fill: '#666666',\n              stroke: '',\n              x: 1,\n              y: 1,\n              width: 10,\n              height: 5,\n              type: 'box-view',\n              id: '89b88746-d325-487b-aec4-11b79afff9e8'\n            },\n            {\n              fill: '#666666',\n              stroke: '',\n              x: 1,\n              y: 1,\n              width: 10,\n              height: 10,\n              type: 'ellipse-view',\n              id: '19b88746-d325-487b-aec4-11b79afff9z8'\n            },\n            {\n              x: 18,\n              y: 9,\n              x2: 23,\n              y2: 4,\n              stroke: '#666666',\n              type: 'line-view',\n              id: '57d49a28-7863-43bd-9593-6570758916f0'\n            },\n            {\n              identifier: {\n                namespace: '',\n                key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n              },\n              x: 8,\n              y: 3,\n              width: 10,\n              height: 5,\n              displayMode: 'all',\n              value: 'sin',\n              stroke: '',\n              fill: '',\n              color: '',\n              size: '13px',\n              type: 'telemetry-view',\n              id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e'\n            },\n            {\n              width: 32,\n              height: 18,\n              x: 78,\n              y: 8,\n              identifier: {\n                namespace: '',\n                key: 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'\n              },\n              hasFrame: true,\n              type: 'subobject-view',\n              id: 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'\n            }\n          ],\n          layoutGrid: [10, 10]\n        },\n        name: 'Display Layout',\n        type: 'layout',\n        identifier: {\n          namespace: '',\n          key: 'c5e636c1-6771-4c9c-b933-8665cab189b3'\n        }\n      };\n      selection = [\n        [\n          {\n            context: {\n              layoutItem: displayLayoutItem.configuration.items[1],\n              index: 1\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        [\n          {\n            context: {\n              layoutItem: displayLayoutItem.configuration.items[0],\n              index: 0\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        [\n          {\n            context: {\n              layoutItem: displayLayoutItem.configuration.items[2],\n              item: displayLayoutItem.configuration.items[2],\n              index: 2\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        [\n          {\n            context: {\n              item: {\n                composition: [\n                  {\n                    namespace: '',\n                    key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n                  }\n                ],\n                configuration: {\n                  series: [\n                    {\n                      identifier: {\n                        namespace: '',\n                        key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n                      }\n                    }\n                  ],\n                  yAxis: {},\n                  xAxis: {}\n                },\n                name: 'Unnamed Overlay Plot',\n                type: 'telemetry.plot.overlay',\n                modified: 1594142141929,\n                location: 'mine',\n                identifier: {\n                  namespace: '',\n                  key: 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'\n                },\n                persisted: 1594142141929\n              },\n              layoutItem: displayLayoutItem.configuration.items[3],\n              index: 3\n            }\n          },\n          {\n            context: {\n              item: displayLayoutItem,\n              supportsMultiSelect: true\n            }\n          }\n        ]\n      ];\n    });\n\n    it('provides controls including separators', () => {\n      const displayLayoutToolbar = openmct.toolbars.get(selection);\n\n      expect(displayLayoutToolbar.length).toBe(8);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/duplicate/DuplicateAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport DuplicateTask from './DuplicateTask.js';\n\nconst DUPLICATE_ACTION_KEY = 'duplicate';\n\nclass DuplicateAction {\n  constructor(openmct) {\n    this.name = 'Duplicate';\n    this.key = DUPLICATE_ACTION_KEY;\n    this.description = 'Duplicate this object.';\n    this.cssClass = 'icon-duplicate';\n    this.group = 'action';\n    this.priority = 7;\n\n    this.openmct = openmct;\n    this.transaction = null;\n  }\n\n  invoke(objectPath) {\n    this.object = objectPath[0];\n    this.parent = objectPath[1];\n\n    this.showForm(this.object, this.parent);\n  }\n\n  inNavigationPath() {\n    return this.openmct.router.path.some((objectInPath) =>\n      this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)\n    );\n  }\n\n  async onSave(changes) {\n    this.startTransaction();\n\n    let inNavigationPath = this.inNavigationPath();\n    if (inNavigationPath && this.openmct.editor.isEditing()) {\n      this.openmct.editor.save();\n    }\n\n    let duplicationTask = new DuplicateTask(this.openmct);\n    if (changes.name && changes.name !== this.object.name) {\n      duplicationTask.changeName(changes.name);\n    }\n\n    const parentDomainObjectpath = changes.location || [this.parent];\n    const parent = parentDomainObjectpath[0];\n\n    await duplicationTask.duplicate(this.object, parent);\n\n    return this.saveTransaction();\n  }\n\n  showForm(domainObject, parentDomainObject) {\n    const formStructure = {\n      title: 'Duplicate Item',\n      sections: [\n        {\n          rows: [\n            {\n              key: 'name',\n              control: 'textfield',\n              name: 'Title',\n              pattern: '\\\\S+',\n              required: true,\n              cssClass: 'l-input-lg',\n              value: domainObject.name\n            },\n            {\n              name: 'Location',\n              cssClass: 'grows',\n              control: 'locator',\n              required: true,\n              parent: parentDomainObject,\n              validate: this.validate(parentDomainObject),\n              key: 'location'\n            }\n          ]\n        }\n      ]\n    };\n\n    this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this));\n  }\n\n  validate(currentParent) {\n    return (data) => {\n      const parentCandidate = data.value[0];\n\n      let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);\n      let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);\n      let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);\n      const isLocked = parentCandidate.locked === true;\n\n      if (isLocked || !this.openmct.objects.isPersistable(parentCandidate.identifier)) {\n        return false;\n      }\n\n      if (!parentCandidateKeystring || !currentParentKeystring) {\n        return false;\n      }\n\n      if (parentCandidateKeystring === objectKeystring) {\n        return false;\n      }\n\n      const parentCandidateComposition = parentCandidate.composition;\n      if (\n        parentCandidateComposition &&\n        parentCandidateComposition.indexOf(objectKeystring) !== -1\n      ) {\n        return false;\n      }\n\n      return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);\n    };\n  }\n\n  appliesTo(objectPath) {\n    const parent = objectPath[1];\n    const parentType = parent && this.openmct.types.get(parent.type);\n    const child = objectPath[0];\n    const childType = child && this.openmct.types.get(child.type);\n    const isPersistable = this.openmct.objects.isPersistable(child.identifier);\n\n    if (!isPersistable) {\n      return false;\n    }\n\n    return (\n      childType &&\n      childType.definition.creatable &&\n      parentType &&\n      parentType.definition.creatable &&\n      Array.isArray(parent.composition)\n    );\n  }\n\n  startTransaction() {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.transaction = this.openmct.objects.startTransaction();\n    }\n  }\n\n  async saveTransaction() {\n    if (!this.transaction) {\n      return;\n    }\n\n    await this.transaction.commit();\n    this.openmct.objects.endTransaction();\n    this.transaction = null;\n  }\n}\n\nexport { DUPLICATE_ACTION_KEY };\n\nexport default DuplicateAction;\n"
  },
  {
    "path": "src/plugins/duplicate/DuplicateTask.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { v4 as uuid } from 'uuid';\n\n/**\n * This class encapsulates the process of  duplicating/copying a domain object\n * and all of its children.\n *\n * @param {DomainObject} domainObject The object to duplicate\n * @param {DomainObject} parent The new location of the cloned object tree\n * @param {src/plugins/duplicate.DuplicateService~filter} filter\n *        a function used to filter out objects from\n *        the cloning process\n * @constructor\n */\nexport default class DuplicateTask {\n  constructor(openmct) {\n    this.domainObject = undefined;\n    this.parent = undefined;\n    this.firstClone = undefined;\n    this.filter = undefined;\n    this.persisted = 0;\n    this.clones = [];\n    this.idMap = {};\n    this.name = undefined;\n\n    this.openmct = openmct;\n  }\n\n  changeName(name) {\n    this.name = name;\n  }\n\n  /**\n   * Execute the duplicate/copy task with the objects provided.\n   * @returns {promise} Which will resolve with a clone of the object\n   * once complete.\n   */\n  async duplicate(domainObject, parent, filter) {\n    this.domainObject = domainObject;\n    this.parent = parent;\n    this.namespace = parent.identifier.namespace;\n    this.filter = filter || this.isCreatable;\n\n    await this.buildDuplicationPlan();\n    await this.persistObjects();\n    await this.addClonesToParent();\n\n    return this.firstClone;\n  }\n\n  /**\n   * Will build a graph of an object and all of its child objects in\n   * memory\n   * @private\n   * @param domainObject The original object to be copied\n   * @param parent The parent of the original object to be copied\n   * @returns {Promise} resolved with an array of clones of the models\n   * of the object tree being copied. Duplicating is done in a bottom-up\n   * fashion, so that the last member in the array is a clone of the model\n   * object being copied. The clones are all full composed with\n   * references to their own children.\n   */\n  async buildDuplicationPlan() {\n    let domainObjectClone = await this.duplicateObject(this.domainObject);\n    if (domainObjectClone !== this.domainObject) {\n      domainObjectClone.location = this.getKeyString(this.parent);\n    }\n\n    if (this.name) {\n      domainObjectClone.name = this.name;\n    }\n\n    this.firstClone = domainObjectClone;\n\n    return;\n  }\n\n  /**\n   * Will persist a list of {@link objectClones}. It will persist all\n   * simultaneously, irrespective of order in the list. This may\n   * result in automatic request batching by the browser.\n   */\n  async persistObjects() {\n    let initialCount = this.clones.length;\n    let dialog = this.openmct.overlays.progressDialog({\n      progressPerc: 0,\n      message: `Duplicating ${initialCount} objects.`,\n      iconClass: 'info',\n      title: 'Duplicating'\n    });\n\n    let clonesDone = Promise.all(\n      this.clones.map((clone) => {\n        let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));\n        let message = `Duplicating ${initialCount - this.persisted} objects.`;\n\n        dialog.updateProgress(percentPersisted, message);\n\n        return this.openmct.objects.save(clone);\n      })\n    );\n\n    await clonesDone;\n\n    dialog.dismiss();\n    this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);\n\n    return;\n  }\n\n  /**\n   * Will add a list of clones to the specified parent's composition\n   */\n  async addClonesToParent() {\n    let parentComposition = this.openmct.composition.get(this.parent);\n    await parentComposition.load();\n    parentComposition.add(this.firstClone);\n\n    return;\n  }\n\n  /**\n   * A recursive function that will perform a bottom-up duplicate of\n   * the object tree with originalObject at the root. Recurses to\n   * the farthest leaf, then works its way back up again,\n   * cloning objects, and composing them with their child clones\n   * as it goes\n   * @private\n   * @returns {DomainObject} If the type of the original object allows for\n   * duplication, then a duplicate of the object, otherwise the object\n   * itself (to allow linking to non duplicatable objects).\n   */\n  async duplicateObject(originalObject) {\n    // Check if the creatable (or other passed in filter).\n    if (this.filter(originalObject)) {\n      let clone = this.cloneObjectModel(originalObject);\n      let composeesCollection = this.openmct.composition.get(originalObject);\n      let composees;\n\n      if (composeesCollection) {\n        composees = await composeesCollection.load();\n      }\n\n      return this.duplicateComposees(clone, composees);\n    }\n\n    // Not creatable, creating a link, no need to iterate children\n    return originalObject;\n  }\n\n  /**\n   * Given an array of objects composed by a parent, clone them, then\n   * add them to the parent.\n   * @private\n   * @returns {*}\n   */\n  async duplicateComposees(clonedParent, composees = []) {\n    let idMappings = [];\n    let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {\n      await previousPromise;\n\n      let clonedComposee = await this.duplicateObject(nextComposee);\n\n      if (clonedComposee) {\n        idMappings.push({\n          newId: clonedComposee.identifier,\n          oldId: nextComposee.identifier\n        });\n        this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);\n      }\n\n      return;\n    }, Promise.resolve());\n\n    await allComposeesDuplicated;\n\n    clonedParent = this.rewriteIdentifiers(clonedParent, idMappings);\n    this.clones.push(clonedParent);\n\n    return clonedParent;\n  }\n\n  /**\n   * Update identifiers in a cloned object model (or part of\n   * a cloned object model) to reflect new identifiers after\n   * duplicating.\n   * @private\n   */\n  rewriteIdentifiers(clonedParent, childIdMappings) {\n    for (let { newId, oldId } of childIdMappings) {\n      let newIdKeyString = this.openmct.objects.makeKeyString(newId);\n      let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);\n\n      // regex replace keystrings\n      clonedParent = JSON.stringify(clonedParent).replace(\n        new RegExp(oldIdKeyString, 'g'),\n        newIdKeyString\n      );\n\n      // parse reviver to replace identifiers\n      clonedParent = JSON.parse(clonedParent, (key, value) => {\n        if (\n          value !== null &&\n          value !== undefined &&\n          Object.prototype.hasOwnProperty.call(value, 'key') &&\n          Object.prototype.hasOwnProperty.call(value, 'namespace') &&\n          value.key === oldId.key &&\n          value.namespace === oldId.namespace\n        ) {\n          return newId;\n        } else {\n          return value;\n        }\n      });\n    }\n\n    return clonedParent;\n  }\n\n  composeChild(child, parent, setLocation) {\n    parent.composition.push(child.identifier);\n\n    //If a location is not specified, set it.\n    if (setLocation && child.location === undefined) {\n      let parentKeyString = this.getKeyString(parent);\n      child.location = parentKeyString;\n    }\n  }\n\n  getTypeDefinition(domainObject, definition) {\n    let typeDefinitions = this.openmct.types.get(domainObject.type).definition;\n\n    return typeDefinitions[definition] || false;\n  }\n\n  cloneObjectModel(domainObject) {\n    let clone = JSON.parse(JSON.stringify(domainObject));\n    let identifier = {\n      key: uuid(),\n      namespace: this.namespace // set to NEW parent's namespace\n    };\n\n    if (clone.modified || clone.persisted || clone.location) {\n      clone.modified = undefined;\n      clone.persisted = undefined;\n      clone.location = undefined;\n      delete clone.modified;\n      delete clone.persisted;\n      delete clone.location;\n    }\n\n    if (clone.composition) {\n      clone.composition = [];\n    }\n\n    clone.identifier = identifier;\n\n    return clone;\n  }\n\n  getKeyString(domainObject) {\n    return this.openmct.objects.makeKeyString(domainObject.identifier);\n  }\n\n  isCreatable(domainObject) {\n    return this.getTypeDefinition(domainObject, 'creatable');\n  }\n}\n"
  },
  {
    "path": "src/plugins/duplicate/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport DuplicateAction from './DuplicateAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new DuplicateAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/duplicate/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, getMockObjects, resetApplicationState } from 'utils/testing';\n\nimport DuplicateTask from './DuplicateTask.js';\nimport DuplicateActionPlugin from './plugin.js';\n\ndescribe('The Duplicate Action plugin', () => {\n  let openmct;\n  let duplicateTask;\n  let childObject;\n  let parentObject;\n  let anotherParentObject;\n\n  // this setups up the app\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    childObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Child Folder',\n          identifier: {\n            namespace: '',\n            key: 'child-folder-object'\n          }\n        }\n      }\n    }).folder;\n\n    parentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Parent Folder',\n          type: 'folder',\n          composition: [childObject.identifier]\n        }\n      }\n    }).folder;\n\n    anotherParentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Another Parent Folder'\n        }\n      }\n    }).folder;\n\n    let objectGet = openmct.objects.get.bind(openmct.objects);\n    spyOn(openmct.objects, 'get').and.callFake((identifier) => {\n      let obj = [childObject, parentObject, anotherParentObject].find(\n        (ob) => ob.identifier.key === identifier.key\n      );\n\n      if (!obj) {\n        // not one of the mocked objs, callthrough basically\n        return objectGet(identifier);\n      }\n\n      return Promise.resolve(obj);\n    });\n\n    spyOn(openmct.composition, 'get').and.callFake((domainObject) => {\n      return {\n        load: async () => {\n          let obj = [childObject, parentObject, anotherParentObject].find(\n            (ob) => ob.identifier.key === domainObject.identifier.key\n          );\n          let children = [];\n\n          if (obj) {\n            for (let i = 0; i < obj.composition.length; i++) {\n              children.push(await openmct.objects.get(obj.composition[i]));\n            }\n          }\n\n          return Promise.resolve(children);\n        },\n        add: (child) => {\n          domainObject.composition.push(child.identifier);\n        }\n      };\n    });\n\n    // already installed by default, but never hurts, just adds to context menu\n    openmct.install(DuplicateActionPlugin());\n    openmct.types.addType('folder', { creatable: true });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should be defined', () => {\n    expect(DuplicateActionPlugin).toBeDefined();\n  });\n\n  describe('when moving an object to a new parent', () => {\n    beforeEach(async () => {\n      duplicateTask = new DuplicateTask(openmct);\n      await duplicateTask.duplicate(parentObject, anotherParentObject);\n    });\n\n    it(\"the duplicate child object's name (when not changing) should be the same as the original object\", async () => {\n      let duplicatedObjectIdentifier = anotherParentObject.composition[0];\n      let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier);\n      let duplicateObjectName = duplicatedObject.name;\n\n      expect(duplicateObjectName).toEqual(parentObject.name);\n    });\n\n    it(\"the duplicate child object's identifier should be new\", () => {\n      let duplicatedObjectIdentifier = anotherParentObject.composition[0];\n\n      expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key);\n    });\n  });\n\n  describe('when a new name is provided for the duplicated object', () => {\n    it('the name is updated', async () => {\n      const NEW_NAME = 'New Name';\n\n      duplicateTask = new DuplicateTask(openmct);\n      duplicateTask.changeName(NEW_NAME);\n      const child = await duplicateTask.duplicate(childObject, anotherParentObject);\n\n      expect(child.name).toEqual(NEW_NAME);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/events/EventInspectorViewProvider.js",
    "content": "import mount from 'utils/mount';\n\nimport EventInspectorView from './components/EventInspectorView.vue';\n\nexport default function EventInspectorViewProvider(openmct) {\n  const TIMELINE_VIEW = 'time-strip.event.inspector';\n  return {\n    key: TIMELINE_VIEW,\n    name: 'Event',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      const selectionType = selection[0][0].context?.type;\n      const event = selection[0][0].context?.event;\n      return selectionType === 'time-strip-event-selection' && event;\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                EventInspectorView\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item,\n                event: selection[0][0].context.event\n              },\n              template: '<event-inspector-view></event-inspector-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.priority.HIGHEST;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/events/EventTimelineViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport EventTimelineView from './components/EventTimelineView.vue';\n\nexport default function EventTimestripViewProvider(openmct, extendedLinesBus) {\n  const type = 'event.time-line.view';\n\n  function hasEventTelemetry(domainObject) {\n    const metadata = openmct.telemetry.getMetadata(domainObject);\n    if (!metadata) {\n      return false;\n    }\n    const hasDomain = metadata.valuesForHints(['domain']).length > 0;\n    const hasNoRange = !metadata.valuesForHints(['range'])?.length;\n\n    // for the moment, let's also exclude telemetry with images\n    const hasNoImages = !metadata.valuesForHints(['image']).length;\n\n    return hasDomain && hasNoRange && hasNoImages;\n  }\n\n  return {\n    key: type,\n    name: 'Event Timeline View',\n    cssClass: 'icon-event',\n    priority: function () {\n      // We want this to be higher priority than the TelemetryTableView\n      return openmct.priority.HIGH;\n    },\n    canView: function (domainObject, objectPath) {\n      const isChildOfTimeStrip = objectPath.some((object) => object.type === 'time-strip');\n\n      return (\n        hasEventTelemetry(domainObject) &&\n        isChildOfTimeStrip &&\n        !openmct.router.isNavigatedObject(objectPath)\n      );\n    },\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show: function (element) {\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                EventTimelineView\n              },\n              provide: {\n                openmct: openmct,\n                domainObject: domainObject,\n                objectPath: objectPath,\n                extendedLinesBus\n              },\n              template: '<event-timeline-view ref=\"root\"></event-timeline-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        },\n\n        getComponent() {\n          return component;\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/events/components/EventInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspect-properties\">\n    <ul class=\"c-inspect-properties__section\">\n      <div class=\"c-inspect-properties_header\" title=\"'Details'\">Details</div>\n      <li\n        v-for=\"[key, value] in Object.entries(event)\"\n        :key=\"key\"\n        class=\"c-inspect-properties__row\"\n      >\n        <span class=\"c-inspect-properties__label\">{{ key }}</span>\n        <span class=\"c-inspect-properties__value\">{{ value }}</span>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'domainObject', 'event']\n};\n</script>\n"
  },
  {
    "path": "src/plugins/events/components/EventTimelineView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"events\" class=\"c-events-tsv js-events-tsv\" :style=\"alignmentStyle\">\n    <SwimLane v-if=\"eventItems.length\" :is-nested=\"true\" :hide-label=\"true\">\n      <template #object>\n        <div ref=\"eventsContainer\" class=\"c-events-tsv__container\">\n          <div\n            v-for=\"event in eventItems\"\n            :id=\"`wrapper-${event.time}`\"\n            :ref=\"`wrapper-${event.time}`\"\n            :key=\"event.id\"\n            :aria-label=\"titleKey ? `${event[titleKey]}` : ''\"\n            class=\"c-events-tsv__event-line\"\n            :class=\"event.limitClass || ''\"\n            :style=\"`left: ${event.left}px`\"\n            @mouseover=\"showToolTip(event)\"\n            @mouseleave=\"dismissToolTip()\"\n            @click.stop=\"createSelectionForInspector(event)\"\n          ></div>\n        </div>\n      </template>\n    </SwimLane>\n    <div v-else class=\"c-timeline__no-items\">No events within timeframe</div>\n  </div>\n</template>\n\n<script>\nimport { scaleLinear, scaleUtc } from 'd3-scale';\nimport _ from 'lodash';\nimport { inject } from 'vue';\n\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins';\nimport { useAlignment } from '../../../ui/composables/alignmentContext.js';\nimport eventData from '../mixins/eventData.js';\n\nconst PADDING = 1;\nconst AXES_PADDING = 20;\n\nexport default {\n  components: { SwimLane },\n  mixins: [eventData, tooltipHelpers],\n  inject: ['openmct', 'domainObject', 'objectPath', 'extendedLinesBus'],\n  setup() {\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);\n    return { alignmentData };\n  },\n  data() {\n    return {\n      eventItems: [],\n      eventHistory: [],\n      titleKey: null\n    };\n  },\n  computed: {\n    alignmentStyle() {\n      let leftMargin = 0;\n      let rightMargin = 0;\n      if (this.alignmentData.leftWidth) {\n        const leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;\n        leftMargin = `${this.alignmentData.leftWidth + leftOffset}px`;\n      }\n\n      if (this.alignmentData.rightWidth) {\n        rightMargin = `${this.alignmentData.rightWidth + AXES_PADDING}px`;\n      }\n\n      return {\n        margin: `0 ${rightMargin} 0 ${leftMargin}`\n      };\n    }\n  },\n  watch: {\n    eventHistory: {\n      handler() {\n        this.updateEventItems();\n      },\n      deep: true\n    },\n    alignmentData: {\n      handler() {\n        this.setScaleAndPlotEvents(this.timeSystem);\n      },\n      deep: true\n    }\n  },\n  created() {\n    this.valueMetadata = {};\n    this.height = 0;\n    this.timeSystem = this.openmct.time.getTimeSystem();\n    this.extendLines = false;\n  },\n  mounted() {\n    this.setDimensions();\n    this.setTimeContext();\n\n    this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    const metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n    if (metadata) {\n      this.valueMetadata =\n        metadata.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(metadata);\n    }\n    // title is in the metadata, and is either a \"hint\" with a \"label\", or failing that, the first string type we find\n    this.titleKey =\n      metadata.valuesForHints(['label'])?.[0]?.key ||\n      metadata.values().find((metadatum) => metadatum.format === 'string')?.key;\n\n    this.updateViewBounds();\n\n    this.resize = _.debounce(this.resize, 400);\n    this.eventStripResizeObserver = new ResizeObserver(this.resize);\n    this.eventStripResizeObserver.observe(this.$refs.events);\n    this.extendedLinesBus.addEventListener('disable-extended-lines', this.disableExtendEventLines);\n    this.extendedLinesBus.addEventListener('enable-extended-lines', this.enableExtendEventLines);\n  },\n  beforeUnmount() {\n    if (this.eventStripResizeObserver) {\n      this.eventStripResizeObserver.disconnect();\n    }\n\n    this.stopFollowingTimeContext();\n    if (this.unlisten) {\n      this.unlisten();\n    }\n    if (this.destroyEventContainer) {\n      this.destroyEventContainer();\n    }\n\n    this.extendedLinesBus.removeEventListener(\n      'disable-extended-lines',\n      this.disableExtendEventLines\n    );\n    this.extendedLinesBus.removeEventListener('enable-extended-lines', this.enableExtendEventLines);\n\n    if (this.unsubscribeSelection) {\n      this.unsubscribeSelection();\n    }\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);\n      this.timeContext.on('boundsChanged', this.updateViewBounds);\n    },\n    enableExtendEventLines(event) {\n      const keyStringToEnable = event.detail;\n      if (this.keyString === keyStringToEnable) {\n        this.extendLines = true;\n        this.emitExtendedLines();\n      }\n    },\n    disableExtendEventLines(event) {\n      const keyStringToDisable = event.detail;\n      if (this.keyString === keyStringToDisable) {\n        this.extendLines = false;\n        this.emitExtendedLines();\n      }\n    },\n    firstNonDomainAttribute(metadata) {\n      return metadata\n        .values()\n        .find((metadatum) => !metadatum.hints.domain && metadatum.key !== 'name');\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('timeSystem', this.setScaleAndPlotEvents);\n        this.timeContext.off('boundsChanged', this.updateViewBounds);\n      }\n    },\n    resize() {\n      const clientWidth = this.getClientWidth();\n      if (clientWidth !== this.width) {\n        this.setDimensions();\n        this.setScaleAndPlotEvents(this.timeSystem);\n      }\n    },\n    getClientWidth() {\n      // Try to use the component’s own element first\n      let clientWidth = this.$refs.events?.clientWidth;\n      if (!clientWidth) {\n        // Fallback: use the actual container element (the immediate parent)\n        const parent = this.$el.parentElement;\n        if (parent) {\n          clientWidth = parent.getBoundingClientRect().width;\n        }\n      }\n      return clientWidth;\n    },\n    updateViewBounds(bounds, isTick) {\n      this.viewBounds = this.timeContext.getBounds();\n\n      if (!this.timeSystem) {\n        this.timeSystem = this.timeContext.getTimeSystem();\n      }\n\n      this.setScaleAndPlotEvents(this.timeSystem, !isTick);\n    },\n    setScaleAndPlotEvents(timeSystem) {\n      if (timeSystem) {\n        this.timeSystem = timeSystem;\n        this.timeFormatter = this.getFormatter(this.timeSystem.key);\n      }\n\n      this.setScale(this.timeSystem);\n      this.updateEventItems();\n    },\n    getFormatter(key) {\n      const metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n\n      const metadataValue = metadata.value(key) || { format: key };\n      const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n      return valueFormatter;\n    },\n    updateEventItems() {\n      if (this.xScale) {\n        this.eventItems = this.eventHistory.map((eventHistoryItem) => {\n          const limitClass = this.getLimitClass(eventHistoryItem);\n          return {\n            ...eventHistoryItem,\n            left: this.xScale(eventHistoryItem.time),\n            limitClass\n          };\n        });\n        if (this.extendLines) {\n          this.emitExtendedLines();\n        }\n      }\n    },\n    setDimensions() {\n      const eventsHolder = this.$refs.events;\n      this.width = this.getClientWidth();\n\n      this.height = Math.round(eventsHolder.getBoundingClientRect().height);\n    },\n    setScale(timeSystem) {\n      if (!this.width) {\n        return;\n      }\n\n      if (!timeSystem) {\n        timeSystem = this.timeContext.getTimeSystem();\n      }\n\n      if (timeSystem.isUTCBased) {\n        this.xScale = scaleUtc();\n        this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);\n      } else {\n        this.xScale = scaleLinear();\n        this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);\n      }\n\n      this.xScale.range([PADDING, this.width - PADDING * 2]);\n    },\n    createPathSelection(eventWrapper) {\n      const selection = [];\n      selection.unshift({\n        element: eventWrapper,\n        context: {\n          item: this.domainObject\n        }\n      });\n      this.objectPath.forEach((pathObject) => {\n        selection.push({\n          element: this.openmct.layout.$refs.browseObject.$el,\n          context: {\n            item: pathObject\n          }\n        });\n      });\n\n      return selection;\n    },\n    setSelection() {\n      let childContext = {};\n      childContext.item = this.childObject;\n      this.context = childContext;\n      if (this.removeSelectable) {\n        this.removeSelectable();\n      }\n\n      this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);\n    },\n    createSelectionForInspector(event) {\n      const eventWrapper = this.$refs[`wrapper-${event.time}`][0];\n      const eventContext = {\n        type: 'time-strip-event-selection',\n        event,\n        item: this.domainObject\n      };\n\n      this.unsubscribeSelection = this.openmct.selection.selectable(\n        eventWrapper,\n        eventContext,\n        true\n      );\n    },\n    getLimitClass(event) {\n      const limitEvaluation = this.limitEvaluator.evaluate(event, this.valueMetadata);\n      return limitEvaluation?.cssClass;\n    },\n    showToolTip(event) {\n      const aClasses = ['c-events-tooltip'];\n      if (event.limitClass) {\n        aClasses.push(event.limitClass);\n      }\n      const showToLeft = false; // Temp, stubbed in\n      if (showToLeft) {\n        aClasses.push('--left');\n      }\n\n      this.buildToolTip(\n        this.titleKey ? `${event[this.titleKey]}` : '',\n        this.openmct.tooltips.TOOLTIP_LOCATIONS.RIGHT,\n        `wrapper-${event.time}`,\n        [aClasses.join(' ')]\n      );\n      this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, event.time);\n    },\n    dismissToolTip() {\n      this.hideToolTip();\n      this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, null);\n    },\n    emitExtendedLines() {\n      let lines = [];\n      if (this.extendLines) {\n        lines = this.eventItems.map((e) => ({\n          x: e.left,\n          limitClass: e.limitClass,\n          id: e.time\n        }));\n      }\n      this.extendedLinesBus.updateExtendedLines(this.keyString, lines);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/events/components/events-view.scss",
    "content": "@mixin styleEventLine($colorConst) {\n    background-color: $colorConst !important;\n    transition: box-shadow 250ms ease-out;\n    &:hover,\n    &[s-selected] {\n        box-shadow: rgba($colorConst, 0.5) 0 0 0px 4px;\n        transition: none;\n        z-index: 2;\n    }\n}\n@mixin styleEventLineExtended($colorConst) {\n    background-color: $colorConst !important;\n}\n\n.c-events-tsv {\n    $m: $interiorMargin;\n    overflow: hidden;\n    @include abs();\n\n    &__container {\n        // Holds event lines\n        background-color: $colorPlotBg;\n        position: absolute;\n        top: $m; right: 0; bottom: $m; left: 0;\n    }\n\n    &__event-line {\n        // Wraps an individual event line\n        // Also holds the hover flyout element\n        $c: $colorEventLine;\n        $lineW: $eventLineW;\n        $hitAreaW: 7px;\n        $m: $interiorMarginSm;\n        cursor: pointer;\n        position: absolute;\n        display: flex;\n        top: $m; bottom: $m;\n        width: $lineW;\n        z-index: 1;\n\n        @include styleEventLine($colorEventLine);\n        &.is-event {\n            &--purple {\n                @include styleEventLine($colorEventPurpleLine);\n            }\n            &--red {\n                @include styleEventLine($colorEventRedLine);\n            }\n            &--orange {\n                @include styleEventLine($colorEventOrangeLine);\n            }\n            &--yellow {\n                @include styleEventLine($colorEventYellowLine);\n            }\n        }\n\n        &:before {\n            // Extend hit area\n            content: '';\n            display: block;\n            position: absolute;\n            top: 0; bottom: 0;\n            z-index: 0;\n            width: $hitAreaW;\n            transform: translateX(($hitAreaW - $lineW) * -0.5);\n        }\n    }\n}\n\n.c-events-canvas {\n    pointer-events: auto;\n    position: absolute;\n    left: 0;\n    top: 0;\n    z-index: 2;\n}\n\n// Extended event lines\n.c-timeline__overlay-lines__extended-line-container {\n    display: contents;\n}\n\n.c-timeline__event-line--extended {\n    @include abs();\n    width: $eventLineW;\n\n    &.--hilite {\n        opacity: 0.8;\n        transition: none;\n    }\n\n    @include styleEventLineExtended($colorEventLine);\n    &.is-event {\n        &--purple {\n            @include styleEventLineExtended($colorEventPurpleLine);\n        }\n        &--red {\n            @include styleEventLineExtended($colorEventRedLine);\n        }\n        &--orange {\n            @include styleEventLineExtended($colorEventOrangeLine);\n        }\n        &--yellow {\n            @include styleEventLineExtended($colorEventYellowLine);\n        }\n    }\n}\n\n.c-events-tooltip {\n    // Default to right of event line\n    border-radius: 0 !important;\n    //transform: translate(0, $interiorMargin);\n}\n"
  },
  {
    "path": "src/plugins/events/mixins/eventData.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst DEFAULT_DURATION_FORMATTER = 'duration';\nimport { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';\n\nexport default {\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  mounted() {\n    // listen\n    this.boundsChanged = this.boundsChanged.bind(this);\n    this.timeSystemChanged = this.timeSystemChanged.bind(this);\n    this.setDataTimeContext = this.setDataTimeContext.bind(this);\n    this.openmct.objectViews.on('clearData', this.dataCleared);\n\n    // Get metadata and formatters\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n\n    this.durationFormatter = this.getFormatter(\n      this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER\n    );\n\n    // initialize\n    this.timeKey = this.timeSystem.key;\n    this.timeFormatter = this.getFormatter(this.timeKey);\n    this.setDataTimeContext();\n    this.loadTelemetry();\n  },\n  beforeUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n      delete this.unsubscribe;\n    }\n\n    this.stopFollowingDataTimeContext();\n    this.openmct.objectViews.off('clearData', this.dataCleared);\n\n    this.telemetryCollection.off('add', this.dataAdded);\n    this.telemetryCollection.off('remove', this.dataRemoved);\n    this.telemetryCollection.off('clear', this.dataCleared);\n\n    this.telemetryCollection.destroy();\n  },\n  methods: {\n    dataAdded(addedItems, addedItemIndices) {\n      const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum));\n      let newEventHistory = this.eventHistory.slice();\n      normalizedDataToAdd.forEach((datum, index) => {\n        newEventHistory.splice(addedItemIndices[index] ?? -1, 0, datum);\n      });\n      //Assign just once so eventHistory watchers don't get called too often\n      this.eventHistory = newEventHistory;\n    },\n    dataCleared() {\n      this.eventHistory = [];\n    },\n    dataRemoved(removed) {\n      const removedTimestamps = {};\n      removed.forEach((_removed) => {\n        const removedTimestamp = this.parseTime(_removed);\n        removedTimestamps[removedTimestamp] = true;\n      });\n\n      this.eventHistory = this.eventHistory.filter((event) => {\n        const eventTimestamp = this.parseTime(event);\n\n        return !removedTimestamps[eventTimestamp];\n      });\n    },\n    setDataTimeContext() {\n      this.stopFollowingDataTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);\n    },\n    stopFollowingDataTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);\n        this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);\n      }\n    },\n    formatEventUrl(datum) {\n      if (!datum) {\n        return;\n      }\n\n      return this.eventFormatter.format(datum);\n    },\n    formatEventThumbnailUrl(datum) {\n      if (!datum || !this.eventThumbnailFormatter) {\n        return;\n      }\n\n      return this.eventThumbnailFormatter.format(datum);\n    },\n    formatTime(datum) {\n      if (!datum) {\n        return;\n      }\n\n      const dateTimeStr = this.timeFormatter.format(datum);\n\n      // Replace ISO \"T\" with a space to allow wrapping\n      return dateTimeStr.replace('T', ' ');\n    },\n    getEventDownloadName(datum) {\n      let eventDownloadName = '';\n      if (datum) {\n        const key = this.eventDownloadNameMetadataValue.key;\n        eventDownloadName = datum[key];\n      }\n\n      return eventDownloadName;\n    },\n    parseTime(datum) {\n      if (!datum) {\n        return;\n      }\n\n      return this.timeFormatter.parse(datum);\n    },\n    loadTelemetry() {\n      this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {\n        timeContext: this.timeContext\n      });\n      this.telemetryCollection.on('add', this.dataAdded);\n      this.telemetryCollection.on('remove', this.dataRemoved);\n      this.telemetryCollection.on('clear', this.dataCleared);\n      this.telemetryCollection.load();\n    },\n    boundsChanged(bounds, isTick) {\n      if (isTick) {\n        return;\n      }\n\n      this.bounds = bounds;\n    },\n    timeSystemChanged() {\n      this.timeSystem = this.timeContext.getTimeSystem();\n      this.timeKey = this.timeSystem.key;\n      this.timeFormatter = this.getFormatter(this.timeKey);\n      this.durationFormatter = this.getFormatter(\n        this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER\n      );\n    },\n    normalizeDatum(datum) {\n      const formattedTime = this.formatTime(datum);\n      const time = this.parseTime(formattedTime);\n\n      return {\n        ...datum,\n        formattedTime,\n        time\n      };\n    },\n    getFormatter(key) {\n      const metadataValue = this.metadata.value(key) || { format: key };\n      const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n      return valueFormatter;\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/events/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport EventInspectorViewProvider from './EventInspectorViewProvider.js';\nimport EventTimelineViewProvider from './EventTimelineViewProvider.js';\n\nexport default function plugin(extendedLinesBus) {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct, extendedLinesBus));\n    openmct.inspectorViews.addProvider(new EventInspectorViewProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/exportAsJSONAction/ExportAsJSONAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { v4 as uuid } from 'uuid';\n\nimport JSONExporter from '/src/exporters/JSONExporter.js';\nconst EXPORT_AS_JSON_ACTION_KEY = 'export.JSON';\n\nclass ExportAsJSONAction {\n  #openmct;\n\n  /**\n   * @param {import('../../../openmct').OpenMCT} openmct The Open MCT API\n   */\n  constructor(openmct) {\n    this.#openmct = openmct;\n\n    // Bind public methods\n    this.invoke = this.invoke.bind(this);\n    this.appliesTo = this.appliesTo.bind(this);\n    // FIXME: This should be private but is used in tests\n    this.saveAs = this.saveAs.bind(this);\n\n    this.name = 'Export as JSON';\n    this.key = EXPORT_AS_JSON_ACTION_KEY;\n    this.description = '';\n    this.cssClass = 'icon-export';\n    this.group = 'export';\n    this.priority = 1;\n\n    this.tree = null;\n    this.calls = null;\n    this.idMap = null;\n    this.dialog = null;\n    this.progressPerc = 0;\n    this.exportedCount = 0;\n    this.totalToExport = 0;\n\n    this.JSONExportService = new JSONExporter();\n  }\n\n  // Public\n  /**\n   *\n   * @param {Object} objectPath\n   * @returns {boolean}\n   */\n  appliesTo(objectPath) {\n    let domainObject = objectPath[0];\n\n    return this.#isCreatableAndPersistable(domainObject);\n  }\n  /**\n   *\n   * @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath\n   */\n  invoke(objectPath) {\n    this.tree = {};\n    this.calls = 0;\n    this.idMap = {};\n\n    const root = objectPath[0];\n    this.root = this.#copy(root);\n\n    const rootId = this.#getKeystring(this.root);\n    this.tree[rootId] = this.root;\n\n    this.dialog = this.#openmct.overlays.progressDialog({\n      message:\n        'Do not navigate away from this page or close this browser tab while this message is displayed.',\n      iconClass: 'info',\n      title: 'Exporting'\n    });\n    this.dialog.show();\n    this.#write(this.root)\n      .then(() => {\n        this.exportedCount++;\n        this.#updateProgress();\n      })\n      .catch((error) => {\n        this.dialog.dismiss();\n        this.dialog = null;\n        this.#resetCounts();\n        this.#openmct.notifications.error({\n          title: 'Export as JSON failed',\n          message: error.message\n        });\n      });\n  }\n\n  /**\n   * @private\n   * @param {import('openmct').DomainObject} parent\n   */\n  async #write(parent) {\n    this.totalToExport++;\n    this.calls++;\n\n    //conditional object styles are not saved on the composition, so we need to check for them\n    const conditionSetIdentifier = this.#getConditionSetIdentifier(parent);\n    const hasItemConditionSetIdentifiers = this.#hasItemConditionSetIdentifiers(parent);\n    const composition = this.#openmct.composition.get(parent);\n\n    if (composition) {\n      const children = await composition.load();\n      const exportPromises = children.map((child) => this.#exportObject(child, parent));\n\n      await Promise.all(exportPromises);\n    }\n\n    if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {\n      this.#decrementCallsAndSave();\n    } else {\n      let conditionSetObjects = [];\n\n      // conditionSetIdentifiers directly in objectStyles object\n      if (conditionSetIdentifier) {\n        const conditionSetObject = this.#openmct.objects.get(conditionSetIdentifier);\n        conditionSetObjects.push(conditionSetObject);\n      }\n\n      // conditionSetIdentifiers stored on item ids in the objectStyles object\n      if (hasItemConditionSetIdentifiers) {\n        const itemConditionSetIdentifiers = this.#getItemConditionSetIdentifiers(parent);\n        const itemConditionSetObjects = itemConditionSetIdentifiers.map((id) =>\n          this.#openmct.objects.get(id)\n        );\n        conditionSetObjects = conditionSetObjects.concat(itemConditionSetObjects);\n\n        for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) {\n          conditionSetObjects.push(this.#openmct.objects.get(itemConditionSetIdentifier));\n        }\n      }\n\n      if (conditionSetObjects.length > 0) {\n        const resolvedConditionSetObjects = await Promise.all(conditionSetObjects);\n        const exportConditionSetPromises = resolvedConditionSetObjects.map((obj) =>\n          this.#exportObject(obj, parent)\n        );\n        await Promise.all(exportConditionSetPromises);\n      }\n\n      this.#decrementCallsAndSave();\n    }\n  }\n\n  #updateProgress() {\n    this.progressPerc = Math.ceil((100 * this.exportedCount) / this.totalToExport);\n    this.dialog?.updateProgress(\n      this.progressPerc,\n      `Exporting ${this.exportedCount} / ${this.totalToExport} objects.`\n    );\n  }\n\n  #exportObject(child, parent) {\n    const originalKeyString = this.#getKeystring(child);\n    const createable = this.#isCreatableAndPersistable(child);\n    const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString);\n\n    if (createable && isNotInfinite) {\n      // for external or linked objects we generate new keys, if they don't exist already\n      if (this.#isLinkedObject(child, parent)) {\n        child = this.#rewriteLink(child, parent);\n      } else {\n        this.tree[originalKeyString] = child;\n      }\n\n      this.#write(child).then(() => {\n        this.exportedCount++;\n        this.#updateProgress();\n      });\n    }\n  }\n\n  /**\n   * @private\n   * @param {Object} child\n   * @param {Object} parent\n   * @returns {Object}\n   */\n  #rewriteLink(child, parent) {\n    const originalKeyString = this.#getKeystring(child);\n    const parentKeyString = this.#getKeystring(parent);\n    const conditionSetIdentifier = this.#getConditionSetIdentifier(parent);\n    const hasItemConditionSetIdentifiers = this.#hasItemConditionSetIdentifiers(parent);\n    const existingMappedKeyString = this.idMap[originalKeyString];\n    let copy;\n\n    if (!existingMappedKeyString) {\n      copy = this.#copy(child);\n      copy.identifier.key = uuid();\n\n      if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {\n        copy.location = parentKeyString;\n      }\n\n      let newKeyString = this.#getKeystring(copy);\n      this.idMap[originalKeyString] = newKeyString;\n      this.tree[newKeyString] = copy;\n    } else {\n      copy = this.tree[existingMappedKeyString];\n    }\n\n    if (conditionSetIdentifier || hasItemConditionSetIdentifiers) {\n      // update objectStyle object\n      if (conditionSetIdentifier) {\n        const directObjectStylesIdentifier = this.#openmct.objects.areIdsEqual(\n          parent.configuration.objectStyles.conditionSetIdentifier,\n          child.identifier\n        );\n\n        if (directObjectStylesIdentifier) {\n          parent.configuration.objectStyles.conditionSetIdentifier = copy.identifier;\n          this.tree[parentKeyString].configuration.objectStyles.conditionSetIdentifier =\n            copy.identifier;\n        }\n      }\n\n      // update per item id on objectStyle object\n      if (hasItemConditionSetIdentifiers) {\n        for (const itemId in parent.configuration.objectStyles) {\n          if (parent.configuration.objectStyles[itemId]) {\n            const itemConditionSetIdentifier =\n              parent.configuration.objectStyles[itemId].conditionSetIdentifier;\n\n            if (\n              itemConditionSetIdentifier &&\n              this.#openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier)\n            ) {\n              parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier;\n              this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier =\n                copy.identifier;\n            }\n          }\n        }\n      }\n    } else {\n      // just update parent\n      const index = parent.composition.findIndex((identifier) => {\n        return this.#openmct.objects.areIdsEqual(child.identifier, identifier);\n      });\n\n      parent.composition[index] = copy.identifier;\n      this.tree[parentKeyString].composition[index] = copy.identifier;\n    }\n\n    return copy;\n  }\n\n  /**\n   * @private\n   * @param {Object} domainObject\n   * @returns {string} A string representation of the given identifier, including namespace and key\n   */\n  #getKeystring(domainObject) {\n    return this.#openmct.objects.makeKeyString(domainObject.identifier);\n  }\n\n  /**\n   * @private\n   * @param {Object} domainObject\n   * @returns {boolean}\n   */\n  #isCreatableAndPersistable(domainObject) {\n    const type = this.#openmct.types.get(domainObject.type);\n    const isPersistable = this.#openmct.objects.isPersistable(domainObject.identifier);\n\n    return type && type.definition.creatable && isPersistable;\n  }\n\n  /**\n   * @private\n   * @param {Object} child\n   * @param {Object} parent\n   * @returns {boolean}\n   */\n  #isLinkedObject(child, parent) {\n    const rootKeyString = this.#getKeystring(this.root);\n    const childKeyString = this.#getKeystring(child);\n    const parentKeyString = this.#getKeystring(parent);\n\n    return (\n      (child.location !== parentKeyString &&\n        !Object.keys(this.tree).includes(child.location) &&\n        childKeyString !== rootKeyString) ||\n      this.idMap[childKeyString] !== undefined\n    );\n  }\n\n  #getConditionSetIdentifier(object) {\n    return object.configuration?.objectStyles?.conditionSetIdentifier;\n  }\n\n  #hasItemConditionSetIdentifiers(parent) {\n    const objectStyles = parent.configuration?.objectStyles;\n\n    for (const itemId in objectStyles) {\n      if (Object.prototype.hasOwnProperty.call(objectStyles[itemId], 'conditionSetIdentifier')) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  #getItemConditionSetIdentifiers(parent) {\n    const objectStyles = parent.configuration?.objectStyles;\n    let identifiers = new Set();\n\n    if (objectStyles) {\n      Object.keys(objectStyles).forEach((itemId) => {\n        if (objectStyles[itemId].conditionSetIdentifier) {\n          identifiers.add(objectStyles[itemId].conditionSetIdentifier);\n        }\n      });\n    }\n\n    return Array.from(identifiers);\n  }\n\n  #resetCounts() {\n    this.totalToExport = 0;\n    this.exportedCount = 0;\n  }\n\n  /**\n   * @private\n   */\n  #rewriteReferences() {\n    const oldKeyStrings = Object.keys(this.idMap);\n    let treeString = JSON.stringify(this.tree);\n\n    oldKeyStrings.forEach((oldKeyString) => {\n      // this will cover keyStrings, identifiers and identifiers created\n      // by hand that may be structured differently from those created with 'makeKeyString'\n      const newKeyString = this.idMap[oldKeyString];\n      const newIdentifier = JSON.stringify(this.#openmct.objects.parseKeyString(newKeyString));\n      const oldIdentifier = this.#openmct.objects.parseKeyString(oldKeyString);\n      const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier);\n      const oldIdentifierKeyFirst = JSON.stringify({\n        key: oldIdentifier.key,\n        namespace: oldIdentifier.namespace\n      });\n\n      // replace keyStrings\n      treeString = treeString.split(oldKeyString).join(newKeyString);\n\n      // check for namespace first identifiers, replace if necessary\n      if (treeString.includes(oldIdentifierNamespaceFirst)) {\n        treeString = treeString.split(oldIdentifierNamespaceFirst).join(newIdentifier);\n      }\n\n      // check for key first identifiers, replace if necessary\n      if (treeString.includes(oldIdentifierKeyFirst)) {\n        treeString = treeString.split(oldIdentifierKeyFirst).join(newIdentifier);\n      }\n    });\n    this.tree = JSON.parse(treeString);\n  }\n  /**\n   * @private\n   * @param {Object} completedTree\n   */\n  saveAs(completedTree) {\n    this.JSONExportService.export(completedTree, { filename: this.root.name + '.json' });\n  }\n  /**\n   * @private\n   * @returns {Object}\n   */\n  #wrapTree() {\n    return {\n      openmct: this.tree,\n      rootId: this.#getKeystring(this.root)\n    };\n  }\n\n  #decrementCallsAndSave() {\n    this.calls--;\n    this.#updateProgress();\n    if (this.calls === 0) {\n      this.#rewriteReferences();\n      this.dialog.dismiss();\n\n      this.#resetCounts();\n      this.saveAs(this.#wrapTree());\n\n      this.dialog = null;\n    }\n  }\n\n  #copy(object) {\n    return JSON.parse(JSON.stringify(object));\n  }\n}\n\nexport { EXPORT_AS_JSON_ACTION_KEY };\n\nexport default ExportAsJSONAction;\n"
  },
  {
    "path": "src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('Export as JSON plugin', () => {\n  const ACTION_KEY = 'export.JSON';\n\n  let openmct;\n  let domainObject;\n  let exportAsJSONAction;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    exportAsJSONAction = openmct.actions.getAction(ACTION_KEY);\n  });\n\n  afterEach(() => resetApplicationState(openmct));\n\n  it('Export as JSON action exist', () => {\n    expect(exportAsJSONAction.key).toEqual(ACTION_KEY);\n  });\n\n  it('ExportAsJSONAction applies to folder', () => {\n    domainObject = {\n      identifier: {\n        key: 'export-testing',\n        namespace: ''\n      },\n      composition: [],\n      location: 'mine',\n      modified: 1640115501237,\n      name: 'Unnamed Folder',\n      persisted: 1640115501237,\n      type: 'folder'\n    };\n\n    expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);\n  });\n\n  it('ExportAsJSONAction applies to telemetry.plot.overlay', () => {\n    domainObject = {\n      identifier: {\n        key: 'export-testing',\n        namespace: ''\n      },\n      composition: [],\n      location: 'mine',\n      modified: 1640115501237,\n      name: 'Unnamed Plot',\n      persisted: 1640115501237,\n      type: 'telemetry.plot.overlay'\n    };\n\n    expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);\n  });\n\n  it('ExportAsJSONAction applies to telemetry.plot.stacked', () => {\n    domainObject = {\n      identifier: {\n        key: 'export-testing',\n        namespace: ''\n      },\n      composition: [],\n      location: 'mine',\n      modified: 1640115501237,\n      name: 'Unnamed Plot',\n      persisted: 1640115501237,\n      type: 'telemetry.plot.stacked'\n    };\n\n    expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);\n  });\n\n  it('ExportAsJSONAction does not apply to non-persistable objects', () => {\n    domainObject = {\n      identifier: {\n        key: 'export-testing',\n        namespace: ''\n      },\n      composition: [],\n      location: 'mine',\n      modified: 1640115501237,\n      name: 'Non Editable Folder',\n      persisted: 1640115501237,\n      type: 'folder'\n    };\n\n    spyOn(openmct.objects, 'getProvider').and.callFake(() => {\n      return { get: () => domainObject };\n    });\n\n    expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(false);\n  });\n\n  it('ExportAsJSONAction exports object from tree', (done) => {\n    const parent = {\n      composition: [\n        {\n          key: 'child',\n          namespace: ''\n        }\n      ],\n      identifier: {\n        key: 'parent',\n        namespace: ''\n      },\n      name: 'Parent',\n      type: 'folder',\n      modified: 1503598129176,\n      location: 'mine',\n      persisted: 1503598129176\n    };\n\n    const child = {\n      composition: [],\n      identifier: {\n        key: 'child',\n        namespace: ''\n      },\n      name: 'Child',\n      type: 'folder',\n      modified: 1503598132428,\n      location: 'parent',\n      persisted: 1503598132428\n    };\n\n    spyOn(openmct.composition, 'get').and.callFake((object) => {\n      return {\n        load: () => {\n          if (object.name === 'Parent') {\n            return Promise.resolve([child]);\n          }\n\n          return Promise.resolve([]);\n        }\n      };\n    });\n\n    spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {\n      expect(Object.keys(completedTree).length).toBe(2);\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).toBeTruthy();\n\n      done();\n    });\n\n    exportAsJSONAction.invoke([parent]);\n  });\n\n  it('ExportAsJSONAction skips non-creatable objects from tree', (done) => {\n    const parent = {\n      composition: [\n        {\n          key: 'child',\n          namespace: ''\n        }\n      ],\n      identifier: {\n        key: 'parent',\n        namespace: ''\n      },\n      name: 'Parent of Non Editable Child Folder',\n      type: 'folder',\n      modified: 1503598129176,\n      location: 'mine',\n      persisted: 1503598129176\n    };\n\n    const child = {\n      composition: [],\n      identifier: {\n        key: 'child',\n        namespace: ''\n      },\n      name: 'Non Editable Child Folder',\n      type: 'noneditable.folder',\n      modified: 1503598132428,\n      location: 'parent',\n      persisted: 1503598132428\n    };\n\n    spyOn(openmct.composition, 'get').and.callFake((object) => {\n      return {\n        load: () => {\n          if (object.identifier.key === 'parent') {\n            return Promise.resolve([child]);\n          }\n\n          return Promise.resolve([]);\n        }\n      };\n    });\n\n    spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {\n      expect(Object.keys(completedTree).length).toBe(2);\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();\n\n      done();\n    });\n\n    exportAsJSONAction.invoke([parent]);\n  });\n\n  it('can export self-containing objects', (done) => {\n    const parent = {\n      composition: [\n        {\n          key: 'infiniteChild',\n          namespace: ''\n        }\n      ],\n      identifier: {\n        key: 'infiniteParent',\n        namespace: ''\n      },\n      name: 'parent',\n      type: 'folder',\n      modified: 1503598129176,\n      location: 'mine',\n      persisted: 1503598129176\n    };\n\n    const child = {\n      composition: [\n        {\n          key: 'infiniteParent',\n          namespace: ''\n        }\n      ],\n      identifier: {\n        key: 'infiniteChild',\n        namespace: ''\n      },\n      name: 'child',\n      type: 'folder',\n      modified: 1503598132428,\n      location: 'infiniteParent',\n      persisted: 1503598132428\n    };\n\n    spyOn(openmct.composition, 'get').and.callFake((object) => {\n      return {\n        load: () => {\n          if (object.name === 'parent') {\n            return Promise.resolve([child]);\n          }\n\n          return Promise.resolve([]);\n        }\n      };\n    });\n\n    spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {\n      expect(Object.keys(completedTree).length).toBe(2);\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();\n      expect(\n        Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')\n      ).toBeTruthy();\n      expect(\n        Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteChild')\n      ).toBeTruthy();\n\n      done();\n    });\n\n    exportAsJSONAction.invoke([parent]);\n  });\n\n  it('exports links to external objects as new objects', function (done) {\n    const parent = {\n      composition: [\n        {\n          key: 'child',\n          namespace: ''\n        }\n      ],\n      identifier: {\n        key: 'parent',\n        namespace: ''\n      },\n      name: 'Parent',\n      type: 'folder',\n      modified: 1503598129176,\n      location: 'mine',\n      persisted: 1503598129176\n    };\n\n    const child = {\n      composition: [],\n      identifier: {\n        key: 'child',\n        namespace: ''\n      },\n      name: 'Child',\n      type: 'folder',\n      modified: 1503598132428,\n      location: 'outsideOfTree',\n      persisted: 1503598132428\n    };\n\n    spyOn(openmct.composition, 'get').and.callFake((object) => {\n      return {\n        load: () => {\n          if (object.name === 'Parent') {\n            return Promise.resolve([child]);\n          }\n\n          return Promise.resolve([]);\n        }\n      };\n    });\n\n    spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {\n      expect(Object.keys(completedTree).length).toBe(2);\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();\n\n      // parent and child objects as part of openmct but child with new id/key\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();\n      expect(Object.keys(completedTree.openmct).length).toBe(2);\n\n      done();\n    });\n\n    exportAsJSONAction.invoke([parent]);\n  });\n\n  it('ExportAsJSONAction exports object references from tree', (done) => {\n    const parent = {\n      composition: [],\n      configuration: {\n        objectStyles: {\n          conditionSetIdentifier: {\n            key: 'child',\n            namespace: ''\n          }\n        }\n      },\n      identifier: {\n        key: 'parent',\n        namespace: ''\n      },\n      name: 'Parent',\n      type: 'folder',\n      modified: 1503598129176,\n      location: 'mine',\n      persisted: 1503598129176\n    };\n\n    const child = {\n      composition: [],\n      identifier: {\n        key: 'child',\n        namespace: ''\n      },\n      name: 'Child',\n      type: 'folder',\n      modified: 1503598132428,\n      location: null,\n      persisted: 1503598132428\n    };\n\n    spyOn(openmct.objects, 'get').and.callFake((object) => {\n      return Promise.resolve(child);\n    });\n\n    spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {\n      expect(Object.keys(completedTree).length).toBe(2);\n      const conditionSetId = Object.keys(completedTree.openmct)[1];\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();\n      expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();\n      expect(completedTree.openmct[conditionSetId].name).toBe('Child');\n\n      done();\n    });\n\n    exportAsJSONAction.invoke([parent]);\n  });\n});\n"
  },
  {
    "path": "src/plugins/exportAsJSONAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ExportAsJSONAction from './ExportAsJSONAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new ExportAsJSONAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementInspector.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div v-if=\"isShowDetails\" class=\"c-inspector__properties c-inspect-properties\">\n    <div class=\"c-inspect-properties__header\">Fault Details</div>\n    <ul class=\"c-inspect-properties__section\">\n      <DetailText :detail=\"sourceDetails\" />\n      <DetailText :detail=\"occurredDetails\" />\n      <DetailText :detail=\"criticalityDetails\" />\n      <DetailText :detail=\"descriptionDetails\" />\n    </ul>\n\n    <div class=\"c-inspect-properties__header\">Telemetry</div>\n    <ul class=\"c-inspect-properties__section\">\n      <DetailText :detail=\"systemDetails\" />\n      <DetailText :detail=\"tripValueDetails\" />\n      <DetailText :detail=\"currentValueDetails\" />\n    </ul>\n  </div>\n</template>\n\n<script>\nimport DetailText from '../inspectorViews/properties/DetailText.vue';\n\nexport default {\n  name: 'FaultManagementInspector',\n  components: {\n    DetailText\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      isShowDetails: false\n    };\n  },\n  computed: {\n    criticalityDetails() {\n      return {\n        name: 'Criticality',\n        value: this.selectedFault?.severity\n      };\n    },\n    currentValueDetails() {\n      return {\n        name: 'Live value',\n        value: this.selectedFault?.currentValueInfo?.value\n      };\n    },\n    descriptionDetails() {\n      return {\n        name: 'Description',\n        value: this.selectedFault?.shortDescription\n      };\n    },\n    occurredDetails() {\n      return {\n        name: 'Occurred',\n        value: this.selectedFault?.triggerTime\n      };\n    },\n    sourceDetails() {\n      return {\n        name: 'Source',\n        value: this.selectedFault?.name\n      };\n    },\n    systemDetails() {\n      return {\n        name: 'System',\n        value: this.selectedFault?.namespace\n      };\n    },\n    tripValueDetails() {\n      return {\n        name: 'Trip Value',\n        value: this.selectedFault?.triggerValueInfo?.value\n      };\n    }\n  },\n  mounted() {\n    this.updateSelectedFaults();\n  },\n  methods: {\n    updateSelectedFaults() {\n      const selection = this.openmct.selection.get();\n      this.isShowDetails = false;\n\n      if (selection.length === 0 || selection[0].length < 2) {\n        return;\n      }\n\n      const selectedFaults = selection[0][1].context.selectedFaults;\n      if (selectedFaults.length !== 1) {\n        return;\n      }\n\n      this.isShowDetails = true;\n      this.selectedFault = selectedFaults[0];\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { FAULT_MANAGEMENT_INSPECTOR, FAULT_MANAGEMENT_TYPE } from './constants.js';\nimport FaultManagementInspector from './FaultManagementInspector.vue';\n\nexport default function FaultManagementInspectorViewProvider(openmct) {\n  return {\n    openmct: openmct,\n    key: FAULT_MANAGEMENT_INSPECTOR,\n    name: 'Config',\n    canView: (selection) => {\n      if (selection.length !== 1 || selection[0].length === 0) {\n        return false;\n      }\n\n      let object = selection[0][0].context.item;\n\n      return object && object.type === FAULT_MANAGEMENT_TYPE;\n    },\n    view: (selection) => {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                FaultManagementInspector\n              },\n              provide: {\n                openmct\n              },\n              template: '<FaultManagementInspector></FaultManagementInspector>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementListHeader.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list\">\n    <div class=\"c-fault-mgmt-item-header c-fault-mgmt__checkbox\">\n      <input type=\"checkbox\" :checked=\"isSelectAll\" @change=\"selectAll\" />\n    </div>\n    <div\n      class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity\"\n    >\n      {{ totalFaultsCount }} Results\n    </div>\n    <div class=\"c-fault-mgmt__list-header-content\">\n      <div class=\"c-fault-mgmt__list-content-right\">\n        <div class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal\">Trip Value</div>\n        <div class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal\">Live Value</div>\n        <div class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime\">Trigger Time</div>\n      </div>\n    </div>\n    <div class=\"c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper\">\n      <div class=\"c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button\">\n        <SelectField\n          class=\"c-fault-mgmt-viewButton\"\n          title=\"Sort By\"\n          :model=\"model\"\n          @on-change=\"onChange\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport SelectField from '@/api/forms/components/controls/SelectField.vue';\n\nimport { SORT_ITEMS } from './constants.js';\n\nexport default {\n  components: {\n    SelectField\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    selectedFaults: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    totalFaultsCount: {\n      type: Number,\n      default() {\n        return 0;\n      }\n    }\n  },\n  emits: ['sort-changed', 'select-all'],\n  data() {\n    return {\n      model: {}\n    };\n  },\n  computed: {\n    isSelectAll() {\n      return this.totalFaultsCount > 0 && this.selectedFaults.length === this.totalFaultsCount;\n    }\n  },\n  beforeMount() {\n    const options = Object.values(SORT_ITEMS);\n    this.model = {\n      options,\n      value: options[0].value\n    };\n  },\n  methods: {\n    onChange(data) {\n      this.$emit('sort-changed', data);\n    },\n    selectAll(e) {\n      this.$emit('select-all', e.target.checked);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementListItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    role=\"listitem\"\n    :aria-label=\"listItemAriaLabel\"\n    class=\"c-fault-mgmt__list data-selectable\"\n    :class=\"classesFromState\"\n  >\n    <div class=\"c-fault-mgmt-item c-fault-mgmt__list-checkbox\">\n      <input\n        type=\"checkbox\"\n        :aria-label=\"checkBoxAriaLabel\"\n        :checked=\"isSelected\"\n        @change=\"toggleSelected\"\n      />\n    </div>\n    <div class=\"c-fault-mgmt-item\">\n      <div\n        class=\"c-fault-mgmt__list-severity\"\n        :aria-label=\"severityAriaLabel\"\n        :class=\"['is-severity-' + severity]\"\n      ></div>\n    </div>\n    <div class=\"c-fault-mgmt-item c-fault-mgmt__list-content\">\n      <div class=\"c-fault-mgmt-item c-fault-mgmt__list-pathname\">\n        <div class=\"c-fault-mgmt__list-path\" aria-label=\"Fault namespace\">\n          {{ fault.namespace }}\n        </div>\n        <div class=\"c-fault-mgmt__list-faultname\" aria-label=\"Fault name\">{{ fault.name }}</div>\n      </div>\n      <div class=\"c-fault-mgmt__list-content-right\">\n        <div class=\"c-fault-mgmt-item c-fault-mgmt__list-trigVal\">\n          <div\n            class=\"c-fault-mgmt-item__value\"\n            :class=\"tripValueClassname\"\n            title=\"Trip Value\"\n            aria-label=\"Trip Value\"\n          >\n            {{ fault.triggerValueInfo.value }}\n          </div>\n        </div>\n        <div class=\"c-fault-mgmt-item c-fault-mgmt__list-curVal\">\n          <div\n            class=\"c-fault-mgmt-item__value\"\n            :class=\"liveValueClassname\"\n            title=\"Live Value\"\n            aria-label=\"Live Value\"\n          >\n            {{ fault.currentValueInfo.value }}\n          </div>\n        </div>\n        <div class=\"c-fault-mgmt-item c-fault-mgmt__list-trigTime\">\n          <div\n            class=\"c-fault-mgmt-item__value\"\n            title=\"Last Trigger Time\"\n            aria-label=\"Last Trigger Time\"\n          >\n            {{ fault.triggerTime }}\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"c-fault-mgmt-item c-fault-mgmt__list-action-wrapper\">\n      <button\n        class=\"c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots\"\n        title=\"Disposition Actions\"\n        aria-label=\"Disposition Actions\"\n        @click=\"showActionMenu\"\n      ></button>\n    </div>\n  </div>\n</template>\n<script>\nconst RANGE_CONDITION_CLASS = {\n  LOW: 'is-limit--lwr',\n  HIGH: 'is-limit--upr'\n};\n\nconst SEVERITY_CLASS = {\n  CRITICAL: 'is-limit--red',\n  WARNING: 'is-limit--yellow',\n  WATCH: 'is-limit--cyan'\n};\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    fault: {\n      type: Object,\n      required: true\n    },\n    isSelected: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],\n  computed: {\n    checkBoxAriaLabel() {\n      return `Select fault: ${this.fault.name || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;\n    },\n    classesFromState() {\n      const exclusiveStates = [\n        {\n          className: 'is-shelved',\n          test: () => this.fault.shelved\n        },\n        {\n          className: 'is-unacknowledged',\n          test: () => !this.fault.acknowledged && !this.fault.shelved\n        },\n        {\n          className: 'is-acknowledged',\n          test: () => this.fault.acknowledged && !this.fault.shelved\n        }\n      ];\n\n      const classes = [];\n\n      if (this.isSelected) {\n        classes.push('is-selected');\n      }\n\n      const matchingState = exclusiveStates.find((stateDefinition) => stateDefinition.test());\n\n      if (matchingState !== undefined) {\n        classes.push(matchingState.className);\n      }\n\n      return classes;\n    },\n    liveValueClassname() {\n      const currentValueInfo = this.fault?.currentValueInfo;\n      if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') {\n        return '';\n      }\n\n      let classname = RANGE_CONDITION_CLASS[currentValueInfo.rangeCondition] || '';\n      classname += ' ';\n      classname += SEVERITY_CLASS[currentValueInfo.monitoringResult] || '';\n\n      return classname.trim();\n    },\n    name() {\n      return `${this.fault?.name}/${this.fault?.namespace}`;\n    },\n    severity() {\n      return this.fault?.severity?.toLowerCase();\n    },\n    triggerTime() {\n      return this.fault?.triggerTime;\n    },\n    triggerValue() {\n      return this.fault?.triggerValueInfo?.value;\n    },\n    tripValueClassname() {\n      const triggerValueInfo = this.fault?.triggerValueInfo;\n      if (!triggerValueInfo || triggerValueInfo.monitoringResult === 'IN_LIMITS') {\n        return '';\n      }\n\n      let classname = RANGE_CONDITION_CLASS[triggerValueInfo.rangeCondition] || '';\n      classname += ' ';\n      classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';\n\n      return classname.trim();\n    },\n    listItemAriaLabel() {\n      return `Fault triggered at ${this.fault.triggerTime || 'Unknown'} with severity ${this.fault.severity || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;\n    },\n    severityAriaLabel() {\n      return `Severity: ${this.fault.severity || 'Unknown'}`;\n    }\n  },\n  methods: {\n    showActionMenu(event) {\n      event.stopPropagation();\n\n      const menuItems = [\n        {\n          cssClass: 'icon-check',\n          isDisabled: this.fault.acknowledged,\n          name: 'Acknowledge',\n          description: '',\n          onItemClicked: (e) => {\n            this.clearAllSelected();\n            this.$emit('acknowledge-selected', [this.fault]);\n          }\n        },\n        {\n          cssClass: 'icon-timer',\n          name: 'Shelve',\n          description: '',\n          onItemClicked: () => {\n            this.clearAllSelected();\n            this.$emit('shelve-selected', [this.fault], { shelved: true });\n          }\n        },\n        {\n          cssClass: 'icon-timer',\n          isDisabled: Boolean(!this.fault.shelved),\n          name: 'Unshelve',\n          description: '',\n          onItemClicked: () => {\n            this.clearAllSelected();\n            this.$emit('shelve-selected', [this.fault], { shelved: false });\n          }\n        }\n      ];\n\n      this.openmct.menus.showMenu(event.x, event.y, menuItems);\n    },\n    toggleSelected(event) {\n      const faultData = {\n        fault: this.fault,\n        selected: event.target.checked\n      };\n\n      this.$emit('toggle-selected', faultData);\n    },\n    clearAllSelected() {\n      this.$emit('clear-all-selected');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementObjectProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  FAULT_MANAGEMENT_NAMESPACE,\n  FAULT_MANAGEMENT_TYPE,\n  FAULT_MANAGEMENT_VIEW\n} from './constants.js';\n\nexport default class FaultManagementObjectProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.namespace = FAULT_MANAGEMENT_NAMESPACE;\n    this.key = FAULT_MANAGEMENT_VIEW;\n    this.objects = {};\n\n    this.createFaultManagementRootObject();\n  }\n\n  createFaultManagementRootObject() {\n    this.rootObject = {\n      identifier: {\n        key: this.key,\n        namespace: this.namespace\n      },\n      name: 'Fault Management',\n      type: FAULT_MANAGEMENT_TYPE,\n      location: 'ROOT'\n    };\n\n    this.openmct.objects.addRoot(this.rootObject.identifier);\n  }\n\n  get(identifier) {\n    if (identifier.key === FAULT_MANAGEMENT_VIEW) {\n      return Promise.resolve(this.rootObject);\n    }\n\n    return Promise.reject();\n  }\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementPlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { FAULT_MANAGEMENT_NAMESPACE, FAULT_MANAGEMENT_TYPE } from './constants.js';\nimport FaultManagementInspectorViewProvider from './FaultManagementInspectorViewProvider.js';\nimport FaultManagementObjectProvider from './FaultManagementObjectProvider.js';\nimport FaultManagementViewProvider from './FaultManagementViewProvider.js';\n\nexport default function FaultManagementPlugin() {\n  return function (openmct) {\n    openmct.types.addType(FAULT_MANAGEMENT_TYPE, {\n      name: 'Fault Management',\n      creatable: false,\n      description: 'Fault Management View',\n      cssClass: 'icon-bell'\n    });\n\n    openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new FaultManagementInspectorViewProvider(openmct));\n    openmct.objects.addProvider(\n      FAULT_MANAGEMENT_NAMESPACE,\n      new FaultManagementObjectProvider(openmct)\n    );\n  };\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementSearch.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-fault-mgmt__search-row\">\n    <Search\n      class=\"c-fault-mgmt-search\"\n      :value=\"searchTerm\"\n      @input=\"updateSearchTerm\"\n      @clear=\"updateSearchTerm\"\n    />\n\n    <SelectField\n      class=\"c-fault-mgmt-viewButton\"\n      title=\"View Filter\"\n      :model=\"model\"\n      @on-change=\"onChange\"\n    />\n  </div>\n</template>\n\n<script>\nimport SelectField from '@/api/forms/components/controls/SelectField.vue';\nimport Search from '@/ui/components/SearchComponent.vue';\n\nimport { FILTER_ITEMS } from './constants.js';\n\nexport default {\n  components: {\n    SelectField,\n    Search\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    searchTerm: {\n      type: String,\n      default: ''\n    }\n  },\n  emits: ['filter-changed', 'update-search-term'],\n  data() {\n    return {\n      items: []\n    };\n  },\n  computed: {\n    model() {\n      return {\n        options: this.items,\n        value: this.items[0] ? this.items[0].value : FILTER_ITEMS[0].toLowerCase()\n      };\n    }\n  },\n  mounted() {\n    this.items = FILTER_ITEMS.map((item) => {\n      return {\n        name: item,\n        value: item.toLowerCase()\n      };\n    });\n  },\n  methods: {\n    onChange(data) {\n      this.$emit('filter-changed', data);\n    },\n    updateSearchTerm(searchTerm) {\n      this.$emit('update-search-term', searchTerm);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementToolbar.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-fault-mgmt__toolbar\">\n    <button\n      class=\"c-icon-button icon-check\"\n      :title=\"acknowledgeButtonLabel\"\n      :aria-label=\"acknowledgeButtonLabel\"\n      :disabled=\"disableAcknowledge\"\n      @click=\"acknowledgeSelected\"\n    >\n      <div class=\"c-icon-button__label\">Acknowledge</div>\n    </button>\n\n    <button\n      class=\"c-icon-button icon-timer\"\n      :title=\"shelveButtonLabel\"\n      :aria-label=\"shelveButtonLabel\"\n      :disabled=\"disableShelve\"\n      @click=\"shelveSelected\"\n    >\n      <div class=\"c-icon-button__label\">Shelve</div>\n    </button>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    selectedFaults: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['acknowledge-selected', 'shelve-selected'],\n  data() {\n    return {\n      disableAcknowledge: true,\n      disableShelve: true\n    };\n  },\n  computed: {\n    acknowledgeButtonLabel() {\n      return 'Acknowledge selected faults';\n    },\n    shelveButtonLabel() {\n      return 'Shelve selected faults';\n    }\n  },\n  watch: {\n    selectedFaults(newSelectedFaults) {\n      const selectedfaults = Object.values(newSelectedFaults);\n\n      let disableAcknowledge = true;\n      let disableShelve = true;\n\n      selectedfaults.forEach((fault) => {\n        if (!fault.shelved) {\n          disableShelve = false;\n        }\n\n        if (!fault.acknowledged) {\n          disableAcknowledge = false;\n        }\n      });\n\n      this.disableAcknowledge = disableAcknowledge;\n      this.disableShelve = disableShelve;\n    }\n  },\n  methods: {\n    acknowledgeSelected() {\n      this.$emit('acknowledge-selected');\n    },\n    shelveSelected() {\n      this.$emit('shelve-selected');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-faults-list-view\">\n    <FaultManagementSearch\n      :search-term=\"searchTerm\"\n      @filter-changed=\"updateFilter\"\n      @update-search-term=\"updateSearchTerm\"\n    />\n\n    <FaultManagementToolbar\n      v-if=\"showToolbar\"\n      :selected-faults=\"selectedFaults\"\n      @acknowledge-selected=\"toggleAcknowledgeSelected\"\n      @shelve-selected=\"toggleShelveSelected\"\n    />\n\n    <div class=\"c-faults-list-view-header-item-container-wrapper\">\n      <div class=\"c-faults-list-view-header-item-container\">\n        <FaultManagementListHeader\n          class=\"header\"\n          :selected-faults=\"selectedFaults\"\n          :total-faults-count=\"filteredFaultsList.length\"\n          @select-all=\"selectAll\"\n          @sort-changed=\"sortChanged\"\n        />\n\n        <div class=\"c-faults-list-view-item-body\">\n          <template v-if=\"filteredFaultsList.length > 0\">\n            <FaultManagementListItem\n              v-for=\"fault of filteredFaultsList\"\n              :key=\"fault.id\"\n              :fault=\"fault\"\n              :is-selected=\"isSelected(fault)\"\n              @toggle-selected=\"toggleSelected\"\n              @acknowledge-selected=\"toggleAcknowledgeSelected\"\n              @shelve-selected=\"toggleShelveSelected\"\n              @clear-all-selected=\"resetSelectedFaultMap\"\n            />\n          </template>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport {\n  FAULT_MANAGEMENT_ALARMS,\n  FAULT_MANAGEMENT_GLOBAL_ALARMS,\n  FILTER_ITEMS,\n  SORT_ITEMS\n} from './constants.js';\nimport FaultManagementListHeader from './FaultManagementListHeader.vue';\nimport FaultManagementListItem from './FaultManagementListItem.vue';\nimport FaultManagementSearch from './FaultManagementSearch.vue';\nimport FaultManagementToolbar from './FaultManagementToolbar.vue';\n\nconst SEARCH_KEYS = [\n  'id',\n  'triggerValueInfo',\n  'currentValueInfo',\n  'triggerTime',\n  'severity',\n  'name',\n  'shortDescription',\n  'namespace'\n];\n\n// Helper function for filtering faults\nfunction filterFaultsByTerm(faults, searchTerm) {\n  return faults.filter((fault) =>\n    SEARCH_KEYS.some((key) => fault[key]?.toString().toLowerCase().includes(searchTerm))\n  );\n}\n\nexport default {\n  components: {\n    FaultManagementListHeader,\n    FaultManagementListItem,\n    FaultManagementSearch,\n    FaultManagementToolbar\n  },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      faultsList: [],\n      filterIndex: 0,\n      searchTerm: '',\n      selectedFaultMap: {},\n      sortBy: Object.values(SORT_ITEMS)[0].value\n    };\n  },\n  computed: {\n    selectedFaults() {\n      return Object.values(this.selectedFaultMap);\n    },\n    filteredFaultsList() {\n      const filterName = FILTER_ITEMS[this.filterIndex];\n      let list = this.faultsList.filter((fault) =>\n        filterName === 'Shelved' ? fault.shelved : !fault.shelved\n      );\n\n      if (filterName === 'Acknowledged') {\n        list = list.filter((fault) => fault.acknowledged);\n      } else if (filterName === 'Unacknowledged') {\n        list = list.filter((fault) => !fault.acknowledged);\n      }\n\n      if (this.searchTerm.length > 0) {\n        list = filterFaultsByTerm(list, this.searchTerm);\n      }\n\n      list.sort(SORT_ITEMS[this.sortBy].sortFunction);\n\n      return list;\n    },\n    showToolbar() {\n      return this.openmct.faults.supportsActions();\n    }\n  },\n  created() {\n    this.shelveDurations = this.openmct.faults.getShelveDurations();\n  },\n  mounted() {\n    this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);\n  },\n  beforeUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n  },\n  methods: {\n    updateFault({ fault, type }) {\n      if (type === FAULT_MANAGEMENT_GLOBAL_ALARMS) {\n        this.updateFaultList();\n      } else if (type === FAULT_MANAGEMENT_ALARMS) {\n        this.faultsList.forEach((faultValue, i) => {\n          if (fault.id === faultValue.id) {\n            this.faultsList[i] = fault;\n          }\n        });\n      }\n    },\n    async updateFaultList() {\n      const faultsData = await this.openmct.faults.request(this.domainObject);\n      if (faultsData?.length > 0) {\n        this.faultsList = faultsData.map((fd) => fd.fault);\n      } else {\n        this.faultsList = [];\n      }\n    },\n    filterUsingSearchTerm(fault) {\n      if (!fault) {\n        return false;\n      }\n\n      let match = false;\n\n      SEARCH_KEYS.forEach((key) => {\n        if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {\n          match = true;\n        }\n      });\n\n      return match;\n    },\n    isSelected(fault) {\n      return Boolean(this.selectedFaultMap[fault.id]);\n    },\n    selectAll(toggle = false) {\n      this.faultsList.forEach((fault) => {\n        const faultData = {\n          fault,\n          selected: toggle\n        };\n        this.toggleSelected(faultData);\n      });\n    },\n    sortChanged(sort) {\n      this.sortBy = sort.value;\n    },\n    toggleSelected({ fault, selected = false }) {\n      if (selected) {\n        this.selectedFaultMap[fault.id] = fault;\n      } else {\n        delete this.selectedFaultMap[fault.id];\n      }\n\n      this.openmct.selection.select(\n        [\n          {\n            element: this.$el,\n            context: {\n              item: this.openmct.router.path[0]\n            }\n          },\n          {\n            element: this.$el,\n            context: {\n              selectedFaults: this.selectedFaults\n            }\n          }\n        ],\n        false\n      );\n    },\n    async toggleAcknowledgeSelected(faults = this.selectedFaults) {\n      const title = this.getAcknowledgeTitle(faults);\n\n      const formStructure = this.getAcknowledgeFormStructure(title);\n\n      try {\n        const data = await this.openmct.forms.showForm(formStructure);\n        this.acknowledgeFaults(faults, data);\n      } catch (err) {\n        console.error(err);\n      } finally {\n        this.resetSelectedFaultMap();\n      }\n    },\n    getAcknowledgeTitle(faults) {\n      if (faults.length > 1) {\n        return `Acknowledge ${faults.length} selected faults`;\n      } else if (faults.length === 1) {\n        return `Acknowledge fault: ${faults[0].name}`;\n      }\n      return '';\n    },\n    getAcknowledgeFormStructure(title) {\n      return {\n        title,\n        sections: [\n          {\n            rows: [\n              {\n                key: 'comment',\n                control: 'textarea',\n                name: 'Optional comment',\n                pattern: '\\\\S+',\n                required: false,\n                cssClass: 'l-input-lg',\n                value: ''\n              }\n            ]\n          }\n        ],\n        buttons: {\n          submit: {\n            label: 'Acknowledge'\n          }\n        }\n      };\n    },\n    acknowledgeFaults(faults, data) {\n      faults.forEach((fault) => {\n        this.openmct.faults.acknowledgeFault(fault, data);\n      });\n    },\n    resetSelectedFaultMap() {\n      Object.keys(this.selectedFaultMap).forEach((key) => {\n        delete this.selectedFaultMap[key];\n      });\n    },\n    async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {\n      const { shelved = true } = shelveData;\n      if (shelved) {\n        const title =\n          faults.length > 1\n            ? `Shelve ${faults.length} selected faults`\n            : `Shelve fault: ${faults[0].name}`;\n        const formStructure = {\n          title,\n          sections: [\n            {\n              rows: [\n                {\n                  key: 'comment',\n                  control: 'textarea',\n                  name: 'Optional comment',\n                  pattern: '\\\\S+',\n                  required: false,\n                  cssClass: 'l-input-lg',\n                  value: ''\n                },\n                {\n                  key: 'shelveDuration',\n                  control: 'select',\n                  name: 'Shelve duration',\n                  options: this.shelveDurations,\n                  required: false,\n                  cssClass: 'l-input-lg',\n                  value: this.shelveDurations[0].value\n                }\n              ]\n            }\n          ],\n          buttons: {\n            submit: {\n              label: 'Shelve'\n            }\n          }\n        };\n\n        let data;\n        try {\n          data = await this.openmct.forms.showForm(formStructure);\n        } catch (e) {\n          return;\n        }\n\n        shelveData.comment = data.comment || '';\n        shelveData.shelveDuration =\n          data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration;\n      } else {\n        shelveData = {\n          shelved: false\n        };\n      }\n\n      await Promise.all(\n        faults.map((selectedFault) => this.openmct.faults.shelveFault(selectedFault, shelveData))\n      );\n\n      this.selectedFaultMap = {};\n    },\n    updateFilter(filter) {\n      this.selectAll();\n\n      this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);\n    },\n    updateSearchTerm(term = '') {\n      this.searchTerm = term.toLowerCase();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/faultManagement/FaultManagementViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW } from './constants.js';\nimport FaultManagementView from './FaultManagementView.vue';\n\nexport default class FaultManagementViewProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.key = FAULT_MANAGEMENT_VIEW;\n  }\n\n  canView(domainObject) {\n    return domainObject.type === FAULT_MANAGEMENT_TYPE;\n  }\n\n  canEdit(domainObject) {\n    return false;\n  }\n\n  view(domainObject) {\n    const openmct = this.openmct;\n    let _destroy = null;\n\n    return {\n      show: (element) => {\n        const { destroy } = mount(\n          {\n            el: element,\n            components: {\n              FaultManagementView\n            },\n            provide: {\n              openmct,\n              domainObject\n            },\n            template: '<FaultManagementView></FaultManagementView>'\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        _destroy = destroy;\n      },\n      destroy: () => {\n        if (_destroy) {\n          _destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/constants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst FAULT_SEVERITY = {\n  CRITICAL: {\n    name: 'CRITICAL',\n    value: 'critical',\n    priority: 0\n  },\n  WARNING: {\n    name: 'WARNING',\n    value: 'warning',\n    priority: 1\n  },\n  WATCH: {\n    name: 'WATCH',\n    value: 'watch',\n    priority: 2\n  }\n};\n\nfunction sortByTriggerTime(a, b) {\n  if (b.triggerTime > a.triggerTime) {\n    return 1;\n  }\n\n  if (a.triggerTime > b.triggerTime) {\n    return -1;\n  }\n\n  return 0;\n}\n\nexport const FAULT_MANAGEMENT_TYPE = 'faultManagement';\nexport const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';\nexport const FAULT_MANAGEMENT_ALARMS = 'alarms';\nexport const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';\nexport const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';\nexport const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';\nexport const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];\nexport const SORT_ITEMS = {\n  'unacknowledged-first': {\n    name: 'Unacknowledged First',\n    value: 'unacknowledged-first',\n    sortFunction: (a, b) => {\n      const aAck = Boolean(a.acknowledged);\n      const bAck = Boolean(b.acknowledged);\n\n      if (aAck !== bAck) {\n        return aAck ? 1 : -1;\n      }\n\n      return sortByTriggerTime(a, b);\n    }\n  },\n  'newest-first': {\n    name: 'Newest First',\n    value: 'newest-first',\n    sortFunction: sortByTriggerTime\n  },\n  'oldest-first': {\n    name: 'Oldest First',\n    value: 'oldest-first',\n    sortFunction: (a, b) => {\n      if (a.triggerTime > b.triggerTime) {\n        return 1;\n      }\n\n      if (a.triggerTime < b.triggerTime) {\n        return -1;\n      }\n\n      return 0;\n    }\n  },\n  severity: {\n    name: 'Severity',\n    value: 'severity',\n    sortFunction: (a, b) => {\n      const diff = FAULT_SEVERITY[a.severity].priority - FAULT_SEVERITY[b.severity].priority;\n      if (diff !== 0) {\n        return diff;\n      }\n\n      return sortByTriggerTime(a, b);\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/faultManagement/fault-manager.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n$colorFaultItemFg: $colorBodyFg;\n$colorFaultItemFgEmphasis: $colorBodyFgEm;\n$colorFaultItemBg: pullForward($colorBodyBg, 5%);\n\n/*********************************************** SEARCH  */\n.c-fault-mgmt__search-row {\n  display: flex;\n  align-items: center;\n  flex: 0 0 auto;\n  > * + * {\n    margin-left: 10px;\n    float: right;\n  }\n}\n\n.c-fault-mgmt-search {\n  width: 95%;\n}\n\n/*********************************************** TOOLBAR */\n.c-fault-mgmt__toolbar {\n  display: flex;\n  justify-content: center;\n  flex: 0 0 auto;\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n}\n\n/*********************************************** LIST VIEW */\n.c-faults-list-view {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n}\n\n.c-faults-list-view-header-item-container {\n  display: grid;\n  width: 100%;\n  grid-template-columns: max-content max-content repeat(5, minmax(max-content, 20%)) max-content;\n  grid-row-gap: $interiorMargin;\n\n  &-wrapper {\n    flex: 1 1 auto;\n    padding-right: $interiorMargin; // Fend of from scrollbar\n    overflow-y: auto;\n  }\n\n  .--width-less-than-600 & {\n    grid-template-columns: max-content max-content 1fr 1fr max-content;\n  }\n}\n\n.c-faults-list-view-item-body {\n  display: contents;\n}\n\n/*********************************************** LIST */\n.c-fault-mgmt__list {\n  display: contents;\n  color: $colorFaultItemFg;\n\n  &-checkbox {\n    border-top-left-radius: 4px;\n    border-bottom-left-radius: 4px;\n  }\n\n  &-severity {\n    font-size: 2em;\n\n    &.is-severity-critical {\n      @include glyphBefore($glyph-icon-alert-triangle);\n      color: $colorStatusError;\n    }\n\n    &.is-severity-warning {\n      @include glyphBefore($glyph-icon-alert-rect);\n      color: $colorStatusAlert;\n    }\n\n    &.is-severity-watch {\n      @include glyphBefore($glyph-icon-info);\n      color: $colorCommand;\n    }\n  }\n\n  &-content {\n    display: contents;\n\n    .--width-less-than-600 & {\n      display: flex;\n      flex-wrap: wrap;\n      grid-column: span 2;\n    }\n  }\n\n  &-pathname {\n    padding-right: $interiorMarginLg;\n    overflow-wrap: anywhere;\n    min-width: 100px;\n  }\n  &-path {\n    font-size: 0.85em;\n    margin-left: $interiorMargin;\n  }\n\n  &-faultname {\n    font-size: 1.3em;\n    margin-left: $interiorMargin;\n  }\n\n  &-content-right {\n    display: contents;\n  }\n\n  &-trigTime {\n    grid-column: 6 / span 2;\n  }\n\n  &-action-wrapper {\n    text-align: right;\n    flex: 0 0 auto;\n    align-items: stretch;\n  }\n\n  &-action-button {\n    flex: 0 0 auto;\n    margin-left: auto;\n    text-align: right;\n  }\n\n  // STATES\n  &.is-unacknowledged {\n    color: $colorFaultItemFgEmphasis;\n    .c-fault-mgmt__list-severity {\n      @include pulse($animName: severityAnim, $dur: 200ms);\n    }\n  }\n\n  &.is-acknowledged,\n  &.is-shelved {\n    .c-fault-mgmt__list-severity {\n      &:before {\n        opacity: 60%;\n        //font-size: 1.5em;\n      }\n\n      &:after {\n        color: $colorFaultItemFgEmphasis;\n        display: block;\n        font-family: symbolsfont;\n        position: absolute;\n        //text-shadow: black 0 0 2px;\n        right: -3px;\n        bottom: -3px;\n        transform-origin: right bottom;\n        transform: scale(0.6);\n      }\n    }\n  }\n\n  &.is-shelved {\n    .c-fault-mgmt__list-pathname {\n      font-style: italic;\n    }\n  }\n\n  &.is-acknowledged .c-fault-mgmt__list-severity:after {\n    content: $glyph-icon-check;\n  }\n\n  &.is-shelved .c-fault-mgmt__list-severity:after {\n    content: $glyph-icon-timer;\n  }\n}\n\n/*********************************************** LIST HEADER */\n.c-fault-mgmt__list-header {\n  display: contents;\n  border-radius: $controlCr;\n  align-items: center;\n\n  * {\n    margin: 0px;\n    border-radius: 0px;\n  }\n\n  .--width-less-than-600 & {\n    .c-fault-mgmt__list-content-right {\n      display: none;\n    }\n  }\n\n  &-content {\n    display: contents;\n  }\n\n  &-results {\n    grid-column: 2 / span 2;\n    font-size: 1em;\n    height: auto;\n  }\n\n  &-action-wrapper {\n    grid-column: 7 / span 2;\n\n    .--width-less-than-600 & {\n      grid-column: 4 / span 2;\n    }\n  }\n}\n\n/*********************************************** GRID ITEM */\n.c-fault-mgmt-item {\n  $p: $interiorMargin;\n  padding: $p;\n  background: $colorFaultItemBg;\n  white-space: nowrap;\n\n  &-header {\n    $c: $colorBodyBg;\n    background: $c;\n    border-bottom: 5px solid $c; // Creates illusion of \"space\" beneath header\n    min-height: 30px; // Needed to align cells\n    padding: $p;\n    position: sticky;\n    top: 0;\n    z-index: 2;\n  }\n\n  &__value {\n    @include isLimit();\n    background: rgba($colorBodyFg, 0.1);\n    padding: $p;\n    border-radius: $controlCr;\n    display: inline-flex;\n  }\n\n  .is-selected & {\n    background: $colorSelectedBg;\n  }\n}\n"
  },
  {
    "path": "src/plugins/faultManagement/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from '../../utils/testing.js';\nimport {\n  FAULT_MANAGEMENT_INSPECTOR,\n  FAULT_MANAGEMENT_NAMESPACE,\n  FAULT_MANAGEMENT_TYPE,\n  FAULT_MANAGEMENT_VIEW\n} from './constants.js';\n\ndescribe('The Fault Management Plugin', () => {\n  let openmct;\n  const faultDomainObject = {\n    name: 'it is not your fault',\n    type: FAULT_MANAGEMENT_TYPE,\n    identifier: {\n      key: 'nobodies',\n      namespace: 'fault'\n    }\n  };\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('is not installed by default', () => {\n    const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;\n\n    expect(typeDef.name).toBe('Unknown Type');\n  });\n\n  it('can be installed', () => {\n    openmct.install(openmct.plugins.FaultManagement());\n    const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;\n\n    expect(typeDef.name).toBe('Fault Management');\n  });\n\n  describe('once it is installed', () => {\n    beforeEach(() => {\n      openmct.install(openmct.plugins.FaultManagement());\n    });\n\n    it('provides a view for fault management types', () => {\n      const applicableViews = openmct.objectViews.get(faultDomainObject, []);\n      const faultManagementView = applicableViews.find(\n        (viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW\n      );\n\n      expect(applicableViews.length).toEqual(1);\n      expect(faultManagementView).toBeDefined();\n    });\n\n    it('provides an inspector view for fault management types', () => {\n      const faultDomainObjectSelection = [\n        [\n          {\n            context: {\n              item: faultDomainObject\n            }\n          }\n        ]\n      ];\n      const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);\n      const faultManagementInspectorView = applicableInspectorViews.filter(\n        (view) => view.key === FAULT_MANAGEMENT_INSPECTOR\n      );\n\n      expect(faultManagementInspectorView.length).toEqual(1);\n    });\n\n    it('creates a root object for fault management', async () => {\n      const root = await openmct.objects.getRoot();\n      const rootCompositionCollection = openmct.composition.get(root);\n      const rootComposition = await rootCompositionCollection.load();\n      const faultObject = rootComposition.find(\n        (obj) => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE\n      );\n\n      expect(faultObject).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/filters/FiltersInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport FiltersView from './components/FiltersView.vue';\n\nconst FILTERS_INSPECTOR_KEY = 'filters-inspector';\nexport default class FiltersInspectorViewProvider {\n  constructor(openmct, supportedObjectTypesArray) {\n    this.openmct = openmct;\n    this.supportedObjectTypesArray = supportedObjectTypesArray;\n    this.key = FILTERS_INSPECTOR_KEY;\n    this.name = 'Filters';\n  }\n  canView(selection) {\n    const domainObject = selection?.[0]?.[0]?.context?.item;\n\n    return (\n      domainObject && this.supportedObjectTypesArray.some((type) => domainObject.type === type)\n    );\n  }\n  view(selection) {\n    let openmct = this.openmct;\n    let _destroy = null;\n\n    return {\n      show: function (element) {\n        const { destroy } = mount(\n          {\n            el: element,\n            components: {\n              FiltersView\n            },\n            provide: {\n              openmct: openmct\n            },\n            template: '<filters-view></filters-view>'\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        _destroy = destroy;\n      },\n      showTab: function (isEditing) {\n        if (isEditing) {\n          return true;\n        }\n      },\n      priority: function () {\n        // Needs to be Default so that filters tab shows up correctly for Overlay Plots\n        return openmct.priority.DEFAULT;\n      },\n      destroy: function () {\n        if (_destroy) {\n          _destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/filters/README.md",
    "content": "\n# Server side filtering in Open MCT\n\n## Introduction\n\nIn Open MCT, filters can be constructed to filter out telemetry data on the server side. This is useful for reducing the amount of data that needs to be sent to the client. For example, in [Open MCT for MCWS](https://github.com/NASA-AMMOS/openmct-mcws/blob/e8846d325cc3f659d8ad58d1d24efaafbe2b6bb7/src/constants.js#L115), they can be used to filter realtime data from recorded data. In the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L44), we can use them to filter incoming event data by severity.\n\n## Installing the filter plugin\n\nYou'll need to install the filter plugin first. For example:\n\n```js\nopenmct.install(openmct.plugins.Filters(['telemetry.plot.overlay', 'table']));\n```\n\nwill install the filters plugin and have it apply to overlay plots and tables. You can see an example of this in the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/example/index.js#L58).\n\n## Defining a filter\n\nTo define a filter, you'll need to add a new `filter` property to the domain object's `telemetry` metadata underneath the `values` array. For example, if you have a domain object with a `telemetry` metadata that looks like this:\n\n```js\n{\n    key: 'fruit',\n    name: 'Types of fruit',\n    filters: [{\n        singleSelectionThreshold: true,\n        comparator: 'equals',\n        possibleValues: [\n            { label: 'Apple', value: 'apple' },\n            { label: 'Banana', value: 'banana' },\n            { label: 'Orange', value: 'orange' }\n        ]\n    }]\n}\n```\n\nThis will define a filter that allows an operator to choose one (due to `singleSelectionThreshold` being `true`) of the three possible values. The `comparator` property defines how the filter will be applied to the telemetry data.\nSetting `singleSelectionThreshold` to `false` will render the `possibleValues` as a series of checkboxes. Removing the `possibleValues` property will render the filter as a text box, allowing the operator to enter a value to filter on.\n\nNote that how the filter is interpreted is ultimately decided by the individual telemetry providers.\n\n## Implementing a filter in a telemetry provider\n\nImplementing a filter requires two parts:\n\n- First, one needs to add the filter implementation to the [subscribe](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L366) method in your telemetry provider. The filter will be passed to you in the `options` argument. You can either add the filter to your telemetry subscription request, or filter manually as new messages appears. An example of the latter is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L95).\n\n- Second, one needs to add the filter implementation to the [request](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L318) method in your telemetry provider. The filter again will be passed to you in the `options` argument. You can either add the filter to your telemetry request, or filter manually after the request is made. An example of the former is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/historical-telemetry-provider.js#L171).\n\n## Using filters\n\nIf you installed the plugin to have it apply to `table`, create a Telemetry Table in Open MCT and drag your telemetry object that contains the filter to it. Then click \"Edit\", and notice the \"Filter\" tab in the inspector. It allows operator to either select a \"Global Filter\", or a regular filter. The \"Global Filter\" will apply for all telemetry objects in the table, while the regular filter will only apply to the telemetry object that it is defined on."
  },
  {
    "path": "src/plugins/filters/components/FilterField.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-inspect-properties__section c-filter-settings\">\n    <li\n      v-for=\"(filter, index) in filterField.filters\"\n      :key=\"index\"\n      class=\"c-inspect-properties__row c-filter-settings__setting\"\n    >\n      <div class=\"c-inspect-properties__label label\" :disabled=\"useGlobal\">\n        {{ filterField.name }} =\n      </div>\n      <div class=\"c-inspect-properties__value value\">\n        <!-- EDITING -->\n        <!-- String input, editing -->\n        <template v-if=\"!filter.possibleValues && isEditing\">\n          <input\n            :id=\"`${filter}filterControl`\"\n            class=\"c-input--flex\"\n            type=\"text\"\n            :aria-label=\"label\"\n            :disabled=\"useGlobal\"\n            :value=\"persistedValue(filter.comparator)\"\n            @change=\"updateFilterValueFromString($event, filter.comparator)\"\n          />\n        </template>\n\n        <!-- Dropdown, editing -->\n        <template v-if=\"filter.possibleValues && filter.singleSelectionThreshold && isEditing\">\n          <select\n            name=\"setSelectionThreshold\"\n            :aria-label=\"label\"\n            :disabled=\"useGlobal\"\n            @change=\"updateFilterValueFromDropdown($event, filter.comparator, $event.target.value)\"\n          >\n            <option key=\"NONE\" value=\"NONE\" selected=\"isSelected(filter.comparator, option.value)\">\n              None\n            </option>\n            <option\n              v-for=\"option in filter.possibleValues\"\n              :key=\"option.label\"\n              :value=\"option.value\"\n              :selected=\"isSelected(filter.comparator, option.value)\"\n            >\n              {{ option.label }}\n            </option>\n          </select>\n        </template>\n\n        <!-- Checkbox list, editing -->\n        <template v-if=\"filter.possibleValues && isEditing && !filter.singleSelectionThreshold\">\n          <div\n            v-for=\"option in filter.possibleValues\"\n            :key=\"option.value\"\n            class=\"c-checkbox-list__row\"\n          >\n            <input\n              :id=\"`${option.value}filterControl`\"\n              class=\"c-checkbox-list__input\"\n              type=\"checkbox\"\n              :aria-label=\"label\"\n              :disabled=\"useGlobal\"\n              :checked=\"isSelected(filter.comparator, option.value)\"\n              @change=\"updateFilterValueFromCheckbox($event, filter.comparator, option.value)\"\n            />\n            <span class=\"c-checkbox-list__value\">\n              {{ option.label }}\n            </span>\n          </div>\n        </template>\n\n        <!-- BROWSING -->\n        <!-- String input, NOT editing -->\n        <template v-if=\"!filter.possibleValues && !isEditing\">\n          {{ persistedValue(filter) }}\n        </template>\n\n        <!-- Checkbox list, NOT editing -->\n        <template v-if=\"filter.possibleValues && !isEditing\">\n          <span v-if=\"persistedFilters[filter.comparator]\">\n            {{ getFilterLabels(filter) }}\n          </span>\n        </template>\n      </div>\n    </li>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    filterField: {\n      type: Object,\n      required: true\n    },\n    label: {\n      type: String,\n      required: true\n    },\n    useGlobal: Boolean,\n    persistedFilters: {\n      type: Object,\n      default: () => {\n        return {};\n      }\n    }\n  },\n  emits: [\n    'filter-text-value-changed',\n    'filter-selected',\n    'clear-filters',\n    'filter-single-selected'\n  ],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.toggleIsEditing);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.toggleIsEditing);\n  },\n  methods: {\n    toggleIsEditing(isEditing) {\n      this.isEditing = isEditing;\n    },\n    isSelected(comparator, value) {\n      if (this.persistedFilters[comparator] && this.persistedFilters[comparator].includes(value)) {\n        return true;\n      } else {\n        return false;\n      }\n    },\n    persistedValue(comparator) {\n      return this.persistedFilters && this.persistedFilters[comparator];\n    },\n    updateFilterValueFromString(event, comparator) {\n      this.$emit('filter-text-value-changed', this.filterField.key, comparator, event.target.value);\n    },\n    updateFilterValueFromCheckbox(event, comparator, value) {\n      this.$emit('filter-selected', this.filterField.key, comparator, value, event.target.checked);\n    },\n    updateFilterValueFromDropdown(event, comparator, value) {\n      if (value === 'NONE') {\n        this.$emit('clear-filters', this.filterField.key);\n      } else {\n        this.$emit('filter-single-selected', this.filterField.key, comparator, value);\n      }\n    },\n    getFilterLabels(filter) {\n      return this.persistedFilters[filter.comparator]\n        .reduce((accum, filterValue) => {\n          accum.push(\n            filter.possibleValues.reduce((label, possibleValue) => {\n              if (filterValue === possibleValue.value) {\n                label = possibleValue.label;\n              }\n\n              return label;\n            }, '')\n          );\n\n          return accum;\n        }, [])\n        .join(', ');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/filters/components/FilterObject.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <li class=\"c-tree__item-h\">\n    <div class=\"c-tree__item menus-to-left\" @click=\"toggleExpanded\">\n      <div\n        class=\"c-filter-tree-item__filter-indicator\"\n        :class=\"{ 'icon-filter': hasActiveFilters }\"\n      ></div>\n      <span\n        class=\"c-disclosure-triangle is-enabled flex-elem\"\n        :class=\"{ 'c-disclosure-triangle--expanded': expanded }\"\n      ></span>\n      <div class=\"c-tree__item__label c-object-label\">\n        <div class=\"c-object-label\">\n          <div class=\"c-object-label__type-icon\" :class=\"objectCssClass\"></div>\n          <div class=\"c-object-label__name flex-elem grows\">\n            {{ filterObject.name }}\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div v-if=\"expanded\">\n      <ul class=\"c-inspect-properties\">\n        <div\n          v-if=\"!isEditing && persistedFilters.useGlobal\"\n          class=\"c-inspect-properties__label span-all\"\n        >\n          Uses global filter\n        </div>\n\n        <div v-if=\"isEditing\" class=\"c-inspect-properties__label span-all\">\n          <ToggleSwitch\n            :id=\"keyString\"\n            :checked=\"persistedFilters.useGlobal\"\n            @change=\"useGlobalFilter\"\n          />\n          Use global filter\n        </div>\n        <FilterField\n          v-for=\"metadatum in activeFilters\"\n          :key=\"metadatum.key\"\n          :filter-field=\"metadatum\"\n          :use-global=\"persistedFilters.useGlobal\"\n          :persisted-filters=\"updatedFilters[metadatum.key]\"\n          label=\"Specific Filter\"\n          @filter-selected=\"updateMultipleFiltersWithSelectedValue\"\n          @filter-text-value-changed=\"updateFiltersWithTextValue\"\n          @filter-single-selected=\"updateSingleSelection\"\n          @clear-filters=\"clearFilters\"\n        />\n      </ul>\n    </div>\n  </li>\n</template>\n\n<script>\nimport isEmpty from 'lodash/isEmpty';\n\nimport ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';\nimport FilterField from './FilterField.vue';\n\nexport default {\n  components: {\n    FilterField,\n    ToggleSwitch\n  },\n  inject: ['openmct'],\n  props: {\n    filterObject: {\n      type: Object,\n      required: true\n    },\n    persistedFilters: {\n      type: Object,\n      default: () => {\n        return {};\n      }\n    }\n  },\n  emits: ['update-filters'],\n  data() {\n    return {\n      expanded: false,\n      objectCssClass: undefined,\n      updatedFilters: JSON.parse(JSON.stringify(this.persistedFilters)),\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  computed: {\n    // do not show filter fields if using global filter\n    // if editing however, show all filter fields\n    activeFilters() {\n      if (!this.isEditing && this.persistedFilters.useGlobal) {\n        return [];\n      }\n\n      return this.filterObject.metadataWithFilters;\n    },\n    hasActiveFilters() {\n      // Should be true when the user has entered any filter values.\n      return Object.values(this.persistedFilters).some((comparator) => {\n        return typeof comparator === 'object' && !isEmpty(comparator);\n      });\n    }\n  },\n  watch: {\n    persistedFilters: {\n      handler: function checkFilters(newpersistedFilters) {\n        this.updatedFilters = JSON.parse(JSON.stringify(newpersistedFilters));\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    let type = this.openmct.types.get(this.filterObject.domainObject.type) || {};\n    this.keyString = this.openmct.objects.makeKeyString(this.filterObject.domainObject.identifier);\n    this.objectCssClass = type.definition.cssClass;\n    this.openmct.editor.on('isEditing', this.toggleIsEditing);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.toggleIsEditing);\n  },\n  methods: {\n    toggleExpanded() {\n      this.expanded = !this.expanded;\n    },\n    updateMultipleFiltersWithSelectedValue(key, comparator, valueName, value) {\n      let filterValue = this.updatedFilters[key];\n\n      if (filterValue[comparator]) {\n        if (value === true) {\n          filterValue[comparator].push(valueName);\n        } else {\n          if (filterValue[comparator].length === 1) {\n            this.updatedFilters[key] = {};\n          } else {\n            filterValue[comparator] = filterValue[comparator].filter((v) => v !== valueName);\n          }\n        }\n      } else {\n        this.updatedFilters[key][comparator] = [valueName];\n      }\n\n      this.$emit('update-filters', this.keyString, this.updatedFilters);\n    },\n    clearFilters(key) {\n      this.updatedFilters[key] = {};\n      this.$emit('update-filters', this.keyString, this.updatedFilters);\n    },\n    updateFiltersWithTextValue(key, comparator, value) {\n      if (value.trim() === '') {\n        this.updatedFilters[key] = {};\n      } else {\n        this.updatedFilters[key][comparator] = value;\n      }\n\n      this.$emit('update-filters', this.keyString, this.updatedFilters);\n    },\n    updateSingleSelection(key, comparator, value) {\n      this.updatedFilters[key][comparator] = [value];\n      this.$emit('update-filters', this.keyString, this.updatedFilters);\n    },\n    useGlobalFilter(checked) {\n      this.updatedFilters.useGlobal = checked;\n      this.$emit('update-filters', this.keyString, this.updatedFilters, checked);\n    },\n    toggleIsEditing(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/filters/components/FiltersView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul v-if=\"Object.keys(children).length\" class=\"c-tree c-filter-tree\">\n    <h2>Data Filters</h2>\n    <div v-if=\"hasActiveFilters\" class=\"c-filter-indication\">\n      {{ label }}\n    </div>\n    <GlobalFilters\n      :global-filters=\"globalFilters\"\n      :global-metadata=\"globalMetadata\"\n      @persist-global-filters=\"persistGlobalFilters\"\n    />\n    <FilterObject\n      v-for=\"(child, key) in children\"\n      :key=\"key\"\n      :filter-object=\"child\"\n      :persisted-filters=\"persistedFilters[key]\"\n      @update-filters=\"persistFilters\"\n    />\n  </ul>\n  <span v-else>\n    This view doesn't include any parameters that have configured filter criteria.\n  </span>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { toRaw } from 'vue';\n\nimport FilterObject from './FilterObject.vue';\nimport GlobalFilters from './GlobalFilters.vue';\n\nconst FILTER_VIEW_TITLE = 'Filters applied';\nconst FILTER_VIEW_TITLE_MIXED = 'Mixed filters applied';\nconst USE_GLOBAL = 'useGlobal';\n\nexport default {\n  components: {\n    FilterObject,\n    GlobalFilters\n  },\n  inject: ['openmct'],\n  data() {\n    let providedObject = this.openmct.selection.get()[0][0].context.item;\n    let configuration = providedObject.configuration;\n\n    return {\n      persistedFilters: (configuration && configuration.filters) || {},\n      globalFilters: (configuration && configuration.globalFilters) || {},\n      globalMetadata: {},\n      providedObject,\n      children: {}\n    };\n  },\n  computed: {\n    hasActiveFilters() {\n      // Should be true when the user has entered any filter values.\n      return Object.values(this.persistedFilters).some((filters) => {\n        return Object.values(filters).some((comparator) => {\n          return typeof comparator === 'object' && !_.isEmpty(comparator);\n        });\n      });\n    },\n    hasMixedFilters() {\n      // Should be true when filter values are mixed.\n      let filtersToCompare = _.omit(this.persistedFilters[Object.keys(this.persistedFilters)[0]], [\n        USE_GLOBAL\n      ]);\n\n      return Object.values(this.persistedFilters).some((filters) => {\n        return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL]));\n      });\n    },\n    label() {\n      if (this.hasActiveFilters) {\n        if (this.hasMixedFilters) {\n          return FILTER_VIEW_TITLE_MIXED;\n        } else {\n          return FILTER_VIEW_TITLE;\n        }\n      }\n\n      return '';\n    }\n  },\n  mounted() {\n    this.composition = this.openmct.composition.get(this.providedObject);\n    this.composition.on('add', this.addChildren);\n    this.composition.on('remove', this.removeChildren);\n    this.composition.load();\n    this.unobserve = this.openmct.objects.observe(\n      this.providedObject,\n      'configuration.filters',\n      this.updatePersistedFilters\n    );\n    this.unobserveGlobalFilters = this.openmct.objects.observe(\n      this.providedObject,\n      'configuration.globalFilters',\n      this.updateGlobalFilters\n    );\n  },\n  beforeUnmount() {\n    this.composition.off('add', this.addChildren);\n    this.composition.off('remove', this.removeChildren);\n    this.unobserve();\n    this.unobserveGlobalFilters();\n  },\n  methods: {\n    addChildren(domainObject) {\n      let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      let metadata = this.openmct.telemetry.getMetadata(domainObject);\n      let metadataWithFilters = metadata\n        ? metadata.valueMetadatas.filter((value) => value.filters)\n        : [];\n      let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;\n      let mutateFilters = false;\n      let childObject = {\n        name: domainObject.name,\n        domainObject: domainObject,\n        metadataWithFilters\n      };\n\n      if (metadataWithFilters.length) {\n        this.children[keyString] = childObject;\n\n        metadataWithFilters.forEach((metadatum) => {\n          if (!this.globalFilters[metadatum.key]) {\n            this.globalFilters[metadatum.key] = {};\n          }\n\n          if (!this.globalMetadata[metadatum.key]) {\n            this.globalMetadata[metadatum.key] = metadatum;\n          }\n\n          if (!hasFiltersWithKeyString) {\n            if (!this.persistedFilters[keyString]) {\n              this.persistedFilters[keyString] = {};\n              this.persistedFilters[keyString].useGlobal = true;\n              mutateFilters = true;\n            }\n\n            this.persistedFilters[keyString][metadatum.key] = this.globalFilters[metadatum.key];\n          }\n        });\n      }\n\n      if (mutateFilters) {\n        this.mutateConfigurationFilters();\n      }\n    },\n    removeChildren(identifier) {\n      let keyString = this.openmct.objects.makeKeyString(identifier);\n      let globalFiltersToRemove = this.getGlobalFiltersToRemove(keyString);\n\n      if (globalFiltersToRemove.length > 0) {\n        globalFiltersToRemove.forEach((key) => {\n          delete this.globalFilters[key];\n          delete this.globalMetadata[key];\n        });\n        this.mutateConfigurationGlobalFilters();\n      }\n\n      delete this.children[keyString];\n      delete this.persistedFilters[keyString];\n      this.mutateConfigurationFilters();\n    },\n    getGlobalFiltersToRemove(keyString) {\n      let filtersToRemove = new Set();\n      const child = this.children[keyString];\n      if (child && child.metadataWithFilters) {\n        const metadataWithFilters = child.metadataWithFilters;\n        metadataWithFilters.forEach((metadatum) => {\n          let keepFilter = false;\n          Object.keys(this.children).forEach((childKeyString) => {\n            if (childKeyString !== keyString) {\n              let filterMatched = this.children[childKeyString].metadataWithFilters.some(\n                (childMetadatum) => childMetadatum.key === metadatum.key\n              );\n\n              if (filterMatched) {\n                keepFilter = true;\n\n                return;\n              }\n            }\n          });\n\n          if (!keepFilter) {\n            filtersToRemove.add(metadatum.key);\n          }\n        });\n      }\n\n      return Array.from(filtersToRemove);\n    },\n    persistFilters(keyString, updatedFilters, useGlobalValues) {\n      this.persistedFilters[keyString] = updatedFilters;\n\n      if (useGlobalValues) {\n        Object.keys(this.persistedFilters[keyString]).forEach((key) => {\n          if (typeof this.persistedFilters[keyString][key] === 'object') {\n            this.persistedFilters[keyString][key] = this.globalFilters[key];\n          }\n        });\n      }\n\n      this.mutateConfigurationFilters();\n    },\n    updatePersistedFilters(filters) {\n      this.persistedFilters = filters;\n    },\n    persistGlobalFilters(key, filters) {\n      this.globalFilters[key] = filters[key];\n      this.mutateConfigurationGlobalFilters();\n      let mutateFilters = false;\n\n      Object.keys(this.children).forEach((keyString) => {\n        if (\n          this.persistedFilters[keyString].useGlobal !== false &&\n          this.containsField(keyString, key)\n        ) {\n          if (!this.persistedFilters[keyString][key]) {\n            this.persistedFilters[keyString][key] = {};\n          }\n\n          this.persistedFilters[keyString][key] = filters[key];\n          mutateFilters = true;\n        }\n      });\n\n      if (mutateFilters) {\n        this.mutateConfigurationFilters();\n      }\n    },\n    updateGlobalFilters(filters) {\n      this.globalFilters = filters;\n    },\n    containsField(keyString, field) {\n      let hasField = false;\n      this.children[keyString].metadataWithFilters.forEach((metadatum) => {\n        if (metadatum.key === field) {\n          hasField = true;\n\n          return;\n        }\n      });\n\n      return hasField;\n    },\n    mutateConfigurationFilters() {\n      this.openmct.objects.mutate(\n        this.providedObject,\n        'configuration.filters',\n        toRaw(this.persistedFilters)\n      );\n    },\n    mutateConfigurationGlobalFilters() {\n      this.openmct.objects.mutate(\n        this.providedObject,\n        'configuration.globalFilters',\n        toRaw(this.globalFilters)\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/filters/components/GlobalFilters.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <li class=\"c-tree__item-h\">\n    <div class=\"c-tree__item menus-to-left\" @click=\"toggleExpanded\">\n      <div\n        class=\"c-filter-tree-item__filter-indicator\"\n        :class=\"{ 'icon-filter': hasActiveGlobalFilters }\"\n      ></div>\n      <span\n        class=\"c-disclosure-triangle is-enabled flex-elem\"\n        :class=\"{ 'c-disclosure-triangle--expanded': expanded }\"\n      ></span>\n      <div class=\"c-tree__item__label c-object-label\">\n        <div class=\"c-object-label\">\n          <div class=\"c-object-label__type-icon icon-gear\"></div>\n          <div class=\"c-object-label__name flex-elem grows\">Global Filtering</div>\n        </div>\n      </div>\n    </div>\n    <ul v-if=\"expanded\" class=\"c-inspect-properties\">\n      <FilterField\n        v-for=\"metadatum in globalMetadata\"\n        :key=\"metadatum.key\"\n        :filter-field=\"metadatum\"\n        :persisted-filters=\"updatedFilters[metadatum.key]\"\n        label=\"Global Filter\"\n        @filter-selected=\"updateFiltersWithSelectedValue\"\n        @filter-text-value-changed=\"updateFiltersWithTextValue\"\n        @filter-single-selected=\"updateSingleSelection\"\n        @clear-filters=\"clearFilters\"\n      />\n    </ul>\n  </li>\n</template>\n\n<script>\nimport FilterField from './FilterField.vue';\n\nexport default {\n  components: {\n    FilterField\n  },\n  inject: ['openmct'],\n  props: {\n    globalMetadata: {\n      type: Object,\n      required: true\n    },\n    globalFilters: {\n      type: Object,\n      default: () => {\n        return {};\n      }\n    }\n  },\n  emits: ['persist-global-filters'],\n  data() {\n    return {\n      expanded: false,\n      updatedFilters: JSON.parse(JSON.stringify(this.globalFilters))\n    };\n  },\n  computed: {\n    hasActiveGlobalFilters() {\n      return Object.values(this.globalFilters).some((field) => {\n        return Object.values(field).some((comparator) => {\n          return comparator && (comparator !== '' || comparator.length > 0);\n        });\n      });\n    }\n  },\n  watch: {\n    globalFilters: {\n      handler: function checkFilters(newGlobalFilters) {\n        this.updatedFilters = JSON.parse(JSON.stringify(newGlobalFilters));\n      },\n      deep: true\n    }\n  },\n  methods: {\n    toggleExpanded() {\n      this.expanded = !this.expanded;\n    },\n    clearFilters(key) {\n      this.updatedFilters[key] = {};\n      this.$emit('persist-global-filters', key, this.updatedFilters);\n    },\n    updateFiltersWithSelectedValue(key, comparator, valueName, value) {\n      let filterValue = this.updatedFilters[key];\n\n      if (filterValue[comparator]) {\n        if (value === true) {\n          filterValue[comparator].push(valueName);\n        } else {\n          if (filterValue[comparator].length === 1) {\n            this.updatedFilters[key] = {};\n          } else {\n            filterValue[comparator] = filterValue[comparator].filter((v) => v !== valueName);\n          }\n        }\n      } else {\n        this.updatedFilters[key][comparator] = [valueName];\n      }\n\n      this.$emit('persist-global-filters', key, this.updatedFilters);\n    },\n    updateSingleSelection(key, comparator, value) {\n      this.updatedFilters[key][comparator] = [value];\n      this.$emit('persist-global-filters', key, this.updatedFilters);\n    },\n    updateFiltersWithTextValue(key, comparator, value) {\n      if (value.trim() === '') {\n        this.updatedFilters[key] = {};\n      } else {\n        this.updatedFilters[key][comparator] = value;\n      }\n\n      this.$emit('persist-global-filters', key, this.updatedFilters);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/filters/components/filters-view.scss",
    "content": ".c-inspector {\n  .c-filter-indication {\n    border-radius: $smallCr;\n    font-size: inherit;\n    padding: $interiorMarginSm $interiorMargin;\n    text-transform: inherit;\n  }\n  .c-filter-tree {\n    // Filters UI uses a tree-based structure\n    .c-inspect-properties {\n      // Add extra margin to account for filter-indicator\n      margin-left: 38px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/filters/components/global-filters.scss",
    "content": ".c-filter-indication {\n  // Appears as a block element beneath tables\n  @include userSelectNone();\n  background: $colorFilterBg;\n  color: $colorFilterFg;\n\n  &:before {\n    font-family: symbolsfont-12px;\n    content: $glyph-icon-filter;\n    margin-right: $interiorMarginSm;\n  }\n\n  &--mixed {\n    .c-filter-indication__mixed {\n      font-style: italic;\n    }\n  }\n\n  &__label {\n    + .c-filter-indication__label {\n      &:before {\n        content: ', ';\n      }\n    }\n  }\n}\n\n.c-filter-tree-item {\n  &__filter-indicator {\n    color: $colorFilter;\n    width: 1.2em; // Set width explicitly for layout reasons: will either have class icon-filter, or none.\n    flex: 0 0 auto;\n  }\n}\n"
  },
  {
    "path": "src/plugins/filters/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport FiltersInspectorViewProvider from './FiltersInspectorViewProvider.js';\n\nexport default function plugin(supportedObjectTypesArray) {\n  return function install(openmct) {\n    openmct.inspectorViews.addProvider(\n      new FiltersInspectorViewProvider(openmct, supportedObjectTypesArray)\n    );\n  };\n}\n"
  },
  {
    "path": "src/plugins/flexibleLayout/components/ContainerComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-fl-container\"\n    :style=\"[{ 'flex-basis': sizeString }]\"\n    :class=\"{ 'is-empty': !frames.length }\"\n    role=\"grid\"\n  >\n    <div\n      v-show=\"isEditing\"\n      class=\"c-fl-container__header\"\n      draggable=\"true\"\n      role=\"columnheader\"\n      :aria-label=\"`Container Handle ${index + 1}`\"\n      @dragstart=\"startContainerDrag\"\n    >\n      <span class=\"c-fl-container__size-indicator\">{{ sizeString }}</span>\n    </div>\n\n    <DropHint\n      class=\"c-fl-frame__drop-hint\"\n      :index=\"-1\"\n      :allow-drop=\"allowDrop\"\n      @object-drop-to=\"moveOrCreateNewFrame\"\n    />\n\n    <div role=\"row\" class=\"c-fl-container__frames-holder\" :class=\"flexLayoutCssClass\">\n      <template v-for=\"(frame, i) in frames\" :key=\"frame.id\">\n        <FrameComponent\n          class=\"c-fl-container__frame\"\n          :frame=\"frame\"\n          :index=\"i\"\n          role=\"gridcell\"\n          :aria-label=\"`Container Frame ${index}`\"\n          :container-index=\"index\"\n          :is-editing=\"isEditing\"\n          :object-path=\"objectPath\"\n        />\n\n        <DropHint\n          class=\"c-fl-frame__drop-hint\"\n          :index=\"i\"\n          :allow-drop=\"allowDrop\"\n          @object-drop-to=\"moveOrCreateNewFrame\"\n        />\n\n        <ResizeHandle\n          v-if=\"i !== frames.length - 1\"\n          :index=\"i\"\n          :drag-orientation=\"rowsLayout ? 'horizontal' : 'vertical'\"\n          :is-editing=\"isEditing\"\n          @init-move=\"startFrameResizing\"\n          @move=\"frameResizing\"\n          @end-move=\"endFrameResizing\"\n        />\n      </template>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';\n\nimport DropHint from './DropHint.vue';\nimport FrameComponent from './FrameComponent.vue';\n\nconst MIN_FRAME_SIZE = 5;\n\nexport default {\n  components: {\n    FrameComponent,\n    ResizeHandle,\n    DropHint\n  },\n  inject: ['openmct'],\n  props: {\n    container: {\n      type: Object,\n      required: true\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    rowsLayout: Boolean,\n    isEditing: {\n      type: Boolean,\n      default: false\n    },\n    locked: {\n      type: Boolean,\n      default: false\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  emits: ['new-frame', 'move-frame', 'persist'],\n  computed: {\n    flexLayoutCssClass() {\n      return this.rowsLayout ? '--layout-rows' : '--layout-cols';\n    },\n    frames() {\n      return this.container.frames;\n    },\n    sizeString() {\n      return `${Math.round(this.container.size)}%`;\n    }\n  },\n  mounted() {\n    let context = {\n      item: this.$parent.domainObject,\n      type: 'container',\n      containerId: this.container.id\n    };\n\n    this.unsubscribeSelection = this.openmct.selection.selectable(this.$el, context, false);\n  },\n  beforeUnmount() {\n    this.unsubscribeSelection();\n  },\n  methods: {\n    allowDrop(event, index) {\n      if (this.locked) {\n        return false;\n      }\n\n      if (event.dataTransfer.types.includes('openmct/domain-object-path')) {\n        return true;\n      }\n\n      let frameId = event.dataTransfer.getData('frameid');\n      let containerIndex = Number(event.dataTransfer.getData('containerIndex'));\n\n      if (!frameId) {\n        return false;\n      }\n\n      if (containerIndex === this.index) {\n        let frame = this.container.frames.filter((f) => f.id === frameId)[0];\n        let framePos = this.container.frames.indexOf(frame);\n\n        if (index === -1) {\n          return framePos !== 0;\n        } else {\n          return framePos !== index && framePos - 1 !== index;\n        }\n      } else {\n        return true;\n      }\n    },\n    moveOrCreateNewFrame(insertIndex, event) {\n      if (event.dataTransfer.types.includes('openmct/domain-object-path')) {\n        this.$emit('new-frame', this.index, insertIndex);\n\n        return;\n      }\n\n      // move frame.\n      let frameId = event.dataTransfer.getData('frameid');\n      let containerIndex = Number(event.dataTransfer.getData('containerIndex'));\n      this.$emit('move-frame', this.index, insertIndex, frameId, containerIndex);\n    },\n    startFrameResizing(index) {\n      let beforeFrame = this.frames[index];\n      let afterFrame = this.frames[index + 1];\n\n      this.maxMoveSize = beforeFrame.size + afterFrame.size;\n    },\n    frameResizing(index, delta, event) {\n      let percentageMoved = Math.round((delta / this.getElSize()) * 100);\n      let beforeFrame = this.frames[index];\n      let afterFrame = this.frames[index + 1];\n\n      beforeFrame.size = this.getFrameSize(beforeFrame.size + percentageMoved);\n      afterFrame.size = this.getFrameSize(afterFrame.size - percentageMoved);\n    },\n    endFrameResizing(index, event) {\n      this.persist();\n    },\n    getElSize() {\n      if (this.rowsLayout) {\n        return this.$el.offsetWidth;\n      } else {\n        return this.$el.offsetHeight;\n      }\n    },\n    getFrameSize(size) {\n      if (size < MIN_FRAME_SIZE) {\n        return MIN_FRAME_SIZE;\n      } else if (size > this.maxMoveSize - MIN_FRAME_SIZE) {\n        return this.maxMoveSize - MIN_FRAME_SIZE;\n      } else {\n        return size;\n      }\n    },\n    persist() {\n      this.$emit('persist', this.index);\n    },\n    startContainerDrag(event) {\n      event.dataTransfer.setData('containerid', this.container.id);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/flexibleLayout/components/DropHint.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div v-show=\"isValidTarget\">\n    <div\n      class=\"c-drop-hint c-drop-hint--always-show\"\n      :class=\"{ 'is-mouse-over': isMouseOver }\"\n      @dragover.prevent\n      @dragenter=\"dragenter\"\n      @dragleave=\"dragleave\"\n      @drop=\"dropHandler\"\n    ></div>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    index: {\n      type: Number,\n      required: true\n    },\n    allowDrop: {\n      type: Function,\n      required: true\n    }\n  },\n  emits: ['object-drop-to'],\n  data() {\n    return {\n      isMouseOver: false,\n      isValidTarget: false\n    };\n  },\n  mounted() {\n    document.addEventListener('dragstart', this.dragstart);\n    document.addEventListener('dragend', this.dragend);\n    document.addEventListener('drop', this.dragend);\n  },\n  unmounted() {\n    document.removeEventListener('dragstart', this.dragstart);\n    document.removeEventListener('dragend', this.dragend);\n    document.removeEventListener('drop', this.dragend);\n  },\n  methods: {\n    dragenter() {\n      this.isMouseOver = true;\n    },\n    dragleave() {\n      this.isMouseOver = false;\n    },\n    dropHandler(event) {\n      this.$emit('object-drop-to', this.index, event);\n      this.isValidTarget = false;\n    },\n    dragstart(event) {\n      this.isValidTarget = this.allowDrop(event, this.index);\n    },\n    dragend() {\n      this.isValidTarget = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/flexibleLayout/components/FlexibleLayout.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-fl\">\n    <div id=\"js-fl-drag-ghost\" class=\"c-fl__drag-ghost\"></div>\n\n    <div v-if=\"allContainersAreEmpty\" class=\"c-fl__empty\">\n      <span class=\"c-fl__empty-message\">This Flexible Layout is currently empty</span>\n    </div>\n\n    <div\n      class=\"c-fl__container-holder u-style-receiver js-style-receiver\"\n      :class=\"flexLayoutCssClass\"\n      :aria-label=\"`Flexible Layout ${rowsLayout ? 'Rows' : 'Columns'}`\"\n    >\n      <template v-for=\"(container, index) in containers\" :key=\"`component-${container.id}`\">\n        <DropHint\n          v-if=\"index === 0 && containers.length > 1\"\n          class=\"c-fl-frame__drop-hint\"\n          :index=\"-1\"\n          :allow-drop=\"allowContainerDrop\"\n          @object-drop-to=\"moveContainer\"\n        />\n\n        <ContainerComponent\n          :index=\"index\"\n          :container=\"container\"\n          :rows-layout=\"rowsLayout\"\n          :is-editing=\"isEditing\"\n          :locked=\"domainObject.locked\"\n          :object-path=\"objectPath\"\n          @move-frame=\"moveFrame\"\n          @new-frame=\"setFrameLocation\"\n          @persist=\"persist\"\n        />\n\n        <ResizeHandle\n          v-if=\"index !== containers.length - 1\"\n          :index=\"index\"\n          :drag-orientation=\"rowsLayout ? 'vertical' : 'horizontal'\"\n          :is-editing=\"isEditing\"\n          @init-move=\"startContainerResizing\"\n          @move=\"containerResizing\"\n          @end-move=\"endContainerResizing\"\n        />\n\n        <DropHint\n          v-if=\"containers.length > 1\"\n          class=\"c-fl-frame__drop-hint\"\n          :index=\"index\"\n          :allow-drop=\"allowContainerDrop\"\n          @object-drop-to=\"moveContainer\"\n        />\n      </template>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Container from '@/ui/layout/Container.js';\nimport Frame from '@/ui/layout/Frame.js';\nimport ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';\n\nimport ContainerComponent from './ContainerComponent.vue';\nimport DropHint from './DropHint.vue';\n\nconst MIN_CONTAINER_SIZE = 5;\n\n// Resize items so that newItem fits proportionally (newItem must be an element\n// of items).  If newItem does not have a size or is sized at 100%, newItem will\n// have size set to 1/n * 100, where n is the total number of items.\nfunction sizeItems(items, newItem) {\n  if (items.length === 1) {\n    newItem.size = 100;\n  } else {\n    if (!newItem.size || newItem.size === 100) {\n      newItem.size = Math.round(100 / items.length);\n    }\n\n    let oldItems = items.filter((item) => item !== newItem);\n    // Resize oldItems to fit inside remaining space;\n    let remainder = 100 - newItem.size;\n    oldItems.forEach((item) => {\n      item.size = Math.round((item.size * remainder) / 100);\n    });\n    // Ensure items add up to 100 in case of rounding error.\n    let total = items.reduce((t, item) => t + item.size, 0);\n    let excess = Math.round(100 - total);\n    oldItems[oldItems.length - 1].size += excess;\n  }\n}\n\n// Scales items proportionally so total is equal to 100.  Assumes that an item\n// was removed from array.\nfunction sizeToFill(items) {\n  if (items.length === 0) {\n    return;\n  }\n\n  let oldTotal = items.reduce((total, item) => total + item.size, 0);\n  items.forEach((item) => {\n    item.size = Math.round((item.size * 100) / oldTotal);\n  });\n  // Ensure items add up to 100 in case of rounding error.\n  let total = items.reduce((t, item) => t + item.size, 0);\n  let excess = Math.round(100 - total);\n  items[items.length - 1].size += excess;\n}\n\nexport default {\n  components: {\n    ContainerComponent,\n    ResizeHandle,\n    DropHint\n  },\n  inject: ['openmct', 'objectPath', 'domainObject'],\n  props: {\n    isEditing: Boolean\n  },\n  data() {\n    return {\n      newFrameLocation: [],\n      identifierMap: {},\n      containers: this.domainObject.configuration.containers,\n      rowsLayout: this.domainObject.configuration.rowsLayout\n    };\n  },\n  computed: {\n    allContainersAreEmpty() {\n      return this.containers.every((container) => container.frames.length === 0);\n    },\n    flexLayoutCssClass() {\n      return this.rowsLayout ? 'c-fl--rows' : 'c-fl--cols';\n    }\n  },\n  created() {\n    this.buildIdentifierMap();\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('remove', this.removeChildObject);\n    this.composition.on('add', this.addFrame);\n    this.composition.load();\n\n    this.unObserveContainers = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.containers',\n      (containers) => {\n        this.containers = containers;\n      }\n    );\n    this.unObserveRowsLayout = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.rowsLayout',\n      (rowsLayout) => {\n        this.rowsLayout = rowsLayout;\n      }\n    );\n  },\n  beforeUnmount() {\n    this.composition.off('remove', this.removeChildObject);\n    this.composition.off('add', this.addFrame);\n\n    if (this.unObserveContainers) {\n      this.unObserveContainers();\n    }\n\n    if (this.unObserveRowsLayout) {\n      this.unObserveRowsLayout();\n    }\n  },\n  methods: {\n    containsObject(identifier) {\n      if ('composition' in this.domainObject) {\n        return this.domainObject.composition.some((childId) =>\n          this.openmct.objects.areIdsEqual(childId, identifier)\n        );\n      }\n\n      return false;\n    },\n    buildIdentifierMap() {\n      this.containers.forEach((container) => {\n        container.frames.forEach((frame) => {\n          if (!this.containsObject(frame.domainObjectIdentifier)) {\n            this.removeChildObject(frame.domainObjectIdentifier);\n\n            return;\n          }\n\n          const keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);\n          this.identifierMap[keystring] = true;\n        });\n      });\n    },\n    addContainer() {\n      let container = new Container();\n      this.containers.push(container);\n      sizeItems(this.containers, container);\n      this.persist();\n    },\n    deleteContainer(containerId) {\n      let container = this.containers.filter((c) => c.id === containerId)[0];\n      let containerIndex = this.containers.indexOf(container);\n\n      // remove associated domainObjects from composition\n      container.frames.forEach((f) => {\n        this.removeFromComposition(f.domainObjectIdentifier);\n      });\n\n      this.containers.splice(containerIndex, 1);\n\n      // add a container when there are no containers in the FL,\n      // to prevent user from not being able to add a frame via\n      // drag and drop.\n      if (this.containers.length === 0) {\n        this.containers.push(new Container(100));\n      }\n\n      sizeToFill(this.containers);\n      this.setSelectionToParent();\n      this.persist();\n    },\n    moveFrame(toContainerIndex, toFrameIndex, frameId, fromContainerIndex) {\n      let toContainer = this.containers[toContainerIndex];\n      let fromContainer = this.containers[fromContainerIndex];\n      let frame = fromContainer.frames.filter((f) => f.id === frameId)[0];\n      let fromIndex = fromContainer.frames.indexOf(frame);\n      fromContainer.frames.splice(fromIndex, 1);\n      sizeToFill(fromContainer.frames);\n      toContainer.frames.splice(toFrameIndex + 1, 0, frame);\n      sizeItems(toContainer.frames, frame);\n      this.persist();\n    },\n    setFrameLocation(containerIndex, insertFrameIndex) {\n      this.newFrameLocation = [containerIndex, insertFrameIndex];\n    },\n    addFrame(domainObject) {\n      let keystring = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n      if (!this.identifierMap[keystring]) {\n        let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0;\n        let container = this.containers[containerIndex];\n        let frameIndex = this.newFrameLocation.length\n          ? this.newFrameLocation[1]\n          : container.frames.length;\n        let frame = new Frame(domainObject.identifier);\n\n        container.frames.splice(frameIndex + 1, 0, frame);\n        sizeItems(container.frames, frame);\n\n        this.newFrameLocation = [];\n        this.persist(containerIndex);\n        this.identifierMap[keystring] = true;\n      }\n    },\n    deleteFrame(frameId) {\n      let container = this.containers.filter((c) => c.frames.some((f) => f.id === frameId))[0];\n      let frame = container.frames.filter((f) => f.id === frameId)[0];\n\n      this.removeFromComposition(frame.domainObjectIdentifier);\n\n      this.$nextTick().then(() => {\n        sizeToFill(container.frames);\n        this.setSelectionToParent();\n      });\n    },\n    removeFromComposition(identifier) {\n      this.composition.remove({ identifier });\n    },\n    setSelectionToParent() {\n      this.$el.click();\n    },\n    allowContainerDrop(event, index) {\n      if (!event.dataTransfer.types.includes('containerid')) {\n        return false;\n      }\n\n      if (!this.isEditing) {\n        return false;\n      }\n\n      let containerId = event.dataTransfer.getData('containerid');\n      let container = this.containers.filter((c) => c.id === containerId)[0];\n      let containerPos = this.containers.indexOf(container);\n\n      if (index === -1) {\n        return containerPos !== 0;\n      } else {\n        return containerPos !== index && containerPos - 1 !== index;\n      }\n    },\n    persist(index) {\n      this.startTransaction();\n      if (index) {\n        this.openmct.objects.mutate(\n          this.domainObject,\n          `configuration.containers[${index}]`,\n          this.containers[index]\n        );\n      } else {\n        this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);\n      }\n\n      return this.endTransaction();\n    },\n    startContainerResizing(index) {\n      let beforeContainer = this.containers[index];\n      let afterContainer = this.containers[index + 1];\n\n      this.maxMoveSize = beforeContainer.size + afterContainer.size;\n    },\n    containerResizing(index, delta, event) {\n      let percentageMoved = Math.round((delta / this.getElSize()) * 100);\n      let beforeContainer = this.containers[index];\n      let afterContainer = this.containers[index + 1];\n\n      beforeContainer.size = this.getContainerSize(beforeContainer.size + percentageMoved);\n      afterContainer.size = this.getContainerSize(afterContainer.size - percentageMoved);\n    },\n    endContainerResizing(event) {\n      this.persist();\n    },\n    getElSize() {\n      if (this.rowsLayout) {\n        return this.$el.offsetHeight;\n      } else {\n        return this.$el.offsetWidth;\n      }\n    },\n    getContainerSize(size) {\n      if (size < MIN_CONTAINER_SIZE) {\n        return MIN_CONTAINER_SIZE;\n      } else if (size > this.maxMoveSize - MIN_CONTAINER_SIZE) {\n        return this.maxMoveSize - MIN_CONTAINER_SIZE;\n      } else {\n        return size;\n      }\n    },\n    updateDomainObject(newDomainObject) {\n      this.domainObject = newDomainObject;\n    },\n    moveContainer(toIndex, event) {\n      let containerId = event.dataTransfer.getData('containerid');\n      let container = this.containers.filter((c) => c.id === containerId)[0];\n      let fromIndex = this.containers.indexOf(container);\n      this.containers.splice(fromIndex, 1);\n      if (fromIndex > toIndex) {\n        this.containers.splice(toIndex + 1, 0, container);\n      } else {\n        this.containers.splice(toIndex, 0, container);\n      }\n\n      this.persist();\n    },\n    removeChildObject(identifier) {\n      let removeIdentifier = this.openmct.objects.makeKeyString(identifier);\n\n      this.identifierMap[removeIdentifier] = undefined;\n      delete this.identifierMap[removeIdentifier];\n\n      this.containers.forEach((container) => {\n        container.frames = container.frames.filter((frame) => {\n          let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);\n\n          return removeIdentifier !== frameIdentifier;\n        });\n      });\n\n      this.persist();\n    },\n    startTransaction() {\n      if (!this.openmct.objects.isTransactionActive()) {\n        this.transaction = this.openmct.objects.startTransaction();\n      }\n    },\n    async endTransaction() {\n      if (!this.transaction) {\n        return;\n      }\n\n      await this.transaction.commit();\n      this.openmct.objects.endTransaction();\n      this.transaction = null;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/flexibleLayout/components/FrameComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-fl-frame\"\n    :style=\"{\n      'flex-basis': `${frame.size}%`\n    }\"\n  >\n    <div\n      ref=\"frame\"\n      class=\"c-frame c-fl-frame__drag-wrapper is-selectable u-inspectable is-moveable\"\n      :draggable=\"draggable\"\n      :aria-label=\"frameLabel\"\n      role=\"group\"\n      @dragstart=\"initDrag\"\n    >\n      <ObjectFrame\n        v-if=\"domainObject\"\n        ref=\"objectFrame\"\n        :domain-object=\"domainObject\"\n        :object-path=\"currentObjectPath\"\n        :has-frame=\"hasFrame\"\n        :show-edit-view=\"false\"\n      />\n\n      <div\n        v-if=\"isEditing\"\n        v-show=\"frame.size && frame.size < 100\"\n        class=\"c-fl-frame__size-indicator\"\n      >\n        {{ frame.size }}%\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ObjectFrame from '../../../ui/components/ObjectFrame.vue';\n\nexport default {\n  components: {\n    ObjectFrame\n  },\n  inject: ['openmct'],\n  props: {\n    frame: {\n      type: Object,\n      required: true\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    containerIndex: {\n      type: Number,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      default: false\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  data() {\n    return {\n      domainObject: undefined,\n      currentObjectPath: undefined\n    };\n  },\n  computed: {\n    hasFrame() {\n      return !this.frame.noFrame;\n    },\n    draggable() {\n      return this.isEditing;\n    },\n    frameLabel() {\n      return `${this.domainObject?.name} Frame` || 'Frame';\n    }\n  },\n  mounted() {\n    if (this.frame.domainObjectIdentifier) {\n      if (this.openmct.objects.supportsMutation(this.frame.domainObjectIdentifier)) {\n        this.domainObjectPromise = this.openmct.objects.getMutable(\n          this.frame.domainObjectIdentifier\n        );\n      } else {\n        this.domainObjectPromise = this.openmct.objects.get(this.frame.domainObjectIdentifier);\n      }\n\n      this.domainObjectPromise.then((object) => {\n        this.setDomainObject(object);\n      });\n    }\n\n    this.dragGhost = document.getElementById('js-fl-drag-ghost');\n  },\n  beforeUnmount() {\n    if (this.domainObjectPromise) {\n      this.domainObjectPromise.then(() => {\n        if (this?.domainObject?.isMutable) {\n          this.openmct.objects.destroyMutable(this.domainObject);\n        }\n      });\n    } else if (this?.domainObject?.isMutable) {\n      this.openmct.objects.destroyMutable(this.domainObject);\n    }\n\n    if (this.unsubscribeSelection) {\n      this.unsubscribeSelection();\n    }\n  },\n  methods: {\n    setDomainObject(object) {\n      this.domainObject = object;\n      this.currentObjectPath = [object].concat(this.objectPath);\n      this.setSelection();\n    },\n    setSelection() {\n      this.$nextTick(() => {\n        if (this.$refs && this.$refs.objectFrame) {\n          let childContext = this.$refs.objectFrame.getSelectionContext();\n          childContext.item = this.domainObject;\n          childContext.type = 'frame';\n          childContext.frameId = this.frame.id;\n          if (this.unsubscribeSelection) {\n            this.unsubscribeSelection();\n          }\n          this.unsubscribeSelection = this.openmct.selection.selectable(\n            this.$refs.frame,\n            childContext,\n            false\n          );\n        }\n      });\n    },\n    initDrag(event) {\n      let type = this.openmct.types.get(this.domainObject.type);\n      let iconClass = type.definition ? type.definition.cssClass : 'icon-object-unknown';\n\n      if (this.dragGhost) {\n        let originalClassName = this.dragGhost.classList[0];\n        this.dragGhost.className = '';\n        this.dragGhost.classList.add(originalClassName, iconClass);\n        this.dragGhost.textContent = '';\n        const span = document.createElement('span');\n        span.textContent = this.domainObject.name;\n        this.dragGhost.appendChild(span);\n\n        event.dataTransfer.setDragImage(this.dragGhost, 0, 0);\n      }\n\n      event.dataTransfer.setData('frameid', this.frame.id);\n      event.dataTransfer.setData('containerIndex', this.containerIndex);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/flexibleLayout/components/flexible-layout.scss",
    "content": "@use 'sass:math';\n\n@mixin containerGrippy($headerSize, $dir) {\n  position: absolute;\n  $h: 6px;\n  $minorOffset: math.div($headerSize - $h, 2);\n  $majorOffset: 35%;\n  content: '';\n  display: block;\n  @include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir);\n  @if $dir == 'x' {\n    top: $minorOffset;\n    right: $majorOffset;\n    bottom: $minorOffset;\n    left: $majorOffset;\n  } @else {\n    top: $majorOffset;\n    right: $minorOffset;\n    bottom: $majorOffset;\n    left: $minorOffset;\n  }\n}\n\n.c-fl {\n  @include abs();\n  display: flex;\n\n  .temp-toolbar {\n    flex: 0 0 auto;\n  }\n\n  &__container-holder {\n    display: flex;\n    flex: 1 1 100%; // Must be 100% to work\n    overflow: auto;\n\n    // Controls layout of c-fl__container(s)\n    &[class*='--cols'] {\n      flex-direction: row;\n      column-gap: 1px;\n    }\n\n    &[class*='--rows'] {\n      flex-direction: column;\n      row-gap: 1px;\n    }\n  }\n\n  &__empty {\n    @include abs();\n    background: rgba($colorBodyFg, 0.1);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n\n    > * {\n      font-style: italic;\n      opacity: 0.5;\n    }\n  }\n\n  &__drag-ghost {\n    background: $colorItemTreeHoverBg;\n    color: $colorItemTreeHoverFg;\n    border-radius: $basicCr;\n    display: flex;\n    align-items: center;\n    padding: $interiorMarginLg $interiorMarginLg * 2;\n    position: absolute;\n    top: -10000px;\n    z-index: 2;\n\n    &:before {\n      color: $colorKey;\n      margin-right: $interiorMarginSm;\n    }\n  }\n}\n\n.c-fl-container {\n  /***************************************************** CONTAINERS */\n  $headerSize: 16px;\n\n  display: flex;\n  flex-direction: column;\n  overflow: auto;\n\n  // flex-basis is set with inline style in code, controls size\n  flex-grow: 1;\n  flex-shrink: 1;\n\n  &__header {\n    // Only displayed when editing, controlled via JS\n    background: $editFrameMovebarColorBg;\n    color: $editFrameMovebarColorFg;\n    cursor: move;\n    display: flex;\n    align-items: center;\n    flex: 0 0 $headerSize;\n\n    &:before {\n      // Drag grippy\n      @include containerGrippy($headerSize, 'x');\n      opacity: 0.5;\n    }\n  }\n\n  &__size-indicator {\n    position: absolute;\n    display: inline-block;\n    right: $interiorMargin;\n  }\n\n  &__frames-holder {\n    display: flex;\n    flex: 1 1 100%; // Must be 100% to work\n    flex-direction: row; // Default\n    align-content: stretch;\n    align-items: stretch;\n    overflow: hidden; // This sucks, but doing in the short-term\n\n    &.--layout-cols {\n      flex-direction: column !important;\n    }\n\n    &.--layout-rows {\n      flex-direction: row !important;\n    }\n  }\n\n  .is-editing & {\n    &:hover {\n      .c-fl-container__header {\n        background: $editFrameHovMovebarColorBg;\n        color: $editFrameHovMovebarColorFg;\n\n        &:before {\n          opacity: 0.75;\n        }\n      }\n    }\n\n    &[s-selected] {\n      border: $editFrameSelectedBorder;\n\n      .c-fl-container__header {\n        background: $editFrameSelectedMovebarColorBg;\n        color: $editFrameSelectedMovebarColorFg;\n        &:before {\n          // Grippy\n          opacity: 1;\n        }\n      }\n    }\n\n    [s-selected].c-fl-frame__drag-wrapper {\n      border: $editFrameSelectedBorder;\n    }\n  }\n\n  /****** THEIR FRAMES */\n  // Frames get styled here because this is particular to their presence in this layout type\n  .c-fl-frame {\n    @include browserPrefix(margin-collapse, collapse);\n  }\n\n  /****** ROWS LAYOUT */\n  .c-fl--rows & {\n    // Layout is rows\n    flex-direction: row;\n\n    &__header {\n      flex-basis: $headerSize;\n      overflow: hidden;\n\n      &:before {\n        // Grippy\n        @include containerGrippy($headerSize, 'y');\n      }\n    }\n\n    &__size-indicator {\n      right: 0;\n      top: $interiorMargin;\n      transform-origin: top right;\n      transform: rotate(-90deg) translateY(-100%);\n    }\n\n    &__frames-holder {\n      flex-direction: row;\n    }\n  }\n}\n\n.c-fl-frame {\n  /***************************************************** CONTAINER FRAMES */\n  $sizeIndicatorM: 16px;\n  $dropHintSize: 15px;\n\n  display: flex;\n  flex: 1 1;\n  flex-direction: column;\n  overflow: hidden; // Needed to allow frames to collapse when sized down\n\n  &__drag-wrapper {\n    flex: 1 1 auto;\n    overflow: auto;\n\n    .is-editing & {\n      > * {\n        pointer-events: none;\n      }\n    }\n  }\n\n  &__header {\n    flex: 0 0 auto;\n    margin-bottom: $interiorMargin;\n  }\n\n  &__size-indicator {\n    $size: 35px;\n\n    @include ellipsize();\n    background: $colorBtnBg;\n    border-top-left-radius: $controlCr;\n    color: $colorBtnFg;\n    display: inline-block;\n    padding: $interiorMarginSm 0;\n    position: absolute;\n    pointer-events: none;\n    text-align: center;\n    width: $size;\n\n    // Changed when layout is different, see below\n    border-top-right-radius: $controlCr;\n    bottom: 1px;\n    right: $sizeIndicatorM;\n  }\n\n  &__drop-hint {\n    flex: 0 0 $dropHintSize;\n    .c-drop-hint {\n      border-radius: $smallCr;\n    }\n  }\n\n  &__resize-handle {\n    $size: 1px;\n    $margin: 3px;\n    $marginHov: 0;\n    $grippyThickness: $size + 6;\n    $grippyLen: $grippyThickness * 2;\n\n    @include resizeHandleStyle($size, $margin);\n\n    display: flex;\n    flex-direction: column;\n    flex: 0 0 ($margin * 2) + $size;\n  }\n\n  // Hide the resize-handles in first and last c-fl-frame elements\n  &:first-child,\n  &:last-child {\n    .c-fl-frame__resize-handle {\n      display: none;\n    }\n  }\n\n  .c-fl--rows & {\n    flex-direction: row;\n\n    &__size-indicator {\n      border-bottom-left-radius: $controlCr;\n      border-top-right-radius: 0;\n      bottom: $sizeIndicatorM;\n      right: 1px;\n    }\n  }\n\n  &--first-in-container {\n    border: none;\n    flex: 0 0 0;\n    .c-fl-frame__drag-wrapper {\n      display: none;\n    }\n\n    &.is-dragging {\n      flex-basis: $dropHintSize;\n    }\n  }\n\n  .is-empty & {\n    &.c-fl-frame--first-in-container {\n      flex: 1 1 auto;\n    }\n\n    &__drop-hint {\n      flex: 1 0 100%;\n      margin: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/flexibleLayout/flexibleLayoutStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function flexibleLayoutStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return domainObject?.type === 'flexible-layout';\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      if (!domainObject.configuration.objectStyles) {\n        domainObject.configuration.objectStyles = {};\n      }\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/flexibleLayout/flexibleLayoutViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport FlexibleLayoutComponent from './components/FlexibleLayout.vue';\n\nconst FLEXIBLE_LAYOUT_KEY = 'flexible-layout';\nexport default class FlexibleLayoutViewProvider {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.key = FLEXIBLE_LAYOUT_KEY;\n    this.name = 'Flexible Layout';\n    this.cssClass = 'icon-layout-view';\n    this.destroy = null;\n  }\n\n  canView(domainObject) {\n    return domainObject.type === FLEXIBLE_LAYOUT_KEY;\n  }\n\n  canEdit(domainObject) {\n    return domainObject.type === FLEXIBLE_LAYOUT_KEY;\n  }\n\n  view(domainObject, objectPath) {\n    let openmct = this.openmct;\n    let _destroy = null;\n    let component = null;\n\n    return {\n      show(element, isEditing) {\n        const { vNode, destroy } = mount(\n          {\n            components: {\n              FlexibleLayoutComponent\n            },\n            provide: {\n              openmct,\n              objectPath,\n              domainObject\n            },\n            data() {\n              return {\n                isEditing: isEditing\n              };\n            },\n            template:\n              '<flexible-layout-component ref=\"flexibleLayout\" :isEditing=\"isEditing\"></flexible-layout-component>'\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        component = vNode.componentInstance;\n        _destroy = destroy;\n      },\n      getSelectionContext() {\n        return {\n          item: domainObject,\n          type: 'flexible-layout'\n        };\n      },\n      contextAction(action, ...args) {\n        if (component?.$refs?.flexibleLayout?.[action]) {\n          component.$refs.flexibleLayout[action](...args);\n        }\n      },\n      onEditModeChange(isEditing) {\n        component.isEditing = isEditing;\n      },\n      destroy() {\n        if (_destroy) {\n          _destroy();\n          component = null;\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/flexibleLayout/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Container from '@/ui/layout/Container.js';\n\nimport flexibleLayoutStylesInterceptor from './flexibleLayoutStylesInterceptor.js';\nimport FlexibleLayoutViewProvider from './flexibleLayoutViewProvider.js';\nimport ToolBarProvider from './toolbarProvider.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct));\n\n    openmct.types.addType('flexible-layout', {\n      name: 'Flexible Layout',\n      creatable: true,\n      description:\n        'A fluid, flexible layout canvas that can display multiple objects in rows or columns.',\n      cssClass: 'icon-flexible-layout',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          containers: [new Container(50), new Container(50)],\n          rowsLayout: false,\n          objectStyles: {}\n        };\n        domainObject.composition = [];\n      }\n    });\n    openmct.objects.addGetInterceptor(flexibleLayoutStylesInterceptor(openmct));\n\n    let toolbar = ToolBarProvider(openmct);\n\n    openmct.toolbars.addProvider(toolbar);\n  };\n}\n"
  },
  {
    "path": "src/plugins/flexibleLayout/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport FlexibleLayout from './plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let flexibleLayoutDefinition;\n  let mockComposition;\n\n  const testViewObject = {\n    identifier: {\n      namespace: '',\n      key: 'test-object'\n    },\n    id: 'test-object',\n    type: 'flexible-layout',\n    configuration: {\n      rowsLayout: false,\n      containers: [\n        {\n          id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e',\n          frames: [],\n          size: 50\n        },\n        {\n          id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f',\n          frames: [],\n          size: 50\n        }\n      ]\n    },\n    composition: []\n  };\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(new FlexibleLayout());\n    flexibleLayoutDefinition = openmct.types.get('flexible-layout');\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.start(child);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('defines a flexible layout object type with the correct key', () => {\n    expect(flexibleLayoutDefinition.definition.name).toEqual('Flexible Layout');\n  });\n\n  describe('the view', function () {\n    let flexibleLayoutViewProvider;\n\n    beforeEach(() => {\n      mockComposition = new EventEmitter();\n      // eslint-disable-next-line require-await\n      mockComposition.load = async () => {\n        return [];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);\n      flexibleLayoutViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'flexible-layout'\n      );\n    });\n\n    it('provides a view', () => {\n      expect(flexibleLayoutViewProvider).toBeDefined();\n    });\n\n    it('renders a view', async () => {\n      const flexibleView = flexibleLayoutViewProvider.view(testViewObject, [testViewObject]);\n      flexibleView.show(child, false);\n\n      await nextTick();\n      const flexTitle = child.querySelector('.c-fl');\n\n      expect(flexTitle).not.toBeNull();\n    });\n  });\n\n  describe('the toolbar', () => {\n    let flexibleLayoutItem;\n    let flexibleLayoutToolbar;\n    let telemetryItem;\n    let selection;\n\n    beforeEach(() => {\n      flexibleLayoutItem = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        id: 'test-object',\n        type: 'flexible-layout',\n        configuration: {\n          rowsLayout: true,\n          containers: [\n            {\n              id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e',\n              frames: [\n                {\n                  id: '329bf482-d0dc-486a-aae0-6176276bd315',\n                  domainObjectIdentifier: {\n                    namespace: '',\n                    key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n                  },\n                  size: 100,\n                  noFrame: false\n                }\n              ],\n              size: 61\n            },\n            {\n              id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f',\n              frames: [\n                {\n                  id: '329bf482-d0dc-486a-aae0-6176276bd316',\n                  domainObjectIdentifier: {\n                    namespace: '',\n                    key: '55122607-e65e-44d5-9c9d-9c31a914ca90'\n                  },\n                  size: 100,\n                  noFrame: false\n                }\n              ],\n              size: 39\n            }\n          ]\n        },\n        composition: [\n          {\n            identifier: {\n              namespace: '',\n              key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n            }\n          },\n          {\n            identifier: {\n              namespace: '',\n              key: '55122607-e65e-44d5-9c9d-9c31a914ca90'\n            }\n          }\n        ]\n      };\n      telemetryItem = {\n        telemetry: {\n          period: 5,\n          amplitude: 5,\n          offset: 5,\n          dataRateInHz: 5,\n          phase: 5,\n          randomness: 0\n        },\n        name: 'Sine Wave Generator',\n        type: 'generator',\n        modified: 1592851063871,\n        location: 'mine',\n        persisted: 1592851063871,\n        id: '55122607-e65e-44d5-9c9d-9c31a914ca89',\n        identifier: {\n          namespace: '',\n          key: '55122607-e65e-44d5-9c9d-9c31a914ca89'\n        }\n      };\n      selection = [\n        [\n          {\n            context: {\n              frameId: '329bf482-d0dc-486a-aae0-6176276bd315',\n              item: telemetryItem,\n              type: 'frame'\n            }\n          },\n          {\n            context: {\n              containerId: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e',\n              item: flexibleLayoutItem,\n              type: 'container'\n            }\n          },\n          {\n            context: {\n              item: flexibleLayoutItem,\n              type: 'flexible-layout'\n            }\n          }\n        ]\n      ];\n\n      flexibleLayoutToolbar = openmct.toolbars.get(selection);\n    });\n\n    it('provides controls including separators', () => {\n      expect(flexibleLayoutToolbar.length).toBe(6);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/flexibleLayout/toolbarProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction ToolbarProvider(openmct) {\n  return {\n    name: 'Flexible Layout Toolbar',\n    key: 'flex-layout',\n    description: 'A toolbar for objects inside a Flexible Layout.',\n    forSelection: function (selection) {\n      let context = selection[0][0].context;\n\n      return (\n        context &&\n        context.type &&\n        (context.type === 'flexible-layout' ||\n          context.type === 'container' ||\n          context.type === 'frame')\n      );\n    },\n    toolbar: function (selection) {\n      let selectionPath = selection[0];\n      let primary = selectionPath[0];\n      let secondary = selectionPath[1];\n      let tertiary = selectionPath[2];\n      let deleteFrame;\n      let toggleContainer;\n      let deleteContainer;\n      let addContainer;\n      let toggleFrame;\n      const primaryKeyString =\n        primary?.context?.item?.identifier &&\n        openmct.objects.makeKeyString(primary.context.item.identifier);\n      const tertiaryKeyString =\n        tertiary?.context?.item?.identifier &&\n        openmct.objects.makeKeyString(tertiary.context.item.identifier);\n\n      toggleContainer = {\n        control: 'toggle-button',\n        key: 'toggle-layout',\n        domainObject: primary.context.item,\n        property: 'configuration.rowsLayout',\n        options: [\n          {\n            value: true,\n            icon: 'icon-rows',\n            title: 'Switch to rows layout'\n          },\n          {\n            value: false,\n            icon: 'icon-columns',\n            title: 'Switch to columns layout'\n          }\n        ]\n      };\n\n      function getSeparator() {\n        return {\n          control: 'separator'\n        };\n      }\n\n      if (primary.context.type === 'frame') {\n        if (secondary.context.item.locked) {\n          return [];\n        }\n\n        let frameId = primary.context.frameId;\n        let layoutObject = tertiary.context.item;\n        let containers = layoutObject.configuration.containers;\n        let container = containers.filter((c) => c.frames.some((f) => f.id === frameId))[0];\n        let containerIndex = containers.indexOf(container);\n        let frame = container && container.frames.filter((f) => f.id === frameId)[0];\n        let frameIndex = container && container.frames.indexOf(frame);\n        deleteFrame = {\n          control: 'button',\n          domainObject: primary.context.item,\n          method: function () {\n            let prompt = openmct.overlays.dialog({\n              iconClass: 'alert',\n              message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,\n              buttons: [\n                {\n                  label: 'Ok',\n                  emphasis: 'true',\n                  callback: function () {\n                    openmct.objectViews.emit(\n                      `contextAction:${tertiaryKeyString}`,\n                      'deleteFrame',\n                      primary.context.frameId\n                    );\n                    prompt.dismiss();\n                  }\n                },\n                {\n                  label: 'Cancel',\n                  callback: function () {\n                    prompt.dismiss();\n                  }\n                }\n              ]\n            });\n          },\n          key: 'remove',\n          icon: 'icon-trash',\n          title: 'Remove Frame'\n        };\n        toggleFrame = {\n          control: 'toggle-button',\n          domainObject: secondary.context.item,\n          property: `configuration.containers[${containerIndex}].frames[${frameIndex}].noFrame`,\n          options: [\n            {\n              value: false,\n              icon: 'icon-frame-hide',\n              title: 'Frame hidden'\n            },\n            {\n              value: true,\n              icon: 'icon-frame-show',\n              title: 'Frame visible'\n            }\n          ]\n        };\n\n        addContainer = {\n          control: 'button',\n          domainObject: tertiary.context.item,\n          method: function (...args) {\n            openmct.objectViews.emit(`contextAction:${tertiaryKeyString}`, 'addContainer', ...args);\n          },\n          key: 'add',\n          icon: 'icon-plus-in-rect',\n          title: 'Add Container'\n        };\n\n        toggleContainer.domainObject = secondary.context.item;\n      } else if (primary.context.type === 'container') {\n        if (primary.context.item.locked) {\n          return [];\n        }\n\n        deleteContainer = {\n          control: 'button',\n          domainObject: primary.context.item,\n          method: function () {\n            let containerId = primary.context.containerId;\n\n            let prompt = openmct.overlays.dialog({\n              iconClass: 'alert',\n              message:\n                'This action will permanently delete this container from this Flexible Layout. Do you want to continue?',\n              buttons: [\n                {\n                  label: 'Ok',\n                  emphasis: 'true',\n                  callback: function () {\n                    openmct.objectViews.emit(\n                      `contextAction:${primaryKeyString}`,\n                      'deleteContainer',\n                      containerId\n                    );\n                    prompt.dismiss();\n                  }\n                },\n                {\n                  label: 'Cancel',\n                  callback: function () {\n                    prompt.dismiss();\n                  }\n                }\n              ]\n            });\n          },\n          key: 'remove',\n          icon: 'icon-trash',\n          title: 'Remove Container'\n        };\n\n        const domainObject = secondary.context.item;\n        const keyString = openmct.objects.makeKeyString(domainObject.identifier);\n\n        addContainer = {\n          control: 'button',\n          domainObject,\n          method: function (...args) {\n            openmct.objectViews.emit(`contextAction:${keyString}`, 'addContainer', ...args);\n          },\n          key: 'add',\n          icon: 'icon-plus-in-rect',\n          title: 'Add Container'\n        };\n      } else if (primary.context.type === 'flexible-layout') {\n        if (primary.context.item.locked) {\n          return [];\n        }\n\n        const domainObject = primary.context.item;\n        const keyString = openmct.objects.makeKeyString(domainObject.identifier);\n\n        addContainer = {\n          control: 'button',\n          domainObject,\n          method: function (...args) {\n            openmct.objectViews.emit(`contextAction:${keyString}`, 'addContainer', ...args);\n          },\n          key: 'add',\n          icon: 'icon-plus-in-rect',\n          title: 'Add Container'\n        };\n      }\n\n      let toolbar = [\n        toggleContainer,\n        addContainer,\n        toggleFrame ? getSeparator() : undefined,\n        toggleFrame,\n        deleteFrame || deleteContainer ? getSeparator() : undefined,\n        deleteFrame,\n        deleteContainer\n      ];\n\n      return toolbar.filter((button) => button !== undefined);\n    }\n  };\n}\n\nexport default ToolbarProvider;\n"
  },
  {
    "path": "src/plugins/folderView/FolderGridView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport GridViewComponent from './components/GridView.vue';\nimport { ALLOWED_FOLDER_TYPES } from './constants.js';\n\nexport default class FolderGridView {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.key = 'grid';\n    this.name = 'Grid View';\n    this.cssClass = 'icon-thumbs-strip';\n  }\n  canView(domainObject) {\n    return ALLOWED_FOLDER_TYPES.includes(domainObject.type);\n  }\n\n  view(domainObject) {\n    return {\n      show: (element) => {\n        const { destroy } = mount(\n          {\n            components: {\n              GridViewComponent\n            },\n            provide: {\n              openmct: this.openmct,\n              domainObject\n            },\n            template: '<GridViewComponent></GridViewComponent>'\n          },\n          {\n            app: this.openmct.app,\n            element\n          }\n        );\n        this._destroy = destroy;\n      },\n      destroy: () => {\n        if (this._destroy) {\n          this._destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/folderView/FolderListView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Moment from 'moment';\nimport mount from 'utils/mount';\n\nimport ListViewComponent from './components/ListView.vue';\nimport { ALLOWED_FOLDER_TYPES } from './constants.js';\n\nexport default class FolderListView {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.key = 'list-view';\n    this.name = 'List View';\n    this.cssClass = 'icon-list-view';\n  }\n\n  canView(domainObject) {\n    return ALLOWED_FOLDER_TYPES.includes(domainObject.type);\n  }\n\n  view(domainObject) {\n    return {\n      show: (element) => {\n        const { destroy } = mount(\n          {\n            el: element,\n            components: {\n              ListViewComponent\n            },\n            provide: {\n              openmct: this.openmct,\n              domainObject,\n              Moment\n            },\n            template: '<ListViewComponent></ListViewComponent>'\n          },\n          {\n            app: this.openmct.app,\n            element\n          }\n        );\n        this._destroy = destroy;\n      },\n      destroy: () => {\n        if (this._destroy) {\n          this._destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/folderView/components/GridItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <a\n    ref=\"root\"\n    class=\"l-grid-view__item c-grid-item js-folder-child\"\n    :class=\"[\n      {\n        'is-alias': item.isAlias === true,\n        'c-grid-item--unknown':\n          item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1\n      },\n      statusClass\n    ]\"\n    @click=\"navigate($event)\"\n  >\n    <div\n      class=\"c-grid-item__type-icon\"\n      :class=\"\n        item.type.cssClass != undefined ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'\n      \"\n    ></div>\n    <div class=\"c-grid-item__details\">\n      <!-- Name and metadata -->\n      <div class=\"c-grid-item__name\" :title=\"item.model.name\">{{ item.model.name }}</div>\n      <div class=\"c-grid-item__metadata\" :aria-label=\"item.type.name\" :title=\"item.type.name\">\n        <span class=\"c-grid-item__metadata__type\">{{ item.type.name }}</span>\n      </div>\n    </div>\n    <div class=\"c-grid-item__controls\">\n      <div\n        class=\"is-status__indicator\"\n        :aria-label=\"`This item is ${status}`\"\n        :title=\"`This item is ${status}`\"\n      ></div>\n      <div class=\"icon-people\" title=\"Shared\"></div>\n      <button\n        class=\"c-icon-button icon-info c-info-button\"\n        aria-label=\"More Info\"\n        title=\"More Info\"\n      ></button>\n      <div class=\"icon-pointer-right c-pointer-icon\"></div>\n    </div>\n  </a>\n</template>\n\n<script>\nimport contextMenuGesture from '../../../ui/mixins/context-menu-gesture.js';\nimport objectLink from '../../../ui/mixins/object-link.js';\nimport statusListener from './status-listener.js';\n\nexport default {\n  mixins: [contextMenuGesture, objectLink, statusListener],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    }\n  },\n  methods: {\n    navigate(_event) {\n      this.openmct.router.navigate(this.objectLink);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/folderView/components/GridView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"l-grid-view\" role=\"grid\" :aria-label=\"`${domainObject.name} Grid View`\">\n    <GridItem\n      v-for=\"(item, index) in items\"\n      :key=\"index\"\n      :item=\"item\"\n      :object-path=\"item.objectPath\"\n      role=\"gridcell\"\n    />\n  </div>\n</template>\n\n<script>\nimport compositionLoader from './composition-loader.js';\nimport GridItem from './GridItem.vue';\n\nexport default {\n  components: { GridItem },\n  mixins: [compositionLoader],\n  inject: ['openmct', 'domainObject']\n};\n</script>\n"
  },
  {
    "path": "src/plugins/folderView/components/ListItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <tr\n    ref=\"root\"\n    class=\"c-list-item js-folder-child\"\n    :class=\"{\n      'is-alias': item.isAlias === true\n    }\"\n    @click=\"navigate\"\n  >\n    <td class=\"c-list-item__name\">\n      <a ref=\"objectLink\" class=\"c-object-label\" :class=\"[statusClass]\" @click=\"navigate\">\n        <div\n          class=\"c-object-label__type-icon c-list-item__name__type-icon\"\n          :class=\"item.type.cssClass\"\n        >\n          <span\n            class=\"is-status__indicator\"\n            :aria-label=\"`This item is ${status}`\"\n            :title=\"`This item is ${status}`\"\n          ></span>\n        </div>\n        <div class=\"c-object-label__name c-list-item__name__name\">{{ item.model.name }}</div>\n      </a>\n    </td>\n    <td class=\"c-list-item__type\">\n      {{ item.type.name }}\n    </td>\n    <td class=\"c-list-item__date-created\">\n      {{ formatTime(item.model.persisted, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z\n    </td>\n    <td class=\"c-list-item__date-updated\">\n      {{ formatTime(item.model.modified, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z\n    </td>\n  </tr>\n</template>\n\n<script>\nimport moment from 'moment';\n\nimport contextMenuGesture from '../../../ui/mixins/context-menu-gesture.js';\nimport objectLink from '../../../ui/mixins/object-link.js';\nimport statusListener from './status-listener.js';\n\nexport default {\n  mixins: [contextMenuGesture, objectLink, statusListener],\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    }\n  },\n  methods: {\n    formatTime(timestamp, format) {\n      return moment(timestamp).format(format);\n    },\n    navigate() {\n      this.openmct.router.navigate(this.objectLink);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/folderView/components/ListView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-table c-table--sortable c-list-view c-list-view--sticky-header c-list-view--selectable\"\n  >\n    <table class=\"c-table__body\">\n      <thead class=\"c-table__header\">\n        <tr>\n          <th\n            class=\"is-sortable\"\n            :class=\"{\n              'is-sorting': sortBy === 'model.name',\n              asc: ascending,\n              desc: !ascending\n            }\"\n            @click=\"sort('model.name', true)\"\n          >\n            Name\n          </th>\n          <th\n            class=\"is-sortable\"\n            :class=\"{\n              'is-sorting': sortBy === 'type.name',\n              asc: ascending,\n              desc: !ascending\n            }\"\n            @click=\"sort('type.name', true)\"\n          >\n            Type\n          </th>\n          <th\n            class=\"is-sortable\"\n            :class=\"{\n              'is-sorting': sortBy === 'model.persisted',\n              asc: ascending,\n              desc: !ascending\n            }\"\n            @click=\"sort('model.persisted', false)\"\n          >\n            Created Date\n          </th>\n          <th\n            class=\"is-sortable\"\n            :class=\"{\n              'is-sorting': sortBy === 'model.modified',\n              asc: ascending,\n              desc: !ascending\n            }\"\n            @click=\"sort('model.modified', false)\"\n          >\n            Updated Date\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        <ListItem\n          v-for=\"item in sortedItems\"\n          :key=\"item.objectKeyString\"\n          :item=\"item\"\n          :object-path=\"item.objectPath\"\n        />\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport compositionLoader from './composition-loader.js';\nimport ListItem from './ListItem.vue';\n\nexport default {\n  components: { ListItem },\n  mixins: [compositionLoader],\n  inject: ['domainObject', 'openmct'],\n  data() {\n    let sortBy = 'model.name';\n    let ascending = true;\n    let persistedSortOrder = window.localStorage.getItem('openmct-listview-sort-order');\n\n    if (persistedSortOrder) {\n      let parsed = JSON.parse(persistedSortOrder);\n\n      sortBy = parsed.sortBy;\n      ascending = parsed.ascending;\n    }\n\n    return {\n      sortBy,\n      ascending\n    };\n  },\n  computed: {\n    sortedItems() {\n      let sortedItems = _.sortBy(this.items, this.sortBy);\n      if (!this.ascending) {\n        sortedItems = sortedItems.reverse();\n      }\n\n      return sortedItems;\n    }\n  },\n  methods: {\n    sort(field, defaultDirection) {\n      if (this.sortBy === field) {\n        this.ascending = !this.ascending;\n      } else {\n        this.sortBy = field;\n        this.ascending = defaultDirection;\n      }\n\n      window.localStorage.setItem(\n        'openmct-listview-sort-order',\n        JSON.stringify({\n          sortBy: this.sortBy,\n          ascending: this.ascending\n        })\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/folderView/components/composition-loader.js",
    "content": "const unknownObjectType = {\n  definition: {\n    cssClass: 'icon-object-unknown',\n    name: 'Unknown Type'\n  }\n};\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      items: []\n    };\n  },\n  mounted() {\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    if (!this.composition) {\n      return;\n    }\n\n    this.composition.on('add', this.add);\n    this.composition.on('remove', this.remove);\n    this.composition.load();\n  },\n  beforeUnmount() {\n    if (!this.composition) {\n      return;\n    }\n    this.composition.off('add', this.add);\n    this.composition.off('remove', this.remove);\n  },\n  methods: {\n    add(child, index, anything) {\n      const type = this.openmct.types.get(child.type) || unknownObjectType;\n      this.items.push({\n        model: child,\n        type: type.definition,\n        isAlias: this.keystring !== child.location,\n        objectPath: [child].concat(this.openmct.router.path),\n        objectKeyString: this.openmct.objects.makeKeyString(child.identifier)\n      });\n    },\n    remove(identifier) {\n      this.items = this.items.filter((i) => {\n        return (\n          i.model.identifier.key !== identifier.key ||\n          i.model.identifier.namespace !== identifier.namespace\n        );\n      });\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/folderView/components/grid-view.scss",
    "content": "@use 'sass:math';\n\n/******************************* GRID VIEW */\n.l-grid-view {\n  display: flex;\n  flex-flow: column nowrap;\n  overflow: auto;\n  height: 100%;\n\n  &__item {\n    flex: 0 0 auto;\n    + .l-grid-view__item {\n      margin-top: $interiorMargin;\n    }\n  }\n\n  body.desktop & {\n    flex-flow: row wrap;\n    align-content: flex-start;\n\n    &__item {\n      height: $gridItemDesk;\n      width: $gridItemDesk;\n      margin: 0 $interiorMargin $interiorMargin 0;\n    }\n  }\n\n  body.mobile & {\n    flex: 1 0 auto;\n  }\n\n  [class*='l-overlay'] & {\n    // When this view is in an overlay, prevent navigation\n    pointer-events: none;\n  }\n}\n\n/******************************* GRID ITEMS */\n.c-grid-item {\n  // Mobile-first\n  @include button($bg: $colorItemBg, $fg: $colorItemFg);\n  @include cControlHov();\n  cursor: pointer;\n  display: flex;\n  padding: $interiorMarginLg;\n\n  &__type-icon {\n    filter: $colorKeyFilter;\n    flex: 0 0 $gridItemMobile;\n    font-size: floor(math.div($gridItemMobile, 2));\n    margin-right: $interiorMarginLg;\n  }\n\n  &.is-alias {\n    // Object is an alias to an original.\n    [class*='__type-icon'] {\n      @include isAlias();\n    }\n  }\n\n  &.is-status--notebook-default {\n    .is-status__indicator {\n      display: block;\n\n      &:before {\n        color: $colorFilter;\n        content: $glyph-icon-notebook-page;\n        font-family: symbolsfont;\n      }\n    }\n  }\n\n  &.is-status--current {\n    .is-status__indicator {\n      display: block;\n\n      &:before {\n        color: $colorFilter;\n        content: $glyph-icon-asterisk;\n        font-family: symbolsfont;\n      }\n    }\n  }\n\n  &.is-status--draft {\n    .is-status__indicator {\n      display: block;\n\n      &:before {\n        color: $colorStatusAlert;\n        content: $glyph-icon-draft;\n        font-family: symbolsfont;\n      }\n    }\n  }\n\n  &[class*='is-status--missing'],\n  &[class*='is-status--suspect'] {\n    [class*='__type-icon'],\n    [class*='__details'] {\n      opacity: $opacityMissing;\n    }\n  }\n\n  &__details {\n    display: flex;\n    flex-flow: column nowrap;\n    flex: 1 1 auto;\n  }\n\n  &__name {\n    @include ellipsize();\n    color: $colorItemFg;\n    font-size: 1.2em;\n    font-weight: 400;\n    margin-bottom: $interiorMarginSm;\n  }\n\n  &__metadata {\n    color: $colorItemFgDetails;\n    //font-size: 0.9em;\n\n    body.mobile & {\n      [class*='__item-count'] {\n        &:before {\n          content: ' - ';\n        }\n      }\n    }\n  }\n\n  &__controls {\n    color: $colorItemFgDetails;\n    flex: 0 0 64px;\n    font-size: 1.2em;\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  body.desktop & {\n    $transOutMs: 300ms;\n    flex-flow: column nowrap;\n\n    &:hover {\n      .c-grid-item__type-icon {\n        transform: scale(1.1);\n      }\n    }\n\n    > * {\n      margin: 0; // Reset from mobile\n    }\n\n    &__controls {\n      align-items: baseline;\n      flex: 0 0 auto;\n      order: 1;\n      .c-info-button,\n      .c-pointer-icon {\n        display: none;\n      }\n    }\n\n    &__type-icon {\n      flex: 1 1 auto;\n      font-size: floor(math.div($gridItemDesk, 3));\n      margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;\n      order: 2;\n    }\n\n    &__details {\n      flex: 0 0 auto;\n      justify-content: flex-end;\n      order: 3;\n    }\n\n    &__metadata {\n      display: flex;\n\n      &__type {\n        flex: 1 1 auto;\n        @include ellipsize();\n      }\n\n      &__item-count {\n        opacity: 0.7;\n        flex: 0 0 auto;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/folderView/components/list-item.scss",
    "content": "/******************************* LIST ITEM */\n.c-list-item {\n  color: $colorItemFgDetails;\n\n  &__name__type-icon {\n    color: $colorItemTreeIcon;\n  }\n\n  &__name__name {\n    @include ellipsize();\n\n    a & {\n      // .c-list-item_name a element\n      color: $colorItemFg;\n    }\n  }\n\n  &:not(.c-list-item__name) {\n  }\n\n  &.is-alias {\n    // Object is an alias to an original.\n    [class*='__type-icon'] {\n      @include isAlias();\n    }\n  }\n\n  [class*='l-overlay'] & {\n    // When this view is in an overlay, prevent navigation\n    pointer-events: none;\n  }\n}\n"
  },
  {
    "path": "src/plugins/folderView/components/status-listener.js",
    "content": "export default {\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    }\n  },\n  computed: {\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    }\n  },\n  data() {\n    return {\n      status: ''\n    };\n  },\n  methods: {\n    setStatus(status) {\n      this.status = status;\n    }\n  },\n  mounted() {\n    let identifier = this.item.model.identifier;\n\n    this.status = this.openmct.status.get(identifier);\n    this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);\n  },\n  unmounted() {\n    this.removeStatusListener();\n  }\n};\n"
  },
  {
    "path": "src/plugins/folderView/constants.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const ALLOWED_FOLDER_TYPES = ['folder', 'noneditable.folder'];\n"
  },
  {
    "path": "src/plugins/folderView/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport FolderGridView from './FolderGridView.js';\nimport FolderListView from './FolderListView.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.types.addType('folder', {\n      name: 'Folder',\n      key: 'folder',\n      description:\n        \"Create folders to organize other objects or links to objects without the ability to edit it's properties.\",\n      cssClass: 'icon-folder',\n      creatable: true,\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n      }\n    });\n\n    openmct.objectViews.addProvider(new FolderGridView(openmct));\n    openmct.objectViews.addProvider(new FolderListView(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/folderView/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport FolderPlugin from './plugin.js';\n\ndescribe('The folder plugin', () => {\n  let openmct;\n  let folderPlugin;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    folderPlugin = new FolderPlugin();\n    openmct.install(folderPlugin);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the folder object type', () => {\n    let folderType;\n\n    beforeEach(() => {\n      folderType = openmct.types.get('folder');\n    });\n\n    it('is installed by the plugin', () => {\n      expect(folderType).toBeDefined();\n    });\n\n    it('is user creatable', () => {\n      expect(folderType.definition.creatable).toBe(true);\n    });\n  });\n\n  describe('the folder grid view', () => {\n    let gridViewProvider;\n    let listViewProvider;\n    let folderObject;\n    let addCallback;\n    let parentDiv;\n    let childDiv;\n\n    beforeEach(() => {\n      parentDiv = document.createElement('div');\n      childDiv = document.createElement('div');\n      parentDiv.appendChild(childDiv);\n\n      folderObject = {\n        identifier: {\n          namespace: 'test-namespace',\n          key: 'folder-object'\n        },\n        name: 'A folder!',\n        type: 'folder',\n        composition: [\n          {\n            namespace: 'test-namespace',\n            key: 'child-object-1'\n          },\n          {\n            namespace: 'test-namespace',\n            key: 'child-object-2'\n          },\n          {\n            namespace: 'test-namespace',\n            key: 'child-object-3'\n          },\n          {\n            namespace: 'test-namespace',\n            key: 'child-object-4'\n          }\n        ]\n      };\n\n      gridViewProvider = openmct.objectViews\n        .get(folderObject, [folderObject])\n        .find((view) => view.key === 'grid');\n      listViewProvider = openmct.objectViews\n        .get(folderObject, [folderObject])\n        .find((view) => view.key === 'list-view');\n\n      const fakeCompositionCollection = jasmine.createSpyObj('compositionCollection', [\n        'on',\n        'load'\n      ]);\n      fakeCompositionCollection.on.and.callFake((eventName, callback) => {\n        if (eventName === 'add') {\n          addCallback = callback;\n        }\n      });\n      fakeCompositionCollection.load.and.callFake(() => {\n        folderObject.composition.forEach((identifier) => {\n          addCallback({\n            identifier,\n            type: 'folder'\n          });\n        });\n      });\n      spyOn(openmct.composition, 'get').and.returnValue(fakeCompositionCollection);\n    });\n\n    describe('the grid view', () => {\n      it('is installed by the plugin and is applicable to the folder type', () => {\n        expect(gridViewProvider).toBeDefined();\n      });\n      it(\"renders each item contained in the folder's composition\", async () => {\n        let folderView = gridViewProvider.view(folderObject, [folderObject]);\n        folderView.show(childDiv, true);\n\n        await nextTick();\n\n        let children = parentDiv.getElementsByClassName('js-folder-child');\n        expect(children.length).toBe(folderObject.composition.length);\n      });\n    });\n\n    describe('the list view', () => {\n      it('installs a list view for the folder type', () => {\n        expect(listViewProvider).toBeDefined();\n      });\n      it(\"renders each item contained in the folder's composition\", async () => {\n        let folderView = listViewProvider.view(folderObject, [folderObject]);\n        folderView.show(childDiv, true);\n\n        await nextTick();\n\n        let children = parentDiv.getElementsByClassName('js-folder-child');\n        expect(children.length).toBe(folderObject.composition.length);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/formActions/CreateAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\nimport { v4 as uuid } from 'uuid';\n\nimport CreateWizard from './CreateWizard.js';\nimport PropertiesAction from './PropertiesAction.js';\n\nconst CREATE_ACTION_KEY = 'create';\n\nclass CreateAction extends PropertiesAction {\n  #transaction;\n\n  constructor(openmct) {\n    super(openmct);\n\n    this.#transaction = null;\n    this.key = CREATE_ACTION_KEY;\n    // Hide the create action from context menus by default\n    this.isHidden = true;\n  }\n\n  get invoke() {\n    return (type, parentDomainObject) => this._showCreateForm(type, parentDomainObject);\n  }\n\n  /**\n   * @private\n   */\n  async _onSave(changes) {\n    let parentDomainObjectPath;\n\n    Object.entries(changes).forEach(([key, value]) => {\n      if (key === 'location') {\n        parentDomainObjectPath = value;\n\n        return;\n      }\n\n      const existingValue = this.domainObject[key];\n      if (!(existingValue instanceof Array) && typeof existingValue === 'object') {\n        value = _.merge(existingValue, value);\n      }\n\n      _.set(this.domainObject, key, value);\n    });\n\n    const parentDomainObject = this.openmct.objects.toMutable(parentDomainObjectPath[0]);\n\n    this.domainObject.modified = Date.now();\n    this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);\n    this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace;\n\n    // Show saving progress dialog\n    let dialog = this.openmct.overlays.progressDialog({\n      progressPerc: null,\n      message:\n        'Do not navigate away from this page or close this browser tab while this message is displayed.',\n      iconClass: 'info',\n      title: 'Saving'\n    });\n\n    try {\n      await this.openmct.objects.save(this.domainObject);\n      const compositionCollection = await this.openmct.composition.get(parentDomainObject);\n      compositionCollection.add(this.domainObject);\n      await this.saveTransaction();\n\n      this._navigateAndEdit(this.domainObject, parentDomainObjectPath);\n\n      this.openmct.notifications.info('Save successful');\n    } catch (err) {\n      console.error(err);\n      this.openmct.notifications.error(`Error saving objects: ${err}`);\n    } finally {\n      this.openmct.objects.destroyMutable(parentDomainObject);\n      dialog.dismiss();\n    }\n  }\n\n  /**\n   * @private\n   */\n  _onCancel() {\n    this.#transaction.cancel().then(() => {\n      this.openmct.objects.endTransaction();\n      this.#transaction = null;\n    });\n  }\n\n  /**\n   * @private\n   */\n  async _navigateAndEdit(domainObject, parentDomainObjectpath) {\n    let objectPath;\n    let self = this;\n    if (parentDomainObjectpath) {\n      objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath);\n    } else {\n      objectPath = await this.openmct.objects.getOriginalPath(domainObject.identifier);\n    }\n\n    const url =\n      '#/browse/' +\n      objectPath\n        .map((object) => object && this.openmct.objects.makeKeyString(object.identifier))\n        .reverse()\n        .join('/');\n\n    function editObject() {\n      const objectView = self.openmct.objectViews.get(domainObject, objectPath)[0];\n      const canEdit =\n        objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);\n\n      if (canEdit) {\n        self.openmct.editor.edit();\n      }\n    }\n\n    this.openmct.router.once('afterNavigation', editObject);\n\n    this.openmct.router.navigate(url);\n  }\n\n  /**\n   * @private\n   */\n  _showCreateForm(type, parentDomainObject) {\n    const typeDefinition = this.openmct.types.get(type);\n    const definition = typeDefinition.definition;\n    const domainObject = {\n      name: `Unnamed ${definition.name}`,\n      type,\n      identifier: {\n        key: uuid(),\n        namespace: parentDomainObject.identifier.namespace\n      }\n    };\n\n    this.domainObject = this.openmct.objects.toMutable(domainObject);\n\n    if (definition.initialize) {\n      definition.initialize(this.domainObject);\n    }\n\n    const createWizard = new CreateWizard(this.openmct, this.domainObject, parentDomainObject);\n    const formStructure = createWizard.getFormStructure(true);\n    formStructure.title = 'Create a New ' + definition.name;\n\n    this.startTransaction();\n\n    this.openmct.forms\n      .showForm(formStructure)\n      .then(this._onSave.bind(this))\n      .catch(this._onCancel.bind(this))\n      .finally(() => {\n        this.openmct.objects.destroyMutable(this.domainObject);\n      });\n  }\n\n  startTransaction() {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.#transaction = this.openmct.objects.startTransaction();\n    }\n  }\n\n  async saveTransaction() {\n    if (!this.#transaction) {\n      return;\n    }\n\n    await this.#transaction.commit();\n    this.openmct.objects.endTransaction();\n    this.#transaction = null;\n  }\n}\n\nexport { CREATE_ACTION_KEY };\n\nexport default CreateAction;\n"
  },
  {
    "path": "src/plugins/formActions/CreateActionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { debounce } from 'lodash';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport { CREATE_ACTION_KEY } from './CreateAction.js';\n\nlet parentObject;\nlet parentObjectPath;\nlet unObserve;\n\ndescribe('The create action plugin', () => {\n  let openmct;\n\n  const TYPES = [\n    'clock',\n    'conditionWidget',\n    'conditionWidget',\n    'example.imagery',\n    'example.state-generator',\n    'flexible-layout',\n    'folder',\n    'generator',\n    'hyperlink',\n    'LadTable',\n    'LadTableSet',\n    'layout',\n    'mmgis',\n    'notebook',\n    'plan',\n    'table',\n    'tabs',\n    'telemetry-mean',\n    'telemetry.plot.bar-graph',\n    'telemetry.plot.overlay',\n    'telemetry.plot.stacked',\n    'time-strip',\n    'timer',\n    'webpage'\n  ];\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('creates new objects for a', () => {\n    beforeEach(() => {\n      parentObject = {\n        name: 'mock folder',\n        type: 'folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        },\n        composition: []\n      };\n      parentObjectPath = [parentObject];\n\n      spyOn(openmct.objects, 'save');\n      openmct.objects.save.and.callThrough();\n      spyOn(openmct.forms, 'showForm');\n      openmct.forms.showForm.and.callFake((formStructure) => {\n        return Promise.resolve({\n          name: 'test',\n          notes: 'test notes',\n          location: parentObjectPath\n        });\n      });\n    });\n\n    afterEach(() => {\n      parentObject = null;\n      unObserve();\n    });\n\n    TYPES.forEach((type) => {\n      it(`type ${type}`, (done) => {\n        function callback(newObject) {\n          const composition = newObject.composition;\n\n          openmct.objects.get(composition[0]).then((object) => {\n            expect(object.type).toEqual(type);\n            expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier));\n\n            done();\n          });\n        }\n\n        const deBouncedCallback = debounce(callback, 300);\n        unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);\n\n        const createAction = openmct.actions.getAction(CREATE_ACTION_KEY);\n        createAction.invoke(type, parentObject);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/formActions/CreateWizard.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class CreateWizard {\n  constructor(openmct, domainObject, parent) {\n    this.openmct = openmct;\n\n    this.domainObject = domainObject;\n    this.type = openmct.types.get(domainObject.type);\n\n    this.model = domainObject;\n    this.parent = parent;\n    this.properties = this.type.definition.form || [];\n  }\n\n  addNotes(sections) {\n    const row = {\n      control: 'textarea',\n      cssClass: 'l-input-lg',\n      key: 'notes',\n      name: 'Notes',\n      required: false,\n      value: this.domainObject.notes\n    };\n\n    sections.forEach((section) => {\n      if (section.name !== 'Properties') {\n        return;\n      }\n\n      section.rows.unshift(row);\n    });\n  }\n\n  addTitle(sections) {\n    const row = {\n      control: 'textfield',\n      cssClass: 'l-input-lg',\n      key: 'name',\n      name: 'Title',\n      pattern: `\\\\S+`,\n      required: true,\n      value: this.domainObject.name\n    };\n\n    sections.forEach((section) => {\n      if (section.name !== 'Properties') {\n        return;\n      }\n\n      section.rows.unshift(row);\n    });\n  }\n\n  /**\n   * Get the form model for this wizard; this is a description\n   * that will be rendered to an HTML form. See the\n   * platform/forms bundle\n   * @param {boolean} includeLocation if true, a 'location' section\n   * will be included that will allow the user to select the location\n   * of the newly created object, otherwise the .location property of\n   * the model will be used.\n   */\n  getFormStructure(includeLocation) {\n    let sections = [];\n    let domainObject = this.domainObject;\n    let self = this;\n\n    sections.push({\n      name: 'Properties',\n      rows: this.properties\n        .map((property) => {\n          const row = JSON.parse(JSON.stringify(property));\n          row.value = this.getValue(row);\n          if (property.validate) {\n            row.validate = property.validate;\n          }\n\n          return row;\n        })\n        .filter((row) => row && row.control)\n    });\n\n    this.addNotes(sections);\n    this.addTitle(sections);\n\n    // Ensure there is always a 'save in' section\n    if (includeLocation) {\n      function validateLocation(data) {\n        const policyCheck = self.openmct.composition.checkPolicy(data.value[0], domainObject);\n        const parentIsPersistable = self.openmct.objects.isPersistable(data.value[0].identifier);\n\n        return policyCheck && parentIsPersistable;\n      }\n\n      sections.push({\n        name: 'Location',\n        cssClass: 'grows',\n        rows: [\n          {\n            name: 'Save In',\n            cssClass: 'grows',\n            control: 'locator',\n            domainObject,\n            required: true,\n            parent: this.parent,\n            validate: validateLocation.bind(this),\n            key: 'location'\n          }\n        ]\n      });\n    }\n\n    return {\n      sections\n    };\n  }\n\n  getValue(row) {\n    if (row.property) {\n      return row.property.reduce((acc, property) => acc && acc[property], this.domainObject);\n    } else {\n      return this.domainObject[row.key];\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/formActions/EditPropertiesAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\n\nimport CreateWizard from './CreateWizard.js';\nimport PropertiesAction from './PropertiesAction.js';\nconst EDIT_PROPERTIES_ACTION_KEY = 'properties';\n\nclass EditPropertiesAction extends PropertiesAction {\n  constructor(openmct) {\n    super(openmct);\n\n    this.name = 'Edit Properties...';\n    this.key = EDIT_PROPERTIES_ACTION_KEY;\n    this.description = 'Edit properties of this object.';\n    this.cssClass = 'major icon-pencil';\n    this.hideInDefaultMenu = true;\n    this.group = 'action';\n    this.priority = 10;\n    this.formProperties = {};\n  }\n\n  appliesTo(objectPath) {\n    const object = objectPath[0];\n    const definition = this._getTypeDefinition(object.type);\n    const persistable = this.openmct.objects.isPersistable(object.identifier);\n\n    return persistable && definition && definition.creatable && !object.locked;\n  }\n\n  invoke(objectPath) {\n    return this._showEditForm(objectPath);\n  }\n\n  /**\n   * @private\n   */\n  async _onSave(changes) {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.openmct.objects.startTransaction();\n    }\n\n    try {\n      Object.entries(changes).forEach(([key, value]) => {\n        const existingValue = this.domainObject[key];\n        if (!Array.isArray(existingValue) && typeof existingValue === 'object') {\n          value = _.merge(existingValue, value);\n        }\n\n        this.openmct.objects.mutate(this.domainObject, key, value);\n      });\n      const transaction = this.openmct.objects.getActiveTransaction();\n      await transaction.commit();\n      this.openmct.objects.endTransaction();\n    } catch (error) {\n      this.openmct.notifications.error('Error saving objects');\n      console.error(error);\n    }\n  }\n\n  /**\n   * @private\n   */\n  _onCancel() {\n    //noop\n  }\n\n  /**\n   * @private\n   */\n  _showEditForm(objectPath) {\n    this.domainObject = objectPath[0];\n\n    const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]);\n    const formStructure = createWizard.getFormStructure(false);\n    formStructure.title = 'Edit ' + this.domainObject.name;\n\n    return this.openmct.forms\n      .showForm(formStructure)\n      .then(this._onSave.bind(this))\n      .catch(this._onCancel.bind(this));\n  }\n}\n\nexport { EDIT_PROPERTIES_ACTION_KEY };\n\nexport default EditPropertiesAction;\n"
  },
  {
    "path": "src/plugins/formActions/PropertiesAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nexport default class PropertiesAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n  }\n\n  /**\n   * @private\n   */\n  _getTypeDefinition(type) {\n    const TypeDefinition = this.openmct.types.get(type);\n\n    return TypeDefinition.definition;\n  }\n}\n"
  },
  {
    "path": "src/plugins/formActions/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport CreateAction from './CreateAction.js';\nimport EditPropertiesAction from './EditPropertiesAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new EditPropertiesAction(openmct));\n    openmct.actions.register(new CreateAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/formActions/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { debounce } from 'lodash';\nimport { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\ndescribe('EditPropertiesAction plugin', () => {\n  let editPropertiesAction;\n  let openmct;\n  let element;\n\n  beforeEach((done) => {\n    element = document.createElement('div');\n    element.style.display = 'block';\n    element.style.width = '1920px';\n    element.style.height = '1080px';\n\n    openmct = createOpenMct();\n    openmct.on('start', done);\n    openmct.startHeadless(element);\n\n    editPropertiesAction = openmct.actions.getAction('properties');\n  });\n\n  afterEach(() => {\n    editPropertiesAction = null;\n\n    return resetApplicationState(openmct);\n  });\n\n  it('editPropertiesAction exists', () => {\n    expect(editPropertiesAction.key).toEqual('properties');\n  });\n\n  it('edit properties action applies to only persistable objects', () => {\n    spyOn(openmct.objects, 'isPersistable').and.returnValue(true);\n\n    const domainObject = {\n      name: 'mock folder',\n      type: 'folder',\n      identifier: {\n        key: 'mock-folder',\n        namespace: ''\n      },\n      composition: []\n    };\n    const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);\n    expect(isApplicableTo).toBe(true);\n  });\n\n  it('edit properties action does not apply to non persistable objects', () => {\n    spyOn(openmct.objects, 'isPersistable').and.returnValue(false);\n\n    const domainObject = {\n      name: 'mock folder',\n      type: 'folder',\n      identifier: {\n        key: 'mock-folder',\n        namespace: ''\n      },\n      composition: []\n    };\n    const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);\n    expect(isApplicableTo).toBe(false);\n  });\n\n  it('edit properties action when invoked shows form', (done) => {\n    const domainObject = {\n      name: 'mock folder',\n      notes: 'mock notes',\n      type: 'folder',\n      identifier: {\n        key: 'mock-folder',\n        namespace: ''\n      },\n      modified: 1643065068597,\n      persisted: 1643065068600,\n      composition: []\n    };\n\n    editPropertiesAction\n      .invoke([domainObject])\n      .then(() => {\n        done();\n      })\n      .catch(() => {\n        done();\n      });\n\n    nextTick(() => {\n      const form = document.querySelector('.js-form');\n      const title = form.querySelector('input');\n      expect(title.value).toEqual(domainObject.name);\n\n      const notes = form.querySelector('textArea');\n      expect(notes.value).toEqual(domainObject.notes);\n\n      const buttons = form.querySelectorAll('button');\n      expect(buttons[0].textContent.trim()).toEqual('Ok');\n      expect(buttons[1].textContent.trim()).toEqual('Cancel');\n\n      const clickEvent = createMouseEvent('click');\n      buttons[1].dispatchEvent(clickEvent);\n    });\n  });\n\n  it('edit properties action saves changes', (done) => {\n    const oldName = 'mock folder';\n    const newName = 'renamed mock folder';\n    const domainObject = {\n      name: oldName,\n      notes: 'mock notes',\n      type: 'folder',\n      identifier: {\n        key: 'mock-folder',\n        namespace: ''\n      },\n      modified: 1643065068597,\n      persisted: 1643065068600,\n      composition: []\n    };\n    let unObserve;\n\n    function callback(newObject) {\n      expect(newObject.name).not.toEqual(oldName);\n      expect(newObject.name).toEqual(newName);\n\n      unObserve();\n      done();\n    }\n\n    const deBouncedCallback = debounce(callback, 300);\n    unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);\n\n    editPropertiesAction.invoke([domainObject]);\n\n    nextTick(() => {\n      const form = document.querySelector('.js-form');\n      const title = form.querySelector('input');\n      const notes = form.querySelector('textArea');\n\n      const buttons = form.querySelectorAll('button');\n      expect(buttons[0].textContent.trim()).toEqual('Ok');\n      expect(buttons[1].textContent.trim()).toEqual('Cancel');\n\n      expect(title.value).toEqual(domainObject.name);\n      expect(notes.value).toEqual(domainObject.notes);\n\n      // change input field value and dispatch event for it\n      title.focus();\n      title.value = newName;\n      title.dispatchEvent(new Event('input'));\n      title.blur();\n\n      const clickEvent = createMouseEvent('click');\n      buttons[0].dispatchEvent(clickEvent);\n    });\n  });\n\n  it('edit properties action discards changes', (done) => {\n    const name = 'mock folder';\n    const domainObject = {\n      name,\n      notes: 'mock notes',\n      type: 'folder',\n      identifier: {\n        key: 'mock-folder',\n        namespace: ''\n      },\n      modified: 1643065068597,\n      persisted: 1643065068600,\n      composition: []\n    };\n\n    editPropertiesAction\n      .invoke([domainObject])\n      .then(() => {\n        expect(domainObject.name).toEqual(name);\n        done();\n      })\n      .catch(() => {\n        expect(domainObject.name).toEqual(name);\n        done();\n      });\n\n    const form = document.querySelector('.js-form');\n    const buttons = form.querySelectorAll('button');\n    const clickEvent = createMouseEvent('click');\n    buttons[1].dispatchEvent(clickEvent);\n  });\n});\n"
  },
  {
    "path": "src/plugins/gauge/GaugeCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function GaugeCompositionPolicy(openmct) {\n  return {\n    allow: function (parent, child) {\n      if (parent.type === 'gauge') {\n        return openmct.telemetry.hasNumericTelemetry(child);\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/gauge/GaugePlugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport GaugeFormController from './components/GaugeFormController.vue';\nimport GaugeCompositionPolicy from './GaugeCompositionPolicy.js';\nimport gaugeStylesInterceptor from './gaugeStylesInterceptor.js';\nimport GaugeViewProvider from './GaugeViewProvider.js';\n\nexport const GAUGE_TYPES = [\n  ['Filled Dial', 'dial-filled'],\n  ['Needle Dial', 'dial-needle'],\n  ['Vertical Meter', 'meter-vertical'],\n  ['Vertical Meter Inverted', 'meter-vertical-inverted'],\n  ['Horizontal Meter', 'meter-horizontal']\n];\n\nexport default function () {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new GaugeViewProvider(openmct));\n    openmct.objects.addGetInterceptor(gaugeStylesInterceptor(openmct));\n    openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));\n    openmct.types.addType('gauge', {\n      name: 'Gauge',\n      creatable: true,\n      description:\n        \"Graphically visualize a telemetry element's current value between a minimum and maximum.\",\n      cssClass: 'icon-gauge',\n      initialize(domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          gaugeController: {\n            gaugeType: GAUGE_TYPES[0][1],\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: true,\n            limitLow: 10,\n            limitHigh: 90,\n            max: 100,\n            min: 0,\n            precision: 2\n          },\n          objectStyles: {}\n        };\n      },\n      form: [\n        {\n          name: 'Gauge type',\n          options: GAUGE_TYPES.map((type) => {\n            return {\n              name: type[0],\n              value: type[1]\n            };\n          }),\n          control: 'select',\n          cssClass: 'l-input-sm',\n          key: 'gaugeController',\n          property: ['configuration', 'gaugeController', 'gaugeType']\n        },\n        {\n          name: 'Display current value',\n          control: 'toggleSwitch',\n          cssClass: 'l-input',\n          key: 'isDisplayCurVal',\n          property: ['configuration', 'gaugeController', 'isDisplayCurVal']\n        },\n        {\n          name: 'Display units',\n          control: 'toggleSwitch',\n          cssClass: 'l-input',\n          key: 'isDisplayUnits',\n          property: ['configuration', 'gaugeController', 'isDisplayUnits']\n        },\n        {\n          name: 'Display range values',\n          control: 'toggleSwitch',\n          cssClass: 'l-input',\n          key: 'isDisplayMinMax',\n          property: ['configuration', 'gaugeController', 'isDisplayMinMax']\n        },\n        {\n          name: 'Float precision',\n          control: 'numberfield',\n          cssClass: 'l-input-sm',\n          key: 'precision',\n          property: ['configuration', 'gaugeController', 'precision']\n        },\n        {\n          name: 'Value ranges and limits',\n          control: 'gauge-controller',\n          cssClass: 'l-input',\n          key: 'gaugeController',\n          required: false,\n          hideFromInspector: true,\n          property: ['configuration', 'gaugeController'],\n          validate: ({ value }, callback) => {\n            if (value.isUseTelemetryLimits) {\n              return true;\n            }\n\n            const { min, max, limitLow, limitHigh } = value;\n            const valid = {\n              min: true,\n              max: true,\n              limitLow: true,\n              limitHigh: true\n            };\n\n            if (min === '') {\n              valid.min = false;\n            }\n\n            if (max === '') {\n              valid.max = false;\n            }\n\n            if (max < min) {\n              valid.min = false;\n              valid.max = false;\n            }\n\n            if (limitLow !== '') {\n              valid.limitLow = min <= limitLow && limitLow < max;\n            }\n\n            if (limitHigh !== '') {\n              valid.limitHigh = min < limitHigh && limitHigh <= max;\n            }\n\n            if (\n              valid.limitLow &&\n              valid.limitHigh &&\n              limitLow !== '' &&\n              limitHigh !== '' &&\n              limitLow > limitHigh\n            ) {\n              valid.limitLow = false;\n              valid.limitHigh = false;\n            }\n\n            if (callback) {\n              callback(valid);\n            }\n\n            return valid.min && valid.max && valid.limitLow && valid.limitHigh;\n          }\n        }\n      ]\n    });\n    openmct.composition.addPolicy(new GaugeCompositionPolicy(openmct).allow);\n  };\n\n  function getGaugeFormController(openmct) {\n    let destroyComponent;\n\n    return {\n      show(element, model, onChange) {\n        const { vNode, destroy } = mount(\n          {\n            el: element,\n            components: {\n              GaugeFormController\n            },\n            provide: {\n              openmct\n            },\n            data() {\n              return {\n                model,\n                onChange\n              };\n            },\n            template: `<GaugeFormController :model=\"model\" @on-change=\"onChange\"></GaugeFormController>`\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        destroyComponent = destroy;\n\n        return vNode.componentInstance;\n      },\n      destroy() {\n        destroyComponent();\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/gauge/GaugePluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { debounce } from 'lodash';\nimport { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nlet gaugeDomainObject = {\n  identifier: {\n    key: 'gauge',\n    namespace: 'test-namespace'\n  },\n  type: 'gauge',\n  composition: []\n};\n\ndescribe('Gauge plugin', () => {\n  let openmct;\n  let child;\n  let gaugeHolder;\n\n  beforeEach((done) => {\n    gaugeHolder = document.createElement('div');\n    gaugeHolder.style.display = 'block';\n    gaugeHolder.style.width = '1920px';\n    gaugeHolder.style.height = '1080px';\n\n    child = document.createElement('div');\n    gaugeHolder.appendChild(child);\n\n    openmct = createOpenMct();\n    openmct.on('start', done);\n\n    openmct.install(openmct.plugins.Gauge());\n\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('Plugin installed by default', () => {\n    const GaugeType = openmct.types.get('gauge');\n\n    expect(GaugeType).not.toBeNull();\n    expect(GaugeType.definition.name).toEqual('Gauge');\n  });\n\n  it('Gauge plugin is creatable', () => {\n    const GaugeType = openmct.types.get('gauge');\n\n    expect(GaugeType.definition.creatable).toBeTrue();\n  });\n\n  it('Gauge plugin is creatable', () => {\n    const GaugeType = openmct.types.get('gauge');\n\n    expect(GaugeType.definition.creatable).toBeTrue();\n  });\n\n  it('Gauge form controller', () => {\n    const gaugeController = openmct.forms.getFormControl('gauge-controller');\n    expect(gaugeController).toBeDefined();\n  });\n\n  describe('Gauge with Filled Dial', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n    let randomValue;\n\n    const minValue = -1;\n    const maxValue = 1;\n\n    beforeEach(() => {\n      randomValue = Math.random();\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'dial-filled',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: false,\n            limitLow: -0.9,\n            limitHigh: 0.9,\n            max: maxValue,\n            min: minValue,\n            precision: 2\n          }\n        },\n        composition: [\n          {\n            namespace: 'test-namespace',\n            key: 'test-object'\n          }\n        ],\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      spyOn(openmct.telemetry, 'getMetadata').and.returnValue({\n        valuesForHints: () => {\n          return [\n            {\n              source: 'sin'\n            }\n          ];\n        },\n        value: () => 1\n      });\n      spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({\n        parse: () => {\n          return 2000;\n        }\n      });\n      spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({\n        sin: {\n          format: (datum) => {\n            return randomValue;\n          }\n        }\n      });\n      spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });\n      spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));\n      spyOn(openmct.time, 'getBounds').and.returnValue({\n        start: 1000,\n        end: 5000\n      });\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');\n      const valueElement = gaugeHolder.querySelector('.js-dial-current-value');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders correct min max values', () => {\n      expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(\n        new RegExp(`\\\\s*${minValue}\\\\s*${maxValue}\\\\s*`)\n      );\n    });\n\n    it('renders correct current value', (done) => {\n      function WatchUpdateValue() {\n        const textElement = gaugeHolder.querySelector('.js-dial-current-value');\n        expect(\n          Number(textElement.textContent).toFixed(\n            gaugeViewObject.configuration.gaugeController.precision\n          )\n        ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));\n        done();\n      }\n\n      const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);\n      nextTick(debouncedWatchUpdate);\n    });\n  });\n\n  describe('Gauge with Needle Dial', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n    let randomValue;\n\n    const minValue = -1;\n    const maxValue = 1;\n    beforeEach(() => {\n      randomValue = Math.random();\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'dial-needle',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: false,\n            limitLow: -0.9,\n            limitHigh: 0.9,\n            max: maxValue,\n            min: minValue,\n            precision: 2\n          }\n        },\n        composition: [\n          {\n            namespace: 'test-namespace',\n            key: 'test-object'\n          }\n        ],\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      spyOn(openmct.telemetry, 'getMetadata').and.returnValue({\n        valuesForHints: () => {\n          return [\n            {\n              source: 'sin'\n            }\n          ];\n        },\n        value: () => 1\n      });\n      spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({\n        parse: () => {\n          return 2000;\n        }\n      });\n      spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({\n        sin: {\n          format: (datum) => {\n            return randomValue;\n          }\n        }\n      });\n      spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });\n      spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));\n      spyOn(openmct.time, 'getBounds').and.returnValue({\n        start: 1000,\n        end: 5000\n      });\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');\n      const valueElement = gaugeHolder.querySelector('.js-dial-current-value');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders correct min max values', () => {\n      expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(\n        new RegExp(`\\\\s*${minValue}\\\\s*${maxValue}\\\\s*`)\n      );\n    });\n\n    it('renders correct current value', (done) => {\n      function WatchUpdateValue() {\n        const textElement = gaugeHolder.querySelector('.js-dial-current-value');\n        expect(\n          Number(textElement.textContent).toFixed(\n            gaugeViewObject.configuration.gaugeController.precision\n          )\n        ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));\n        done();\n      }\n\n      const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);\n      nextTick(debouncedWatchUpdate);\n    });\n  });\n\n  describe('Gauge with Vertical Meter', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n    let randomValue;\n\n    const minValue = -1;\n    const maxValue = 1;\n    beforeEach(() => {\n      randomValue = Math.random();\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'meter-vertical',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: false,\n            limitLow: -0.9,\n            limitHigh: 0.9,\n            max: maxValue,\n            min: minValue,\n            precision: 2\n          }\n        },\n        composition: [\n          {\n            namespace: 'test-namespace',\n            key: 'test-object'\n          }\n        ],\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      spyOn(openmct.telemetry, 'getMetadata').and.returnValue({\n        valuesForHints: () => {\n          return [\n            {\n              source: 'sin'\n            }\n          ];\n        },\n        value: () => 1\n      });\n      spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({\n        parse: () => {\n          return 2000;\n        }\n      });\n      spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({\n        sin: {\n          format: (datum) => {\n            return randomValue;\n          }\n        }\n      });\n      spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });\n      spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));\n      spyOn(openmct.time, 'getBounds').and.returnValue({\n        start: 1000,\n        end: 5000\n      });\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');\n      const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders correct min max values', () => {\n      expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toMatch(\n        new RegExp(`\\\\s*${maxValue}\\\\s*${minValue}\\\\s*`)\n      );\n    });\n\n    it('renders correct current value', (done) => {\n      function WatchUpdateValue() {\n        const textElement = gaugeHolder.querySelector('.js-gauge-current-value');\n        expect(\n          Number(textElement.textContent).toFixed(\n            gaugeViewObject.configuration.gaugeController.precision\n          )\n        ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));\n        done();\n      }\n\n      const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);\n      nextTick(debouncedWatchUpdate);\n    });\n  });\n\n  describe('Gauge with Vertical Meter Inverted', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n\n    beforeEach(() => {\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'meter-vertical',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: false,\n            limitLow: -0.9,\n            limitHigh: 0.9,\n            max: 1,\n            min: -1,\n            precision: 2\n          }\n        },\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');\n      const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n  });\n\n  describe('Gauge with Horizontal Meter', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n\n    beforeEach(() => {\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'meter-vertical',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: false,\n            limitLow: -0.9,\n            limitHigh: 0.9,\n            max: 1,\n            min: -1,\n            precision: 2\n          }\n        },\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.c-gauge__range');\n      const curveElement = gaugeHolder.querySelector('.c-meter');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n  });\n\n  describe('Gauge with Filled Dial with Use Telemetry Limits', () => {\n    let gaugeViewProvider;\n    let gaugeView;\n    let gaugeViewObject;\n    let mutablegaugeObject;\n    let randomValue;\n\n    beforeEach(() => {\n      randomValue = Math.random();\n\n      gaugeViewObject = {\n        ...gaugeDomainObject,\n        configuration: {\n          gaugeController: {\n            gaugeType: 'dial-filled',\n            isDisplayMinMax: true,\n            isDisplayCurVal: true,\n            isDisplayUnits: true,\n            isUseTelemetryLimits: true,\n            limitLow: 10,\n            limitHigh: 90,\n            max: 100,\n            min: 0,\n            precision: 2\n          }\n        },\n        composition: [\n          {\n            namespace: 'test-namespace',\n            key: 'test-object'\n          }\n        ],\n        id: 'test-object',\n        name: 'gauge'\n      };\n\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n\n      const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);\n      gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge');\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      spyOn(openmct.telemetry, 'getMetadata').and.returnValue({\n        valuesForHints: () => {\n          return [\n            {\n              source: 'sin'\n            }\n          ];\n        },\n        value: () => 1\n      });\n      spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({\n        parse: () => {\n          return 2000;\n        }\n      });\n      spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({\n        sin: {\n          format: (datum) => {\n            return randomValue;\n          }\n        }\n      });\n      spyOn(openmct.telemetry, 'getLimits').and.returnValue({\n        limits: () =>\n          Promise.resolve({\n            CRITICAL: {\n              high: 0.99,\n              low: -0.99\n            }\n          })\n      });\n      spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));\n      spyOn(openmct.time, 'getBounds').and.returnValue({\n        start: 1000,\n        end: 5000\n      });\n\n      return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {\n        mutablegaugeObject = mutableObject;\n        gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);\n        gaugeView.show(child, false, { renderWhenVisible });\n\n        return nextTick();\n      });\n    });\n\n    afterEach(() => {\n      gaugeView.destroy();\n\n      return resetApplicationState(openmct);\n    });\n\n    it('provides gauge view', () => {\n      expect(gaugeViewProvider).toBeDefined();\n    });\n\n    it('renders gauge element', () => {\n      const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');\n      expect(gaugeElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');\n      const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');\n      const valueElement = gaugeHolder.querySelector('.js-dial-current-value');\n\n      const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders correct min max values', () => {\n      const { min, max } = gaugeViewObject.configuration.gaugeController;\n      expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(\n        new RegExp(`\\\\s*${min}\\\\s*${max}\\\\s*`)\n      );\n    });\n\n    it('renders correct current value', (done) => {\n      function WatchUpdateValue() {\n        const textElement = gaugeHolder.querySelector('.js-dial-current-value');\n        expect(\n          Number(textElement.textContent).toFixed(\n            gaugeViewObject.configuration.gaugeController.precision\n          )\n        ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));\n        done();\n      }\n\n      const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);\n      nextTick(debouncedWatchUpdate);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/gauge/GaugeViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport GaugeComponent from './components/GaugeComponent.vue';\n\nexport default function GaugeViewProvider(openmct) {\n  return {\n    key: 'gauge',\n    name: 'Gauge',\n    cssClass: 'icon-gauge',\n    canView: function (domainObject) {\n      return domainObject.type === 'gauge';\n    },\n    canEdit: function (domainObject) {\n      if (domainObject.type === 'gauge') {\n        return true;\n      }\n    },\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element, isEditing, { renderWhenVisible }) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                GaugeComponent\n              },\n              provide: {\n                openmct,\n                domainObject,\n                composition: openmct.composition.get(domainObject),\n                renderWhenVisible\n              },\n              template: '<gauge-component></gauge-component>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/gauge/components/GaugeComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <!-- TODO: Better a11y for Gauges. See https://github.com/nasa/openmct/issues/7790 -->\n  <div\n    ref=\"gaugeWrapper\"\n    class=\"c-gauge__wrapper js-gauge-wrapper\"\n    :class=\"gaugeClasses\"\n    :title=\"gaugeTitle\"\n    :aria-valuemin=\"rangeLow\"\n    :aria-valuemax=\"rangeHigh\"\n    :aria-valuenow=\"curVal\"\n    :aria-valuetext=\"`Current value: ${curVal}`\"\n  >\n    <template v-if=\"typeDial\">\n      <svg\n        ref=\"gauge\"\n        class=\"c-gauge c-dial\"\n        viewBox=\"0 0 10 10\"\n        role=\"meter\"\n        @mouseover.ctrl=\"showToolTip\"\n        @mouseleave=\"hideToolTip\"\n      >\n        <g class=\"c-dial__masks\">\n          <mask id=\"gaugeValueMask\">\n            <path\n              d=\"M1.8926 8.1074C1.09734 7.31215 0.605469 6.21352 0.605469 5C0.605469 2.57297 2.57297 0.605469 5 0.605469C7.42703 0.605469 9.39453 2.57297 9.39453 5C9.39453 6.21352 8.90266 7.31215 8.1074 8.1074L7.14066 7.14066C7.6885 6.59281 8.02734 5.83598 8.02734 5C8.02734 3.32804 6.67196 1.97266 5 1.97266C3.32804 1.97266 1.97266 3.32804 1.97266 5C1.97266 5.83598 2.3115 6.59281 2.85934 7.14066L1.8926 8.1074Z\"\n              fill=\"white\"\n            />\n          </mask>\n          <mask id=\"gaugeBgMask\">\n            <path\n              d=\"M8.53553 8.53553C9.44036 7.63071 10 6.38071 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5C0 6.38071 0.559644 7.63071 1.46447 8.53553L2.85934 7.14066C2.3115 6.59281 1.97266 5.83598 1.97266 5C1.97266 3.32804 3.32804 1.97266 5 1.97266C6.67196 1.97266 8.02734 3.32804 8.02734 5C8.02734 5.83598 7.6885 6.59281 7.14066 7.14066L8.53553 8.53553Z\"\n              fill=\"white\"\n            />\n          </mask>\n        </g>\n\n        <g class=\"c-dial__graphics\" mask=\"url(#gaugeBgMask)\">\n          <rect class=\"c-dial__bg\" x=\"0\" y=\"0\" width=\"10\" height=\"10\" />\n          <g\n            v-if=\"isDialLowLimit\"\n            class=\"c-dial__limit-low\"\n            :style=\"`transform: rotate(${dialLowLimitDeg}deg)`\"\n          >\n            <rect\n              v-if=\"isDialLowLimitLow\"\n              class=\"c-dial__low-limit__low\"\n              x=\"5\"\n              y=\"5\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialLowLimitMid\"\n              class=\"c-dial__low-limit__mid\"\n              x=\"5\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialLowLimitHigh\"\n              class=\"c-dial__low-limit__high\"\n              x=\"0\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n          </g>\n          <g\n            v-if=\"isDialHighLimit\"\n            class=\"c-dial__limit-high\"\n            :style=\"`transform: rotate(${dialHighLimitDeg}deg)`\"\n          >\n            <rect\n              v-if=\"isDialHighLimitLow\"\n              class=\"c-dial__high-limit__low\"\n              x=\"0\"\n              y=\"5\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialHighLimitMid\"\n              class=\"c-dial__high-limit__mid\"\n              x=\"0\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialHighLimitHigh\"\n              class=\"c-dial__high-limit__high\"\n              x=\"5\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n          </g>\n        </g>\n\n        <g class=\"c-dial__graphics\" mask=\"url(#gaugeValueMask)\">\n          <g\n            v-if=\"typeFilledDial\"\n            class=\"c-dial__filled-value\"\n            :style=\"`transform: rotate(${degValueFilledDial}deg)`\"\n          >\n            <rect\n              v-if=\"isDialFilledValueLow\"\n              class=\"c-dial__filled-value__low\"\n              x=\"5\"\n              y=\"5\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialFilledValueMid\"\n              class=\"c-dial__filled-value__mid\"\n              x=\"5\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n            <rect\n              v-if=\"isDialFilledValueHigh\"\n              class=\"c-dial__filled-value__high\"\n              x=\"0\"\n              y=\"0\"\n              width=\"5\"\n              height=\"5\"\n            />\n          </g>\n          <g\n            v-if=\"valueInBounds && typeNeedleDial\"\n            class=\"c-dial__needle-value\"\n            :style=\"`transform: rotate(${degValue}deg)`\"\n          >\n            <path\n              d=\"M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z\"\n            />\n          </g>\n          <path\n            id=\"dialTextPath\"\n            class=\"c-dial__range-msg-path\"\n            d=\"M8.3501 5.0001C8.3501 6.85025 6.85025 8.3501 5.0001 8.3501C3.14994 8.3501 1.6501 6.85025 1.6501 5.0001C1.6501 3.14994 3.14994 1.6501 5.0001 1.6501C6.85025 1.6501 8.3501 3.14994 8.3501 5.0001Z\"\n            fill=\"none\"\n            style=\"transform-origin: center; transform: rotate(182deg)\"\n          />\n        </g>\n        <g class=\"c-dial__text\">\n          <text\n            v-if=\"displayUnits\"\n            x=\"50%\"\n            y=\"70%\"\n            text-anchor=\"middle\"\n            class=\"c-gauge__units\"\n            font-size=\"8%\"\n          >\n            {{ units }}\n          </text>\n\n          <g\n            v-if=\"displayMinMax\"\n            class=\"c-dial__range-text js-gauge-dial-range\"\n            :font-size=\"rangeFontSize\"\n          >\n            <text transform=\"translate(1.5 8.7) rotate(-45)\" dominant-baseline=\"hanging\">\n              {{ rangeLow }}\n            </text>\n            <text\n              transform=\"translate(8.4 8.7) rotate(45)\"\n              dominant-baseline=\"hanging\"\n              text-anchor=\"end\"\n            >\n              {{ rangeHigh }}\n            </text>\n          </g>\n        </g>\n\n        <svg\n          v-if=\"!valueInBounds && valueExpected\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 512 512\"\n          xml:space=\"preserve\"\n          class=\"c-dial__value-oor-indicator\"\n          x=\"45%\"\n          y=\"80%\"\n          width=\"1\"\n          height=\"1\"\n        >\n          <path\n            d=\"M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z\"\n          />\n        </svg>\n\n        <svg\n          class=\"c-gauge__current-value-text-wrapper\"\n          :viewBox=\"curValViewBox\"\n          preserveAspectRatio=\"xMidYMid meet\"\n        >\n          <rect class=\"svg-viewbox-debug\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" />\n          <text\n            class=\"c-dial__current-value-text js-dial-current-value\"\n            font-size=\"3.5\"\n            lengthAdjust=\"spacing\"\n            text-anchor=\"middle\"\n            dominant-baseline=\"middle\"\n            :aria-label=\"`gauge value of ${curVal}`\"\n            x=\"50%\"\n            y=\"50%\"\n          >\n            <template v-if=\"displayCurVal\">\n              <tspan>{{ curVal }}</tspan>\n            </template>\n          </text>\n        </svg>\n      </svg>\n    </template>\n\n    <template v-if=\"typeMeter\">\n      <div class=\"c-meter\" role=\"meter\" @mouseover.ctrl=\"showToolTip\" @mouseleave=\"hideToolTip\">\n        <div v-if=\"displayMinMax\" class=\"c-gauge__range c-meter__range js-gauge-meter-range\">\n          <div class=\"c-meter__range__high\">{{ rangeHigh }}</div>\n          <div class=\"c-meter__range__low\">{{ rangeLow }}</div>\n        </div>\n        <div class=\"c-meter__bg\">\n          <div v-if=\"!valueInBounds && valueExpected\" class=\"c-meter__value-oor-indicator\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 512 512\"\n              :preserveAspectRatio=\"meterOutOfRangeIndicatorAspectRatio\"\n            >\n              <path\n                d=\"M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z\"\n              />\n            </svg>\n          </div>\n\n          <template v-if=\"typeMeterVertical\">\n            <div\n              v-if=\"valueExpected\"\n              class=\"c-meter__value\"\n              :class=\"{ 'c-meter__value-needle': typeNeedleMeter }\"\n              :style=\"`transform: translateY(${meterValueToPerc}%)`\"\n            ></div>\n\n            <div\n              v-if=\"isMeterLimitHigh\"\n              class=\"c-meter__limit-high\"\n              :style=\"`height: ${meterHighLimitPerc}%`\"\n            ></div>\n\n            <div\n              v-if=\"isMeterLimitLow\"\n              class=\"c-meter__limit-low\"\n              :style=\"`height: ${meterLowLimitPerc}%`\"\n            ></div>\n          </template>\n\n          <template v-if=\"typeMeterHorizontal\">\n            <div\n              v-if=\"valueExpected\"\n              class=\"c-meter__value\"\n              :class=\"{ 'c-meter__value-needle': typeNeedleMeter }\"\n              :style=\"`transform: translateX(${meterValueToPerc * -1}%)`\"\n            ></div>\n\n            <div\n              v-if=\"isMeterLimitHigh\"\n              class=\"c-meter__limit-high\"\n              :style=\"`width: ${meterHighLimitPerc}%`\"\n            ></div>\n\n            <div\n              v-if=\"isMeterLimitLow\"\n              class=\"c-meter__limit-low\"\n              :style=\"`width: ${meterLowLimitPerc}%`\"\n            ></div>\n          </template>\n\n          <svg\n            class=\"c-gauge__current-value-text-wrapper\"\n            :viewBox=\"curValViewBox\"\n            preserveAspectRatio=\"xMidYMid meet\"\n          >\n            <rect class=\"svg-viewbox-debug\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" />\n            <text\n              class=\"c-meter__current-value-text js-gauge-current-value\"\n              font-size=\"4\"\n              lengthAdjust=\"spacing\"\n              text-anchor=\"middle\"\n              :dominant-baseline=\"meterTextBaseline\"\n              x=\"50%\"\n              y=\"50%\"\n            >\n              <template v-if=\"displayCurVal\">\n                <tspan>{{ curVal }}</tspan>\n                <tspan\n                  v-if=\"typeMeterHorizontal && displayUnits\"\n                  class=\"c-gauge__units\"\n                  font-size=\"80%\"\n                >\n                  {{ units }}\n                </tspan>\n                <tspan\n                  v-if=\"typeMeterVertical && displayUnits\"\n                  x=\"50%\"\n                  dy=\"3.5\"\n                  class=\"c-gauge__units\"\n                  font-size=\"80%\"\n                >\n                  {{ units }}\n                </tspan>\n              </template>\n            </text>\n          </svg>\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util.js';\n\nconst LIMIT_PADDING_IN_PERCENT = 10;\nconst DEFAULT_CURRENT_VALUE = '--';\n\nexport default {\n  mixins: [stalenessMixin, tooltipHelpers],\n  inject: ['openmct', 'domainObject', 'composition', 'renderWhenVisible'],\n  data() {\n    let gaugeController = this.domainObject.configuration.gaugeController;\n\n    return {\n      curVal: DEFAULT_CURRENT_VALUE,\n      digits: 3,\n      precision: gaugeController.precision,\n      displayMinMax: gaugeController.isDisplayMinMax,\n      displayCurVal: gaugeController.isDisplayCurVal,\n      displayUnits: gaugeController.isDisplayUnits,\n      limitHigh: gaugeController.limitHigh,\n      limitLow: gaugeController.limitLow,\n      rangeHigh: gaugeController.max,\n      rangeLow: gaugeController.min,\n      gaugeType: gaugeController.gaugeType,\n      showUnits: gaugeController.showUnits,\n      activeTimeSystem: this.openmct.time.getTimeSystem(),\n      units: ''\n    };\n  },\n  computed: {\n    degValue() {\n      return this.percentToDegrees(this.valToPercent(this.curVal));\n    },\n    degValueFilledDial() {\n      if (this.curVal > this.rangeHigh) {\n        return this.percentToDegrees(100);\n      }\n\n      return this.percentToDegrees(this.valToPercent(this.curVal));\n    },\n    dialHighLimitDeg() {\n      return this.percentToDegrees(this.valToPercent(this.limitHigh));\n    },\n    dialLowLimitDeg() {\n      return this.percentToDegrees(this.valToPercent(this.limitLow));\n    },\n    meterOutOfRangeIndicatorAspectRatio() {\n      return this.typeMeterVertical ? 'xMidYMax meet' : 'xMinYMid meet';\n    },\n    meterTextBaseline() {\n      return this.typeMeterVertical ? 'auto' : 'middle';\n    },\n    curValViewBox() {\n      const DIGITS_RATIO = 3;\n      const VIEWBOX_STR = '0 0 X 10';\n\n      return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);\n    },\n    gaugeClasses() {\n      let classes = [`c-gauge--${this.gaugeType}`];\n\n      if (this.isStale) {\n        classes.push('is-stale');\n      }\n\n      return classes;\n    },\n    rangeFontSize() {\n      const CHAR_THRESHOLD = 3;\n      const START_PERC = 8.5;\n      const REDUCE_PERC = 0.8;\n      const RANGE_CHARS_MAX =\n        this.rangeLow && this.rangeHigh\n          ? Math.max(this.rangeLow.toString().length, this.rangeHigh.toString().length)\n          : CHAR_THRESHOLD;\n\n      return this.fontSizeFromChars(RANGE_CHARS_MAX, CHAR_THRESHOLD, START_PERC, REDUCE_PERC);\n    },\n    isDialLowLimit() {\n      return (\n        this.limitLow.toString().length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max')\n      );\n    },\n    isDialLowLimitLow() {\n      return this.dialLowLimitDeg >= getLimitDegree('low', 'q1');\n    },\n    isDialLowLimitMid() {\n      return this.dialLowLimitDeg >= getLimitDegree('low', 'q2');\n    },\n    isDialLowLimitHigh() {\n      return this.dialLowLimitDeg >= getLimitDegree('low', 'q3');\n    },\n    isDialHighLimit() {\n      return (\n        this.limitHigh.toString().length > 0 &&\n        this.dialHighLimitDeg < getLimitDegree('high', 'max')\n      );\n    },\n    isDialHighLimitLow() {\n      return this.dialHighLimitDeg <= getLimitDegree('high', 'max');\n    },\n    isDialHighLimitMid() {\n      return this.dialHighLimitDeg <= getLimitDegree('high', 'q2');\n    },\n    isDialHighLimitHigh() {\n      return this.dialHighLimitDeg <= getLimitDegree('high', 'q3');\n    },\n    isDialFilledValueLow() {\n      return this.degValue >= getLimitDegree('low', 'q1');\n    },\n    isDialFilledValueMid() {\n      return this.degValue >= getLimitDegree('low', 'q2');\n    },\n    isDialFilledValueHigh() {\n      return this.degValue >= getLimitDegree('low', 'q3');\n    },\n    isMeterLimitHigh() {\n      return this.limitHigh.toString().length > 0 && this.meterHighLimitPerc > 0;\n    },\n    isMeterLimitLow() {\n      return this.limitLow.toString().length > 0 && this.meterLowLimitPerc > 0;\n    },\n    gaugeTitle() {\n      return this.valueInBounds\n        ? 'Gauge'\n        : 'Value is currently out of range and cannot be graphically displayed';\n    },\n    typeDial() {\n      return this.matchGaugeType('dial');\n    },\n    typeFilledDial() {\n      return this.matchGaugeType('dial-filled');\n    },\n    typeNeedleDial() {\n      return this.matchGaugeType('dial-needle');\n    },\n    typeMeter() {\n      return this.matchGaugeType('meter');\n    },\n    typeMeterHorizontal() {\n      return this.matchGaugeType('horizontal');\n    },\n    typeMeterVertical() {\n      return this.matchGaugeType('vertical');\n    },\n    typeMeterInverted() {\n      return this.matchGaugeType('inverted');\n    },\n    typeFilledMeter() {\n      return true; // Stubbing in for future capability\n    },\n    typeNeedleMeter() {\n      return false; // Stubbing in for future capability\n    },\n    meterValueToPerc() {\n      const meterDirection = this.typeMeterInverted ? -1 : 1;\n\n      if (this.typeFilledMeter) {\n        // Filled meter is a filled rectangle that is transformed along a vertical or horizontal axis\n        // So never move it below the low range more than 100%, or above the high range more than 0%\n        if (this.curVal <= this.rangeLow) {\n          return meterDirection * 100;\n        }\n\n        if (this.curVal >= this.rangeHigh) {\n          return 0;\n        }\n      }\n\n      return this.valToPercentMeter(this.curVal) * meterDirection;\n    },\n    meterHighLimitPerc() {\n      return this.valToPercentMeter(this.limitHigh);\n    },\n    meterLowLimitPerc() {\n      return 100 - this.valToPercentMeter(this.limitLow);\n    },\n    valueExpected() {\n      if (this.curVal === undefined || Object.is(this.curVal, 'null')) {\n        return false;\n      }\n\n      return this.curVal.toString().indexOf(DEFAULT_CURRENT_VALUE) === -1;\n    },\n    valueInBounds() {\n      return this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh;\n    },\n    timeFormatter() {\n      const timeSystem = this.activeTimeSystem;\n      const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };\n\n      return this.openmct.telemetry.getValueFormatter(metadataValue);\n    }\n  },\n  watch: {\n    curVal(newCurValue) {\n      if (this.digits < newCurValue.toString().length) {\n        this.digits = newCurValue.toString().length;\n      }\n    }\n  },\n  mounted() {\n    this.composition.on('add', this.addedToComposition);\n    this.composition.on('remove', this.removeTelemetryObject);\n\n    this.composition.load();\n\n    this.openmct.time.on('boundsChanged', this.refreshData);\n    this.openmct.time.on('timeSystem', this.setTimeSystem);\n\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  unmounted() {\n    this.composition.off('add', this.addedToComposition);\n    this.composition.off('remove', this.removeTelemetryObject);\n\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n\n    this.openmct.time.off('boundsChanged', this.refreshData);\n    this.openmct.time.off('timeSystem', this.setTimeSystem);\n  },\n  methods: {\n    getLimitDegree: getLimitDegree,\n    addTelemetryObjectAndSubscribe(domainObject) {\n      this.telemetryObject = domainObject;\n      this.request();\n      this.subscribe();\n\n      this.subscribeToStaleness(domainObject);\n    },\n    addedToComposition(domainObject) {\n      if (this.telemetryObject) {\n        this.confirmRemoval(domainObject);\n      } else {\n        this.addTelemetryObjectAndSubscribe(domainObject);\n      }\n    },\n    confirmRemoval(domainObject) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will replace the current telemetry source. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              this.removeFromComposition();\n              this.removeTelemetryObject();\n              this.addTelemetryObjectAndSubscribe(domainObject);\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              this.removeFromComposition(domainObject);\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    fontSizeFromChars(charNum, charThreshold, startPerc, reducePerc) {\n      const fs =\n        charNum <= charThreshold ? startPerc : startPerc - (charNum - charThreshold) * reducePerc;\n\n      return fs.toString() + '%';\n    },\n    matchGaugeType(str) {\n      return this.gaugeType.indexOf(str) !== -1;\n    },\n    percentToDegrees(vPercent) {\n      return this.round((vPercent / 100) * 270 + DIAL_VALUE_DEG_OFFSET, 2);\n    },\n    removeFromComposition(telemetryObject = this.telemetryObject) {\n      this.composition.remove(telemetryObject);\n    },\n    refreshData(bounds, isTick) {\n      if (!isTick) {\n        this.request();\n      }\n    },\n    removeTelemetryObject() {\n      if (this.unsubscribe) {\n        this.unsubscribe();\n        this.unsubscribe = null;\n      }\n\n      this.triggerUnsubscribeFromStaleness(this.domainObject);\n\n      this.curVal = DEFAULT_CURRENT_VALUE;\n      this.formats = null;\n      this.limitHigh = '';\n      this.limitLow = '';\n      this.metadata = null;\n      this.rangeHigh = null;\n      this.rangeLow = null;\n      this.valueKey = null;\n    },\n    request(domainObject = this.telemetryObject) {\n      this.metadata = this.openmct.telemetry.getMetadata(domainObject);\n\n      if (!this.metadata) {\n        return;\n      }\n\n      this.formats = this.openmct.telemetry.getFormatMap(this.metadata);\n      const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);\n      LimitEvaluator.limits().then(this.updateLimits);\n\n      this.valueKey = this.metadata.valuesForHints(['range'])[0].source;\n\n      const options = {\n        strategy: 'latest',\n        timeContext: this.openmct.time.getContextForView([])\n      };\n      this.openmct.telemetry.request(domainObject, options).then((values) => {\n        const length = values.length;\n        this.updateValue(values[length - 1]);\n      });\n\n      this.units = this.metadata.value(this.valueKey).unit || '';\n    },\n    round(val, decimals = this.precision) {\n      let precision = Math.pow(10, decimals);\n\n      return Math.round(val * precision) / precision;\n    },\n    setTimeSystem(timeSystem) {\n      this.activeTimeSystem = timeSystem;\n    },\n    subscribe(domainObject = this.telemetryObject) {\n      this.unsubscribe = this.openmct.telemetry.subscribe(\n        domainObject,\n        this.updateValue.bind(this)\n      );\n    },\n    updateLimits(telemetryLimit) {\n      if (\n        !telemetryLimit ||\n        !this.domainObject.configuration.gaugeController.isUseTelemetryLimits\n      ) {\n        return;\n      }\n\n      let limits = {\n        high: 0,\n        low: 0\n      };\n      if (telemetryLimit.CRITICAL) {\n        limits = telemetryLimit.CRITICAL;\n      } else if (telemetryLimit.DISTRESS) {\n        limits = telemetryLimit.DISTRESS;\n      } else if (telemetryLimit.SEVERE) {\n        limits = telemetryLimit.SEVERE;\n      } else if (telemetryLimit.WARNING) {\n        limits = telemetryLimit.WARNING;\n      } else if (telemetryLimit.WATCH) {\n        limits = telemetryLimit.WATCH;\n      } else {\n        this.openmct.notifications.error(\n          'No limits definition for given telemetry, hiding low and high limits'\n        );\n        this.displayMinMax = false;\n        this.limitHigh = '';\n        this.limitLow = '';\n\n        return;\n      }\n\n      this.limitHigh = this.round(limits.high[this.valueKey]);\n      this.limitLow = this.round(limits.low[this.valueKey]);\n      this.rangeHigh = this.round(\n        this.limitHigh + (this.limitHigh * LIMIT_PADDING_IN_PERCENT) / 100\n      );\n      this.rangeLow = this.round(\n        this.limitLow - Math.abs((this.limitLow * LIMIT_PADDING_IN_PERCENT) / 100)\n      );\n\n      this.displayMinMax = this.domainObject.configuration.gaugeController.isDisplayMinMax;\n    },\n    updateValue(datum) {\n      this.datum = datum;\n\n      if (this.isRendering) {\n        return;\n      }\n\n      const { start, end } = this.openmct.time.getBounds();\n      const parsedValue = this.timeFormatter.parse(this.datum);\n\n      const beforeStartOfBounds = parsedValue < start;\n      const afterEndOfBounds = parsedValue > end;\n      if (afterEndOfBounds || beforeStartOfBounds) {\n        return;\n      }\n\n      this.isRendering = this.renderWhenVisible(() => {\n        this.isRendering = false;\n\n        this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);\n      });\n    },\n    valToPercent(vValue) {\n      // Used by dial\n      if (vValue >= this.rangeHigh && this.typeFilledDial) {\n        // For filled dial, clip values over the high range to prevent over-rotation\n        return 100;\n      }\n\n      return ((vValue - this.rangeLow) / (this.rangeHigh - this.rangeLow)) * 100;\n    },\n    valToPercentMeter(vValue) {\n      return this.round(((this.rangeHigh - vValue) / (this.rangeHigh - this.rangeLow)) * 100, 2);\n    },\n    async showToolTip() {\n      const { CENTER } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getTelemetryPathString(), CENTER, 'gauge');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/gauge/components/GaugeFormController.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <span class=\"form-control\">\n    <span class=\"field control\" :class=\"model.cssClass\">\n      <ToggleSwitch\n        :id=\"'gaugeToggle'\"\n        :checked=\"isUseTelemetryLimits\"\n        label=\"Use telemetry limits for minimum and maximum ranges\"\n        @change=\"toggleUseTelemetryLimits\"\n      />\n\n      <div v-if=\"!isUseTelemetryLimits\" class=\"c-form--sub-grid\">\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator req\"> </span>\n          <label>Minimum value</label>\n          <input\n            ref=\"min\"\n            v-model.number=\"min\"\n            data-field-name=\"min\"\n            type=\"number\"\n            @input=\"onChange\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\"> </span>\n          <label>Low limit</label>\n          <input\n            ref=\"limitLow\"\n            v-model.number=\"limitLow\"\n            data-field-name=\"limitLow\"\n            type=\"number\"\n            @input=\"onChange\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator\"> </span>\n          <label>High limit</label>\n          <input\n            ref=\"limitHigh\"\n            v-model.number=\"limitHigh\"\n            data-field-name=\"limitHigh\"\n            type=\"number\"\n            @input=\"onChange\"\n          />\n        </div>\n\n        <div class=\"c-form__row\">\n          <span class=\"req-indicator req\"> </span>\n          <label>Maximum value</label>\n          <input\n            ref=\"max\"\n            v-model.number=\"max\"\n            data-field-name=\"max\"\n            type=\"number\"\n            @input=\"onChange\"\n          />\n        </div>\n      </div>\n    </span>\n  </span>\n</template>\n\n<script>\nimport ToggleSwitch from '@/ui/components/ToggleSwitch.vue';\n\nexport default {\n  components: {\n    ToggleSwitch\n  },\n  inject: ['openmct'],\n  props: {\n    model: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['on-change'],\n  data() {\n    this.changes = {};\n\n    return {\n      isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,\n      limitHigh: this.model.value.limitHigh,\n      limitLow: this.model.value.limitLow,\n      max: this.model.value.max,\n      min: this.model.value.min\n    };\n  },\n  methods: {\n    onChange(event) {\n      let data = {\n        model: {}\n      };\n\n      if (event) {\n        const target = event.target;\n        const property = target.dataset.fieldName;\n        data.model.property = Array.from(this.model.property).concat([property]);\n        data.value = this[property];\n        const targetIndicator = target.parentElement.querySelector('.req-indicator');\n        if (targetIndicator.classList.contains('req')) {\n          targetIndicator.classList.add('visited');\n        }\n\n        this.model.validate(data, (valid) => {\n          Object.entries(valid).forEach(([key, isValid]) => {\n            const element = this.$refs[key];\n            const reqIndicatorElement = element.parentElement.querySelector('.req-indicator');\n            reqIndicatorElement.classList.toggle('invalid', !isValid);\n\n            if (\n              reqIndicatorElement.classList.contains('req') &&\n              (!isValid || reqIndicatorElement.classList.contains('visited'))\n            ) {\n              reqIndicatorElement.classList.toggle('valid', isValid);\n            }\n          });\n        });\n      }\n\n      this.$emit('on-change', data);\n    },\n    toggleUseTelemetryLimits() {\n      this.isUseTelemetryLimits = !this.isUseTelemetryLimits;\n      const data = {\n        model: {\n          property: Array.from(this.model.property).concat(['isUseTelemetryLimits'])\n        },\n        value: this.isUseTelemetryLimits\n      };\n      this.$emit('on-change', data);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/gauge/gauge-limit-util.js",
    "content": "const GAUGE_LIMITS = {\n  q1: 0,\n  q2: 90,\n  q3: 180,\n  q4: 270\n};\n\nexport const DIAL_VALUE_DEG_OFFSET = 45;\n\n// type: low, high\n// quadrant: low, mid, high, max\nexport function getLimitDegree(type, quadrant) {\n  if (quadrant === 'max') {\n    return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;\n  }\n\n  return type === 'low' ? getLowLimitDegree(quadrant) : getHighLimitDegree(quadrant);\n}\n\nfunction getLowLimitDegree(quadrant) {\n  return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;\n}\n\nfunction getHighLimitDegree(quadrant) {\n  if (quadrant === 'q1') {\n    return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;\n  }\n\n  if (quadrant === 'q2') {\n    return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;\n  }\n\n  if (quadrant === 'q3') {\n    return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;\n  }\n}\n"
  },
  {
    "path": "src/plugins/gauge/gauge.scss",
    "content": "$meterNeedlePerc: 1%;\n$meterNeedleMinPx: 4px;\n$meterNeedleMaxPx: 20px;\n$meterNeedleBorderRadius: 5px;\n\n.is-object-type-gauge {\n  overflow: hidden;\n}\n\n.req-indicator {\n  width: 20px;\n\n  &.invalid,\n  &.invalid.req {\n    @include validationState($glyph-icon-x, $colorFormInvalid);\n  }\n  &.valid,\n  &.valid.req {\n    @include validationState($glyph-icon-check, $colorFormValid);\n  }\n  &.req {\n    @include validationState($glyph-icon-asterisk, $colorFormRequired);\n  }\n}\n\n.c-gauge {\n  // Both dial and meter types\n  overflow: hidden;\n\n  &__range,\n  &__units,\n  &__units text {\n    $c: $colorGaugeRange;\n    color: $c;\n    fill: $c;\n  }\n\n  &__wrapper {\n    @include abs();\n    overflow: hidden;\n\n    &.is-stale {\n      @include isStaleHolder();\n\n      [class*='__current-value-text'] {\n        fill: $colorTelemStale;\n        font-style: italic;\n      }\n    }\n  }\n\n  &__current-value-text-wrapper {\n    // SVG\n    position: absolute;\n    height: 100%;\n    width: 100%;\n  }\n}\n\n.c-dial__value-oor-indicator,\n.c-meter__value-oor-indicator {\n  fill: $colorGaugeRange;\n  opacity: 0.5;\n}\n\n/********************************************** DIAL GAUGE */\n.c-dial {\n  max-height: 100%;\n  max-width: 100%;\n  display: block;\n  margin: auto; // Centers SVG in container while allowing scaling\n\n  &__bg {\n    fill: $colorGaugeBg;\n  }\n  &__limit-high rect {\n    fill: $colorGaugeLimitHigh;\n  }\n  &__limit-low rect {\n    fill: $colorGaugeLimitLow;\n  }\n  &__filled-value,\n  &__range-msg-text {\n    fill: $colorGaugeValue;\n  }\n  &__needle-value {\n    fill: $colorGaugeNeedle;\n\n  }\n  &__current-value-text {\n    fill: $colorGaugeTextValue;\n    font-family: $numericFont;\n  }\n\n  &__units-text,\n  &__range-text {\n    fill: $colorGaugeRange;\n  }\n\n  &__graphics g {\n    transform-origin: center;\n  }\n}\n\n/********************************************** METER GAUGE */\n.c-meter {\n  // Common styles for c-meter\n  $meterOutOfRangeIndicatorMaxSize: 50%;\n  @include abs();\n  display: flex;\n\n  &__range {\n    display: flex;\n    flex: 0 0 auto;\n    justify-content: space-between;\n  }\n\n  &__bg {\n    background: $colorGaugeBg;\n    border-radius: $basicCr;\n    flex: 1 1 auto;\n    overflow: hidden;\n  }\n\n  &__value {\n    // Filled area\n    position: absolute;\n    background: $colorGaugeValue;\n    box-shadow: $gaugeMeterValueShadow 0px 2px 10px 1px;\n    //z-index: 3;\n  }\n\n  &__value-needle {\n    background: none !important;\n    &:before {\n      @include abs();\n      content: '';\n      display: block;\n      background: $colorGaugeValue;\n\n    }\n  }\n\n  &__value-oor-indicator {\n    $mxPx: 50px;\n    $wh: 50%;\n    position: absolute;\n    height: $wh;\n    width: $wh;\n    max-height: $mxPx;\n    max-width: $mxPx;\n\n    svg {\n      position: absolute;\n      width: 100%;\n      height: 100%;\n      max-height: 100%;\n      max-width: 100%;\n    }\n  }\n\n  &__current-value-text {\n    fill: $colorGaugeTextValue;\n    font-family: $numericFont;\n  }\n\n  .c-gauge__curval {\n    fill: $colorGaugeMeterTextValue !important;\n  }\n\n  [class*='limit'] {\n    position: absolute;\n  }\n\n  &__limit-high {\n    background: $colorGaugeLimitHigh;\n  }\n\n  &__limit-low {\n    background: $colorGaugeLimitLow;\n  }\n}\n\n.c-meter {\n  .c-gauge--meter-vertical &,\n  .c-gauge--meter-vertical-inverted & {\n    &__range {\n      flex-direction: column;\n      min-width: min-content;\n      margin-right: $interiorMarginSm;\n      text-align: right;\n    }\n\n    &__value {\n      // Filled area\n      $lrM: $marginGaugeMeterValue;\n      left: $lrM;\n      right: $lrM;\n      top: 0;\n      bottom: 0;\n    }\n\n    &__value-needle {\n      right: 0;\n\n      &:before {\n        border-bottom-left-radius: $meterNeedleBorderRadius;\n        border-top-left-radius: $meterNeedleBorderRadius;\n        height: $meterNeedlePerc;\n        min-height: $meterNeedleMinPx;\n        max-height: $meterNeedleMaxPx;\n      }\n    }\n\n    [class*='limit'] {\n      left: 0;\n      right: 0;\n    }\n\n    .c-meter__value-oor-indicator {\n      bottom: 10%;\n      left: 50%;\n      transform: translateX(-50%);\n    }\n  }\n\n  .c-gauge--meter-vertical & {\n    &__limit-low {\n      bottom: 0;\n    }\n\n    &__limit-high {\n      top: 0;\n    }\n\n    &__value-needle {\n      &:before {\n        bottom: auto;\n        transform: translateY(-50%);\n      }\n    }\n  }\n\n  .c-gauge--meter-vertical-inverted & {\n    &__limit-low {\n      top: 0;\n    }\n\n    &__limit-high {\n      bottom: 0;\n    }\n\n    &__range__low {\n      order: 1;\n    }\n\n    &__range__high {\n      order: 2;\n    }\n\n    &__value-needle {\n      &:before {\n        top: auto;\n        transform: translateY(50%);\n      }\n    }\n  }\n\n  .c-gauge--meter-horizontal & {\n    flex-direction: column;\n\n    &__range {\n      flex-direction: row;\n      min-height: min-content;\n      margin-top: $interiorMarginSm;\n      order: 2;\n\n      &__high {\n        order: 2;\n      }\n\n      &__low {\n        order: 1;\n      }\n    }\n\n    &__bg {\n      order: 1;\n    }\n\n    &__value {\n      // Filled area\n      $m: $marginGaugeMeterValue;\n      top: $m;\n      bottom: $m;\n      left: 0;\n      right: 0;\n    }\n\n    &__value-needle {\n      top: 0;\n\n      &:before {\n        border-bottom-left-radius: $meterNeedleBorderRadius;\n        border-bottom-right-radius: $meterNeedleBorderRadius;\n        left: auto;\n        width: $meterNeedlePerc;\n        min-width: $meterNeedleMinPx;\n        max-width: $meterNeedleMaxPx;\n        transform: translateX(50%);\n      }\n    }\n\n    [class*='limit'] {\n      top: 0;\n      bottom: 0;\n    }\n\n    &__limit-low {\n      left: 0;\n    }\n\n    &__limit-high {\n      right: 0;\n    }\n\n    .c-meter__value-oor-indicator {\n      // Horizontal meter\n      left: 2%;\n      top: 50%;\n      transform: translateY(-50%);\n    }\n  }\n}\n.svg-viewbox-debug {\n  fill: rgba(deeppink, 0.5);\n  display: none;\n}\n"
  },
  {
    "path": "src/plugins/gauge/gaugeStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function gaugeStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return domainObject?.type === 'gauge';\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      if (!domainObject.configuration.objectStyles) {\n        domainObject.configuration.objectStyles = {};\n      }\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/goToOriginalAction/goToOriginalAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst GO_TO_ORIGINAL_ACTION_KEY = 'goToOriginal';\n\nclass GoToOriginalAction {\n  constructor(openmct) {\n    this.name = 'Go To Original';\n    this.key = GO_TO_ORIGINAL_ACTION_KEY;\n    this.description = 'Go to the original unlinked instance of this object';\n    this.group = 'action';\n    this.priority = 4;\n\n    this._openmct = openmct;\n  }\n  invoke(objectPath) {\n    this._openmct.objects.getOriginalPath(objectPath[0].identifier).then((originalPath) => {\n      let url =\n        '#/browse/' +\n        originalPath\n          .map(\n            function (o) {\n              return o && this._openmct.objects.makeKeyString(o.identifier);\n            }.bind(this)\n          )\n          .reverse()\n          .slice(1)\n          .join('/');\n\n      this._openmct.router.navigate(url);\n    });\n  }\n  appliesTo(objectPath) {\n    if (this._openmct.editor.isEditing()) {\n      return false;\n    }\n\n    let parentKeystring =\n      objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier);\n\n    if (!parentKeystring) {\n      return false;\n    }\n\n    return parentKeystring !== objectPath[0].location;\n  }\n}\n\nexport { GO_TO_ORIGINAL_ACTION_KEY };\n\nexport default GoToOriginalAction;\n"
  },
  {
    "path": "src/plugins/goToOriginalAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport GoToOriginalAction from './goToOriginalAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new GoToOriginalAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/goToOriginalAction/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('the goToOriginalAction plugin', () => {\n  let openmct;\n  let goToOriginalAction;\n  let mockRootFolder;\n  let mockSubFolder;\n  let mockSubSubFolder;\n  let mockObject;\n  let mockObjectPath;\n  let hash;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    goToOriginalAction = openmct.actions._allActions.goToOriginal;\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('installs the go to folder action', () => {\n    expect(goToOriginalAction).toBeDefined();\n  });\n\n  describe('when invoked', () => {\n    beforeEach(() => {\n      mockRootFolder = getMockObject('mock-root');\n      mockSubFolder = getMockObject('mock-sub');\n      mockSubSubFolder = getMockObject('mock-sub-sub');\n      mockObject = getMockObject('mock-table');\n\n      mockObjectPath = [mockObject, mockSubSubFolder, mockSubFolder, mockRootFolder];\n\n      spyOn(openmct.objects, 'get').and.callFake((identifier) => {\n        const mockedObject = getMockObject(identifier);\n\n        return Promise.resolve(mockedObject);\n      });\n\n      spyOn(openmct.router, 'navigate').and.callFake((navigateTo) => {\n        hash = navigateTo;\n      });\n\n      return goToOriginalAction.invoke(mockObjectPath);\n    });\n\n    it('goes to the original location', () => {\n      const originalLocationHash = '#/browse/mock-root/mock-table';\n\n      return waitForNavigation(() => {\n        return hash === originalLocationHash;\n      }).then(() => {\n        expect(hash).toEqual(originalLocationHash);\n      });\n    });\n  });\n\n  function waitForNavigation(navigated) {\n    return new Promise((resolve, reject) => {\n      const start = Date.now();\n\n      checkNavigated();\n\n      function checkNavigated() {\n        const elapsed = Date.now() - start;\n\n        if (navigated()) {\n          resolve();\n        } else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) {\n          reject(\"didn't navigate in time\");\n        } else {\n          setTimeout(checkNavigated);\n        }\n      }\n    });\n  }\n\n  function getMockObject(key) {\n    const id = typeof key === 'string' ? key : key.key;\n\n    const mockMCTObjects = {\n      ROOT: {\n        composition: [\n          {\n            namespace: '',\n            key: 'mock-root'\n          }\n        ],\n        identifier: {\n          namespace: '',\n          key: 'mock-root'\n        }\n      },\n      'mock-root': {\n        composition: [\n          {\n            namespace: '',\n            key: 'mock-sub'\n          },\n          {\n            namespace: '',\n            key: 'mock-table'\n          }\n        ],\n        name: 'root',\n        type: 'folder',\n        id: 'mock-root',\n        location: 'ROOT',\n        identifier: {\n          namespace: '',\n          key: 'mock-root'\n        }\n      },\n      'mock-sub': {\n        composition: [\n          {\n            namespace: '',\n            key: 'mock-sub-sub'\n          },\n          {\n            namespace: '',\n            key: 'mock-table'\n          }\n        ],\n        name: 'sub',\n        type: 'folder',\n        location: 'mock-root',\n        identifier: {\n          namespace: '',\n          key: 'mock-sub'\n        }\n      },\n      'mock-table': {\n        composition: [],\n        configuration: {\n          columnWidths: {},\n          hiddenColumns: {}\n        },\n        name: 'table',\n        type: 'table',\n        location: 'mock-root',\n        identifier: {\n          namespace: '',\n          key: 'mock-table'\n        }\n      },\n      'mock-sub-sub': {\n        composition: [\n          {\n            namespace: '',\n            key: 'mock-table'\n          }\n        ],\n        name: 'sub sub',\n        type: 'folder',\n        location: 'mock-sub',\n        identifier: {\n          namespace: '',\n          key: 'mock-sub-sub'\n        }\n      }\n    };\n\n    return mockMCTObjects[id];\n  }\n});\n"
  },
  {
    "path": "src/plugins/hyperlink/HyperlinkLayout.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <a\n    class=\"c-hyperlink\"\n    :class=\"{\n      'c-hyperlink--button': isButton\n    }\"\n    :target=\"domainObject.linkTarget\"\n    :href=\"url\"\n  >\n    <span class=\"c-hyperlink__label\">{{ domainObject.displayText }}</span>\n  </a>\n</template>\n\n<script>\nimport { sanitizeUrl } from '@braintree/sanitize-url';\n\nexport default {\n  inject: ['domainObject'],\n  computed: {\n    isButton() {\n      if (this.domainObject.displayFormat === 'link') {\n        return false;\n      }\n\n      return true;\n    },\n    url() {\n      return sanitizeUrl(this.domainObject.url);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/hyperlink/HyperlinkProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport HyperlinkLayout from './HyperlinkLayout.vue';\n\nexport default function HyperlinkProvider(openmct) {\n  return {\n    key: 'hyperlink.view',\n    name: 'Hyperlink',\n    cssClass: 'icon-chain-links',\n    canView(domainObject) {\n      return domainObject.type === 'hyperlink';\n    },\n\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                HyperlinkLayout\n              },\n              provide: {\n                domainObject\n              },\n              template: '<hyperlink-layout></hyperlink-layout>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/hyperlink/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport HyperlinkProvider from './HyperlinkProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.types.addType('hyperlink', {\n      name: 'Hyperlink',\n      key: 'hyperlink',\n      description: 'A text element or button that links to any URL including Open MCT views.',\n      creatable: true,\n      cssClass: 'icon-chain-links',\n      initialize: function (domainObject) {\n        domainObject.displayFormat = 'link';\n        domainObject.linkTarget = '_self';\n      },\n      form: [\n        {\n          key: 'url',\n          name: 'URL',\n          control: 'textfield',\n          required: true,\n          cssClass: 'l-input-lg'\n        },\n        {\n          key: 'displayText',\n          name: 'Text to Display',\n          control: 'textfield',\n          required: true,\n          cssClass: 'l-input-lg'\n        },\n        {\n          key: 'displayFormat',\n          name: 'Display Format',\n          control: 'select',\n          options: [\n            {\n              name: 'Link',\n              value: 'link'\n            },\n            {\n              name: 'Button',\n              value: 'button'\n            }\n          ],\n          cssClass: 'l-inline'\n        },\n        {\n          key: 'linkTarget',\n          name: 'Tab to Open Hyperlink',\n          control: 'select',\n          options: [\n            {\n              name: 'Open in this tab',\n              value: '_self'\n            },\n            {\n              name: 'Open in a new tab',\n              value: '_blank'\n            }\n          ],\n          cssClass: 'l-inline'\n        }\n      ]\n    });\n    openmct.objectViews.addProvider(new HyperlinkProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/hyperlink/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport HyperlinkPlugin from './plugin.js';\n\nfunction getView(openmct, domainObj, objectPath) {\n  const applicableViews = openmct.objectViews.get(domainObj, objectPath);\n  const hyperLinkView = applicableViews.find(\n    (viewProvider) => viewProvider.key === 'hyperlink.view'\n  );\n\n  return hyperLinkView.view(domainObj, [domainObj]);\n}\n\nfunction destroyView(view) {\n  return view.destroy();\n}\n\ndescribe('The controller for hyperlinks', function () {\n  let mockDomainObject;\n  let mockObjectPath;\n  let openmct;\n  let element;\n  let child;\n  let view;\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock hyperlink',\n        type: 'hyperlink',\n        identifier: {\n          key: 'mock-hyperlink',\n          namespace: ''\n        }\n      }\n    ];\n\n    mockDomainObject = {\n      displayFormat: '',\n      linkTarget: '',\n      name: 'Unnamed HyperLink',\n      type: 'hyperlink',\n      location: 'f69c21ac-24ef-450c-8e2f-3d527087d285',\n      modified: 1627483839783,\n      url: '123',\n      displayText: '123',\n      persisted: 1627483839783,\n      id: '3d9c243d-dffb-446b-8474-d9931a99d679',\n      identifier: {\n        namespace: '',\n        key: '3d9c243d-dffb-446b-8474-d9931a99d679'\n      }\n    };\n\n    openmct = createOpenMct();\n    openmct.install(new HyperlinkPlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    destroyView(view);\n\n    return resetApplicationState(openmct);\n  });\n  it('knows when it should open a new tab', () => {\n    mockDomainObject.displayFormat = 'link';\n    mockDomainObject.linkTarget = '_blank';\n\n    view = getView(openmct, mockDomainObject, mockObjectPath);\n    view.show(child, true);\n\n    expect(element.querySelector('.c-hyperlink').target).toBe('_blank');\n  });\n  it('knows when it should open in the same tab', function () {\n    mockDomainObject.displayFormat = 'button';\n    mockDomainObject.linkTarget = '_self';\n\n    view = getView(openmct, mockDomainObject, mockObjectPath);\n    view.show(child, true);\n\n    expect(element.querySelector('.c-hyperlink').target).toBe('_self');\n  });\n\n  it('knows when it is a button', function () {\n    mockDomainObject.displayFormat = 'button';\n\n    view = getView(openmct, mockDomainObject, mockObjectPath);\n    view.show(child, true);\n\n    expect(element.querySelector('.c-hyperlink--button')).toBeDefined();\n  });\n  it('knows when it is a link', function () {\n    mockDomainObject.displayFormat = 'link';\n\n    view = getView(openmct, mockDomainObject, mockObjectPath);\n    view.show(child, true);\n\n    expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button');\n  });\n});\n"
  },
  {
    "path": "src/plugins/imagery/ImageryTimestripViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport ImageryTimeView from './components/ImageryTimeView.vue';\n\nexport default function ImageryTimestripViewProvider(openmct) {\n  const type = 'example.imagery.time-strip.view';\n\n  function hasImageTelemetry(domainObject) {\n    const metadata = openmct.telemetry.getMetadata(domainObject);\n    if (!metadata) {\n      return false;\n    }\n\n    return metadata.valuesForHints(['image']).length > 0;\n  }\n\n  return {\n    key: type,\n    name: 'Imagery Timestrip View',\n    cssClass: 'icon-image',\n    canView: function (domainObject, objectPath) {\n      let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n      return (\n        hasImageTelemetry(domainObject) &&\n        isChildOfTimeStrip &&\n        !openmct.router.isNavigatedObject(objectPath)\n      );\n    },\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show: function (element) {\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                ImageryTimeView\n              },\n              provide: {\n                openmct: openmct,\n                domainObject: domainObject,\n                objectPath: objectPath\n              },\n              template: '<imagery-time-view ref=\"root\"></imagery-time-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        },\n\n        getComponent() {\n          return component;\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/imagery/ImageryView.js",
    "content": "import mount from 'utils/mount';\n\nimport ImageryViewComponent from './components/ImageryView.vue';\n\nconst DEFAULT_IMAGE_FRESHNESS_OPTIONS = {\n  fadeOutDelayTime: '0s',\n  fadeOutDurationTime: '30s'\n};\nexport default class ImageryView {\n  constructor(openmct, domainObject, objectPath, options) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this.options = options;\n    this.component = null;\n    this._destroy = null;\n  }\n\n  show(element, isEditing, viewOptions) {\n    let alternateObjectPath;\n    let focusedImageTimestamp;\n    if (viewOptions) {\n      focusedImageTimestamp = viewOptions.timestamp;\n      alternateObjectPath = viewOptions.objectPath;\n    }\n\n    const { vNode, destroy } = mount(\n      {\n        el: element,\n        components: {\n          'imagery-view': ImageryViewComponent\n        },\n        provide: {\n          openmct: this.openmct,\n          domainObject: this.domainObject,\n          objectPath: alternateObjectPath || this.objectPath,\n          imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS,\n          showCompassHUD: this.options?.showCompassHUD,\n          currentView: this\n        },\n        data() {\n          return {\n            focusedImageTimestamp\n          };\n        },\n        template:\n          '<imagery-view :focused-image-timestamp=\"focusedImageTimestamp\" @update:focusedImageTimestamp=\"value => focusedImageTimestamp = value\" ref=\"ImageryContainer\"></imagery-view>'\n      },\n      {\n        app: this.openmct.app,\n        element\n      }\n    );\n    this.component = vNode.componentInstance;\n    this._destroy = destroy;\n  }\n\n  getViewContext() {\n    if (!this.component) {\n      return {};\n    }\n\n    return this.component.$refs.ImageryContainer;\n  }\n\n  pause() {\n    const imageContext = this.getViewContext();\n    // persist previous pause value to return to after unpausing\n    this.previouslyPaused = imageContext.isPaused;\n    imageContext.thumbnailClicked(imageContext.focusedImageIndex);\n  }\n  unpause() {\n    const pausedStateBefore = this.previouslyPaused;\n    this.previouslyPaused = undefined; // clear value\n    const imageContext = this.getViewContext();\n    imageContext.paused(pausedStateBefore);\n  }\n\n  onPreviewModeChange({ isPreviewing } = {}) {\n    if (isPreviewing) {\n      this.pause();\n    } else {\n      this.unpause();\n    }\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n    }\n  }\n\n  _getInstance() {\n    return this.component;\n  }\n}\n"
  },
  {
    "path": "src/plugins/imagery/ImageryViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ImageryView from './ImageryView.js';\n\nexport default function ImageryViewProvider(openmct, options) {\n  const type = 'example.imagery';\n\n  function hasImageTelemetry(domainObject) {\n    const metadata = openmct.telemetry.getMetadata(domainObject);\n    if (!metadata) {\n      return false;\n    }\n\n    return metadata.valuesForHints(['image']).length > 0;\n  }\n\n  return {\n    key: type,\n    name: 'Imagery Layout',\n    cssClass: 'icon-image',\n    canView: function (domainObject, objectPath) {\n      let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n      return (\n        hasImageTelemetry(domainObject) &&\n        (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath))\n      );\n    },\n    view: function (domainObject, objectPath) {\n      return new ImageryView(openmct, domainObject, objectPath, options);\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/imagery/actions/OpenImageInNewTabAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY = 'openImageInNewTab';\nclass OpenImageInNewTabAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-new-window';\n    this.description = 'Open the image in a new tab';\n    this.group = 'action';\n    this.key = OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY;\n    this.name = 'Open Image in New Tab';\n    this.priority = 1;\n  }\n\n  invoke(objectPath, view) {\n    const viewContext = (view.getViewContext && view.getViewContext()) || {};\n    window.open(viewContext.imageUrl, '_blank').focus();\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const viewContext = (view.getViewContext && view.getViewContext()) || {};\n    if (!viewContext.imageUrl) {\n      return false;\n    }\n  }\n}\n\nexport { OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY };\n\nexport default OpenImageInNewTabAction;\n"
  },
  {
    "path": "src/plugins/imagery/actions/SaveImageAsAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst SAVE_IMAGE_ACTION_KEY = 'saveImageAs';\nclass SaveImageAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-save-as';\n    this.description = 'Save image to file';\n    this.group = 'action';\n    this.key = SAVE_IMAGE_ACTION_KEY;\n    this.name = 'Save Image As';\n    this.priority = 1;\n  }\n\n  async invoke(objectPath, view) {\n    const viewContext = (view.getViewContext && view.getViewContext()) || {};\n    try {\n      const filename =\n        viewContext.imageUrl.split('/').pop().split('#')[0].split('?')[0] || 'downloaded-image.png';\n      const response = await fetch(viewContext.imageUrl);\n      const blob = await response.blob();\n      const blobUrl = URL.createObjectURL(blob);\n\n      // Create a temporary anchor element and trigger the download\n      const a = document.createElement('a');\n      a.href = blobUrl;\n      a.download = filename; // Set the filename for the download\n\n      // Append anchor to body, trigger click, then remove it from the DOM\n      document.body.appendChild(a);\n      a.click();\n      document.body.removeChild(a);\n\n      // Revoke the blob URL after the download\n      URL.revokeObjectURL(blobUrl);\n    } catch (error) {\n      console.error('Could not download the image.', error);\n    }\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const viewContext = (view.getViewContext && view.getViewContext()) || {};\n    if (!viewContext.imageUrl) {\n      return false;\n    }\n  }\n}\n\nexport { SAVE_IMAGE_ACTION_KEY };\n\nexport default SaveImageAction;\n"
  },
  {
    "path": "src/plugins/imagery/components/AnnotationsCanvas.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <canvas\n    ref=\"canvas\"\n    class=\"c-image-canvas\"\n    style=\"width: 100%; height: 100%\"\n    @mousedown=\"clearSelectedAnnotations\"\n    @mousemove=\"trackAnnotationDrag\"\n    @click=\"selectOrCreateAnnotation\"\n    @contextmenu=\"showContextMenu\"\n  ></canvas>\n</template>\n\n<script>\nimport Flatbush from 'flatbush';\nimport isEqual from 'lodash/isEqual';\nimport { toRaw } from 'vue';\n\nimport TagEditorClassNames from '../../inspectorViews/annotations/tags/TagEditorClassNames.js';\nimport { OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY } from '../actions/OpenImageInNewTabAction.js';\nimport { SAVE_IMAGE_ACTION_KEY } from '../actions/SaveImageAsAction.js';\n\nconst EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';\nconst EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';\nconst SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';\nconst SELECTED_ANNOTATION_FILL_STYLE = 'rgba(199, 87, 231, 0.2)';\n\nconst CONTEXT_MENU_ACTIONS = [OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY, SAVE_IMAGE_ACTION_KEY];\n\nexport default {\n  inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],\n  props: {\n    image: {\n      type: Object,\n      required: true\n    },\n    imageryAnnotations: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['annotation-marquee-started', 'annotations-changed', 'annotation-marquee-finished'],\n  data() {\n    return {\n      dragging: false,\n      mouseDown: false,\n      newAnnotationRectangle: {},\n      keyString: null,\n      context: null,\n      canvas: null,\n      selectedAnnotations: [],\n      indexToAnnotationMap: {}\n    };\n  },\n  computed: {\n    annotationsIndex() {\n      if (this.imageryAnnotations.length) {\n        // create a flatbush index for the annotations\n        const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length);\n        this.imageryAnnotations.forEach((annotation) => {\n          const annotationRectangle = annotation.targets.find(\n            (target) => target.keyString === this.keyString\n          )?.rectangle;\n          const annotationRectangleForPixelDepth =\n            this.transformRectangleToPixelDense(annotationRectangle);\n          const { x, y, x2, y2 } = this.transformAnnotationRectangleToFlatbushRectangle(\n            annotationRectangleForPixelDepth\n          );\n          const indexNumber = builtAnnotationsIndex.add(x, y, x2, y2);\n          this.indexToAnnotationMap[indexNumber] = annotation;\n        });\n        builtAnnotationsIndex.finish();\n\n        return builtAnnotationsIndex;\n      } else {\n        return null;\n      }\n    }\n  },\n  watch: {\n    imageryAnnotations: {\n      handler() {\n        this.drawAnnotations();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.canvas = this.$refs.canvas;\n    this.context = this.canvas.getContext('2d');\n\n    // adjust canvas size for retina displays\n    const pixelScale = window.devicePixelRatio;\n    this.canvas.width = Math.floor(this.canvas.width * pixelScale);\n    this.canvas.height = Math.floor(this.canvas.height * pixelScale);\n\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.openmct.selection.on('change', this.updateSelection);\n    this.drawAnnotations();\n  },\n  beforeUnmount() {\n    this.openmct.selection.off('change', this.updateSelection);\n    document.body.removeEventListener('click', this.cancelSelection);\n  },\n  methods: {\n    onAnnotationChange(updatedAnnotations) {\n      updatedAnnotations.forEach((updatedAnnotation) => {\n        // Try to find the annotation in the existing selected annotations\n        const existingIndex = this.selectedAnnotations.findIndex((annotation) =>\n          this.openmct.objects.areIdsEqual(annotation.identifier, updatedAnnotation.identifier)\n        );\n\n        // If found, update it\n        if (existingIndex > -1) {\n          this.selectedAnnotations[existingIndex] = updatedAnnotation;\n        } else {\n          // If not found, add it\n          this.selectedAnnotations.push(updatedAnnotation);\n        }\n      });\n      this.$emit('annotations-changed', this.selectedAnnotations);\n    },\n    transformAnnotationRectangleToFlatbushRectangle(annotationRectangle) {\n      let { x, y, width, height } = annotationRectangle;\n      let x2 = x + width;\n      let y2 = y + height;\n\n      // if height or width are negative, we need to adjust the x and y\n      if (width < 0) {\n        x2 = x;\n        x = x + width;\n      }\n      if (height < 0) {\n        y2 = y;\n        y = y + height;\n      }\n\n      return { x, y, x2, y2 };\n    },\n    updateSelection(selection) {\n      const selectionContext = selection?.[0]?.[0]?.context?.item;\n      const selectionType = selection?.[0]?.[0]?.context?.type;\n      const validSelectionTypes = ['clicked-on-image-selection'];\n\n      if (!validSelectionTypes.includes(selectionType)) {\n        // wrong type of selection\n        return;\n      }\n\n      if (\n        selectionContext &&\n        this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)\n      ) {\n        return;\n      }\n\n      const incomingSelectedAnnotations = selection?.[0]?.[0]?.context?.annotations;\n\n      this.prepareExistingAnnotationSelection(incomingSelectedAnnotations);\n    },\n    prepareExistingAnnotationSelection(annotations) {\n      const targetDetails = [];\n      annotations.forEach((annotation) => {\n        annotation.targets.forEach((target) => {\n          // only add targetDetails if we haven't added it before\n          const targetAlreadyAdded = targetDetails.some((targetDetail) => {\n            return isEqual(targetDetail, toRaw(target));\n          });\n          if (!targetAlreadyAdded) {\n            targetDetails.push(toRaw(target));\n          }\n        });\n      });\n      this.selectedAnnotations = annotations;\n      this.drawAnnotations();\n\n      return {\n        targetDomainObjects: [this.domainObject],\n        targetDetails\n      };\n    },\n    clearSelectedAnnotations() {\n      if (!this.openmct.annotation.getAvailableTags().length) {\n        // don't bother with new annotations if there are no tags\n        return;\n      }\n\n      this.mouseDown = true;\n      this.selectedAnnotations = [];\n    },\n    /**\n     * Given a rectangle, returns a rectangle that conforms to the pixel density of the device\n     * @param {Object} rectangle without pixel density applied\n     * @returns {Object} transformed rectangle with pixel density applied\n     */\n    transformRectangleToPixelDense(rectangle) {\n      const pixelScale = window.devicePixelRatio;\n      const transformedRectangle = {\n        x: rectangle.x * pixelScale,\n        y: rectangle.y * pixelScale,\n        width: rectangle.width * pixelScale,\n        height: rectangle.height * pixelScale\n      };\n      return transformedRectangle;\n    },\n    /**\n     * Given a rectangle, returns a rectangle that is independent of the pixel density of the device\n     * @param {Object} rectangle with pixel density applied\n     * @returns {Object} transformed rectangle without pixel density applied\n     */\n    transformRectangleFromPixelDense(rectangle) {\n      const pixelScale = window.devicePixelRatio;\n      const transformedRectangle = {\n        x: rectangle.x / pixelScale,\n        y: rectangle.y / pixelScale,\n        width: rectangle.width / pixelScale,\n        height: rectangle.height / pixelScale\n      };\n      return transformedRectangle;\n    },\n    drawRectInCanvas(rectangle, fillStyle, strokeStyle) {\n      this.context.beginPath();\n      this.context.lineWidth = 1;\n      this.context.fillStyle = fillStyle;\n      this.context.strokeStyle = strokeStyle;\n      this.context.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);\n      this.context.fill();\n      this.context.stroke();\n    },\n    trackAnnotationDrag(event) {\n      if (this.mouseDown && !this.dragging && event.shiftKey && event.altKey) {\n        this.startAnnotationDrag(event);\n      } else if (this.dragging) {\n        const boundingRect = this.canvas.getBoundingClientRect();\n        const scaleX = this.canvas.width / boundingRect.width;\n        const scaleY = this.canvas.height / boundingRect.height;\n        this.newAnnotationRectangle = {\n          x: this.newAnnotationRectangle.x,\n          y: this.newAnnotationRectangle.y,\n          width: (event.clientX - boundingRect.left) * scaleX - this.newAnnotationRectangle.x,\n          height: (event.clientY - boundingRect.top) * scaleY - this.newAnnotationRectangle.y\n        };\n        this.drawAnnotations();\n        this.drawRectInCanvas(\n          this.newAnnotationRectangle,\n          SELECTED_ANNOTATION_FILL_STYLE,\n          SELECTED_ANNOTATION_STROKE_COLOR\n        );\n      }\n    },\n    clearCanvas() {\n      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n    },\n    selectImageView() {\n      // should show ImageView itself if we have no annotations to display\n      const selection = this.createPathSelection();\n      this.openmct.selection.select(selection, true);\n    },\n    createSelection(annotation) {\n      const selection = this.createPathSelection();\n      selection[0].context = annotation;\n\n      return selection;\n    },\n    selectImageAnnotations({ targetDetails, targetDomainObjects, annotations }) {\n      const annotationContext = {\n        type: 'clicked-on-image-selection',\n        targetDetails,\n        targetDomainObjects,\n        annotations,\n        annotationType: this.openmct.annotation.ANNOTATION_TYPES.PIXEL_SPATIAL,\n        onAnnotationChange: this.onAnnotationChange\n      };\n      const selection = this.createPathSelection();\n      if (\n        selection.length &&\n        this.openmct.objects.areIdsEqual(\n          selection[0].context.item.identifier,\n          this.domainObject.identifier\n        )\n      ) {\n        selection[0].context = {\n          ...selection[0].context,\n          ...annotationContext\n        };\n      } else {\n        selection.unshift({\n          element: this.$el,\n          context: {\n            item: this.domainObject,\n            ...annotationContext\n          }\n        });\n      }\n\n      this.openmct.selection.select(selection, true);\n\n      document.body.addEventListener('click', this.cancelSelection);\n    },\n    cancelSelection(event) {\n      if (this.$refs.canvas) {\n        const clickedInsideCanvas = this.$refs.canvas.contains(event.target);\n        // unfortunate side effect from possibly being detached from the DOM when\n        // adding/deleting tags, so closest() won't work\n        const clickedTagEditor = Object.values(TagEditorClassNames).some((className) => {\n          return event.target.classList.contains(className);\n        });\n        const clickedInsideInspector = event.target.closest('.js-inspector') !== null;\n        if (!clickedInsideCanvas && !clickedTagEditor && !clickedInsideInspector) {\n          this.newAnnotationRectangle = {};\n          this.selectedAnnotations = [];\n          this.drawAnnotations();\n        }\n      }\n    },\n    createNewAnnotation() {\n      this.dragging = false;\n      this.selectedAnnotations = [];\n      this.selectedAnnotations = [];\n      this.$emit('annotation-marquee-finished');\n\n      const rectangleFromCanvas = {\n        x: this.newAnnotationRectangle.x,\n        y: this.newAnnotationRectangle.y,\n        width: this.newAnnotationRectangle.width,\n        height: this.newAnnotationRectangle.height\n      };\n      const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas);\n      const targetDetails = [\n        {\n          rectangle: rectangleWithoutPixelScale,\n          time: this.image.time,\n          keyString: this.keyString\n        }\n      ];\n      this.selectImageAnnotations({\n        targetDetails,\n        targetDomainObjects: [this.domainObject],\n        annotations: []\n      });\n    },\n    attemptToSelectExistingAnnotation(event) {\n      this.dragging = false;\n      this.$emit('annotation-marquee-finished');\n      // use flatbush to find annotations that are close to the click\n      const boundingRect = this.canvas.getBoundingClientRect();\n      const scaleX = this.canvas.width / boundingRect.width;\n      const scaleY = this.canvas.height / boundingRect.height;\n      const x = (event.clientX - boundingRect.left) * scaleX;\n      const y = (event.clientY - boundingRect.top) * scaleY;\n      if (this.annotationsIndex) {\n        let nearbyAnnotations = [];\n        const resultIndicies = this.annotationsIndex.search(x, y, x, y);\n        resultIndicies.forEach((resultIndex) => {\n          const foundAnnotation = this.indexToAnnotationMap[resultIndex];\n          nearbyAnnotations.push(foundAnnotation);\n        });\n        //if everything has been deleted, don't bother with the selection\n        const allAnnotationsDeleted = nearbyAnnotations.every((annotation) => annotation._deleted);\n        if (allAnnotationsDeleted) {\n          nearbyAnnotations = [];\n        }\n        const { targetDomainObjects, targetDetails } =\n          this.prepareExistingAnnotationSelection(nearbyAnnotations);\n        this.selectImageAnnotations({\n          targetDetails,\n          targetDomainObjects,\n          annotations: nearbyAnnotations\n        });\n      } else {\n        // nothing selected\n        this.drawAnnotations();\n      }\n    },\n    selectOrCreateAnnotation(event) {\n      event.stopPropagation();\n      this.mouseDown = false;\n      if (\n        !this.dragging ||\n        (!this.newAnnotationRectangle.width && !this.newAnnotationRectangle.height)\n      ) {\n        this.newAnnotationRectangle = {};\n        this.attemptToSelectExistingAnnotation(event);\n      } else {\n        this.createNewAnnotation();\n      }\n    },\n    createPathSelection() {\n      let selection = [];\n      selection.unshift({\n        element: this.$el,\n        context: {\n          item: this.domainObject\n        }\n      });\n      this.objectPath.forEach((pathObject, index) => {\n        selection.push({\n          element: this.openmct.layout.$refs.browseObject.$el,\n          context: {\n            item: pathObject\n          }\n        });\n      });\n\n      return selection;\n    },\n    startAnnotationDrag(event) {\n      this.$emit('annotation-marquee-started');\n      this.newAnnotationRectangle = {};\n      const boundingRect = this.canvas.getBoundingClientRect();\n      const scaleX = this.canvas.width / boundingRect.width;\n      const scaleY = this.canvas.height / boundingRect.height;\n      this.newAnnotationRectangle = {\n        x: (event.clientX - boundingRect.left) * scaleX,\n        y: (event.clientY - boundingRect.top) * scaleY\n      };\n      this.dragging = true;\n    },\n    isSelectedAnnotation(annotation) {\n      const someSelectedAnnotationExists = this.selectedAnnotations.some((selectedAnnotation) => {\n        return this.openmct.objects.areIdsEqual(\n          selectedAnnotation.identifier,\n          annotation.identifier\n        );\n      });\n\n      return someSelectedAnnotationExists;\n    },\n    drawAnnotations() {\n      this.clearCanvas();\n      let drawnRectangles = [];\n      this.imageryAnnotations.forEach((annotation) => {\n        if (annotation._deleted) {\n          return;\n        }\n        const annotationRectangle = annotation.targets.find(\n          (target) => target.keyString === this.keyString\n        )?.rectangle;\n\n        // Check if the rectangle has already been drawn\n        const hasBeenDrawn = drawnRectangles.some(\n          (drawnRect) =>\n            drawnRect.x === annotationRectangle.x &&\n            drawnRect.y === annotationRectangle.y &&\n            drawnRect.width === annotationRectangle.width &&\n            drawnRect.height === annotationRectangle.height\n        );\n        if (!hasBeenDrawn) {\n          const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);\n          if (this.isSelectedAnnotation(annotation)) {\n            this.drawRectInCanvas(\n              rectangleForPixelDensity,\n              SELECTED_ANNOTATION_FILL_STYLE,\n              SELECTED_ANNOTATION_STROKE_COLOR\n            );\n          } else {\n            this.drawRectInCanvas(\n              rectangleForPixelDensity,\n              EXISTING_ANNOTATION_FILL_STYLE,\n              EXISTING_ANNOTATION_STROKE_STYLE\n            );\n          }\n          drawnRectangles.push(annotationRectangle);\n        }\n      });\n    },\n    showContextMenu: function (event) {\n      event.preventDefault();\n\n      let objectPath = this.objectPath;\n\n      const actions = CONTEXT_MENU_ACTIONS.map((key) => this.openmct.actions.getAction(key));\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        actions,\n        objectPath,\n        this.currentView\n      );\n      if (menuItems.length) {\n        this.openmct.menus.showMenu(event.x, event.y, menuItems);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/CompassComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-compass\" :style=\"`width: 100%; height: 100%`\">\n    <CompassHud\n      v-if=\"showCompassHUD\"\n      :camera-angle-of-view=\"cameraAngleOfView\"\n      :heading=\"heading\"\n      :camera-azimuth=\"cameraAzimuth\"\n      :transformations=\"transformations\"\n      :has-gimble=\"hasGimble\"\n      :normalized-camera-azimuth=\"normalizedCameraAzimuth\"\n      :sun-heading=\"sunHeading\"\n    />\n    <CompassRose\n      :camera-angle-of-view=\"cameraAngleOfView\"\n      :heading=\"heading\"\n      :camera-azimuth=\"cameraAzimuth\"\n      :transformations=\"transformations\"\n      :has-gimble=\"hasGimble\"\n      :normalized-camera-azimuth=\"normalizedCameraAzimuth\"\n      :sun-heading=\"sunHeading\"\n      :sized-image-dimensions=\"sizedImageDimensions\"\n    />\n  </div>\n</template>\n\n<script>\nimport CompassHud from './CompassHud.vue';\nimport CompassRose from './CompassRose.vue';\nimport { rotate } from './utils.js';\n\nexport default {\n  components: {\n    CompassHud,\n    CompassRose\n  },\n  inject: ['showCompassHUD'],\n  props: {\n    image: {\n      type: Object,\n      required: true\n    },\n    sizedImageDimensions: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['toggle-lock-compass'],\n  computed: {\n    hasGimble() {\n      return this.cameraAzimuth !== undefined;\n    },\n    // compass ordinal orientation of camera\n    normalizedCameraAzimuth() {\n      return this.hasGimble\n        ? rotate(this.cameraAzimuth)\n        : rotate(this.heading, -this.transformations.rotation || 0);\n    },\n    // horizontal rotation from north in degrees\n    heading() {\n      return this.image.heading;\n    },\n    hasHeading() {\n      return this.heading !== undefined;\n    },\n    // horizontal rotation from north in degrees\n    sunHeading() {\n      return this.image.sunOrientation;\n    },\n    // horizontal rotation from north in degrees\n    cameraAzimuth() {\n      return this.image.cameraPan;\n    },\n    cameraAngleOfView() {\n      return this.transformations.cameraAngleOfView;\n    },\n    transformations() {\n      return this.image.transformations;\n    }\n  },\n  methods: {\n    toggleLockCompass() {\n      this.$emit('toggle-lock-compass');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/CompassHud.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-compass__hud c-hud\">\n    <div\n      v-for=\"point in visibleCompassPoints\"\n      :key=\"point.direction\"\n      :class=\"point.class\"\n      :style=\"point.style\"\n    >\n      {{ point.direction }}\n    </div>\n    <div v-if=\"isSunInRange\" ref=\"sun\" class=\"c-hud__sun\" :style=\"sunPositionStyle\"></div>\n    <div class=\"c-hud__range\"></div>\n  </div>\n</template>\n\n<script>\nimport { inRange, percentOfRange, rotate } from './utils.js';\n\nconst COMPASS_POINTS = [\n  {\n    direction: 'N',\n    class: 'c-hud__dir',\n    degrees: 0\n  },\n  {\n    direction: 'NE',\n    class: 'c-hud__dir--sub',\n    degrees: 45\n  },\n  {\n    direction: 'E',\n    class: 'c-hud__dir',\n    degrees: 90\n  },\n  {\n    direction: 'SE',\n    class: 'c-hud__dir--sub',\n    degrees: 135\n  },\n  {\n    direction: 'S',\n    class: 'c-hud__dir',\n    degrees: 180\n  },\n  {\n    direction: 'SW',\n    class: 'c-hud__dir--sub',\n    degrees: 225\n  },\n  {\n    direction: 'W',\n    class: 'c-hud__dir',\n    degrees: 270\n  },\n  {\n    direction: 'NW',\n    class: 'c-hud__dir--sub',\n    degrees: 315\n  }\n];\n\nexport default {\n  props: {\n    cameraAngleOfView: {\n      type: Number,\n      required: true\n    },\n    heading: {\n      type: Number,\n      required: true\n    },\n    cameraAzimuth: {\n      type: Number,\n      default: undefined\n    },\n    transformations: {\n      type: Object,\n      required: true\n    },\n    hasGimble: {\n      type: Boolean,\n      required: true\n    },\n    normalizedCameraAzimuth: {\n      type: Number,\n      required: true\n    },\n    sunHeading: {\n      type: Number,\n      default: undefined\n    }\n  },\n  computed: {\n    visibleCompassPoints() {\n      return COMPASS_POINTS.filter((point) => inRange(point.degrees, this.visibleRange)).map(\n        (point) => {\n          const percentage = percentOfRange(point.degrees, this.visibleRange);\n          point.style = Object.assign({ left: `${percentage * 100}%` });\n\n          return point;\n        }\n      );\n    },\n    isSunInRange() {\n      return inRange(this.sunHeading, this.visibleRange);\n    },\n    sunPositionStyle() {\n      const percentage = percentOfRange(this.sunHeading, this.visibleRange);\n\n      return {\n        left: `${percentage * 100}%`\n      };\n    },\n    cameraRotation() {\n      return this.transformations?.rotation;\n    },\n    visibleRange() {\n      return [\n        rotate(this.normalizedCameraAzimuth, -this.cameraAngleOfView / 2),\n        rotate(this.normalizedCameraAzimuth, this.cameraAngleOfView / 2)\n      ];\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/CompassRose.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    ref=\"compassRoseWrapper\"\n    class=\"w-direction-rose\"\n    :class=\"compassRoseSizingClasses\"\n    @click=\"toggleLockCompass\"\n  >\n    <svg ref=\"compassRoseSvg\" class=\"c-compass-rose-svg\" viewBox=\"0 0 100 100\">\n      <mask\n        id=\"mask0\"\n        mask-type=\"alpha\"\n        maskUnits=\"userSpaceOnUse\"\n        x=\"0\"\n        y=\"0\"\n        width=\"100\"\n        height=\"100\"\n      >\n        <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"black\" />\n      </mask>\n      <g class=\"c-cr__compass-wrapper\">\n        <g class=\"c-cr__compass-main\" mask=\"url(#mask0)\">\n          <!-- Background and clipped elements -->\n          <rect class=\"c-cr__bg\" width=\"100\" height=\"100\" fill=\"black\" />\n          <rect class=\"c-cr__edge\" width=\"100\" height=\"100\" fill=\"url(#gradient_edge)\" />\n          <rect\n            v-if=\"hasSunHeading\"\n            class=\"c-cr__sun\"\n            width=\"100\"\n            height=\"100\"\n            fill=\"url(#gradient_sun)\"\n            :style=\"sunHeadingStyle\"\n          />\n\n          <mask\n            id=\"mask2\"\n            class=\"c-cr__cam-fov-l-mask\"\n            mask-type=\"alpha\"\n            maskUnits=\"userSpaceOnUse\"\n            x=\"0\"\n            y=\"0\"\n            width=\"50\"\n            height=\"100\"\n          >\n            <rect width=\"51\" height=\"100\" />\n          </mask>\n          <mask\n            id=\"mask1\"\n            class=\"c-cr__cam-fov-r-mask\"\n            mask-type=\"alpha\"\n            maskUnits=\"userSpaceOnUse\"\n            x=\"50\"\n            y=\"0\"\n            width=\"50\"\n            height=\"100\"\n          >\n            <rect x=\"49\" width=\"51\" height=\"100\" />\n          </mask>\n          <g class=\"c-cr-cam-and-body\" :style=\"cameraHeadingStyle\">\n            <!-- Equipment (spacecraft) body holder. Transforms relative to the camera position. -->\n            <g v-if=\"hasHeading\" class=\"cr-vrover\" :style=\"camAngleAndPositionStyle\">\n              <!-- Equipment body. Rotates relative to the camera pan value for cameras that gimble. -->\n              <path\n                class=\"cr-vrover__body\"\n                :style=\"gimbledCameraPanStyle\"\n                x\n                fill-rule=\"evenodd\"\n                clip-rule=\"evenodd\"\n                d=\"M5 0C2.23858 0 0 2.23858 0 5V95C0 97.7614 2.23858 100 5 100H95C97.7614 100 100 97.7614 100 95V5C100 2.23858 97.7614 0 95 0H5ZM85 59L50 24L15 59H33V75H67.0455V59H85Z\"\n              />\n            </g>\n\n            <!-- Camera FOV -->\n            <g class=\"c-cr__cam-fov\">\n              <g mask=\"url(#mask2)\">\n                <rect\n                  class=\"c-cr__cam-fov-r\"\n                  x=\"49\"\n                  width=\"51\"\n                  height=\"100\"\n                  :style=\"cameraFOVStyleRightHalf\"\n                />\n              </g>\n              <g mask=\"url(#mask1)\">\n                <rect\n                  class=\"c-cr__cam-fov-l\"\n                  width=\"51\"\n                  height=\"100\"\n                  :style=\"cameraFOVStyleLeftHalf\"\n                />\n              </g>\n              <polygon class=\"c-cr__cam\" points=\"0,0 100,0 70,40 70,100 30,100 30,40\" />\n            </g>\n          </g>\n        </g>\n\n        <!-- NSEW and ticks -->\n        <g class=\"c-cr__nsew\" :style=\"compassDialStyle\">\n          <g class=\"c-cr__ticks-major\">\n            <path d=\"M50 3L43 10H57L50 3Z\" />\n            <path d=\"M4 51V49H10V51H4Z\" class=\"--hide-min\" />\n            <path d=\"M49 96V90H51V96H49Z\" class=\"--hide-min\" />\n            <path d=\"M90 49V51H96V49H90Z\" class=\"--hide-min\" />\n          </g>\n          <g class=\"c-cr__ticks-minor --hide-small\">\n            <path d=\"M4 51V49H10V51H4Z\" />\n            <path d=\"M90 51V49H96V51H90Z\" />\n            <path d=\"M51 96H49V90H51V96Z\" />\n            <path d=\"M51 10L49 10V4L51 4V10Z\" />\n          </g>\n          <g class=\"c-cr__nsew-text\">\n            <path\n              :style=\"cardinalTextRotateW\"\n              class=\"c-cr__nsew-w --hide-small\"\n              d=\"M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z\"\n            />\n            <path\n              :style=\"cardinalTextRotateE\"\n              class=\"c-cr__nsew-e --hide-small\"\n              d=\"M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z\"\n            />\n            <path\n              :style=\"cardinalTextRotateS\"\n              class=\"c-cr__nsew-s --hide-small\"\n              d=\"M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z\"\n            />\n            <path\n              :style=\"cardinalTextRotateN\"\n              class=\"c-cr__nsew-n\"\n              d=\"M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z\"\n            />\n          </g>\n        </g>\n      </g>\n      <defs>\n        <radialGradient\n          id=\"gradient_edge\"\n          cx=\"0\"\n          cy=\"0\"\n          r=\"1\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"translate(50 50) rotate(90) scale(50)\"\n        >\n          <stop offset=\"0.6\" stop-opacity=\"0\" />\n          <stop offset=\"1\" stop-color=\"white\" />\n        </radialGradient>\n        <radialGradient\n          id=\"gradient_sun\"\n          cx=\"0\"\n          cy=\"0\"\n          r=\"1\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"translate(50 -7) rotate(-90) scale(18.5)\"\n        >\n          <stop offset=\"0.7\" stop-color=\"#FFCC00\" />\n          <stop offset=\"0.7\" stop-color=\"#FFCC00\" stop-opacity=\"0.6\" />\n          <stop offset=\"1\" stop-color=\"#FF6600\" stop-opacity=\"0\" />\n        </radialGradient>\n      </defs>\n    </svg>\n  </div>\n</template>\n\n<script>\nimport { throttle } from 'lodash';\n\nimport { rotate } from './utils.js';\n\nexport default {\n  props: {\n    cameraAngleOfView: {\n      type: Number,\n      required: true\n    },\n    heading: {\n      type: Number,\n      required: true\n    },\n    cameraAzimuth: {\n      type: Number,\n      default: undefined\n    },\n    transformations: {\n      type: Object,\n      required: true\n    },\n    hasGimble: {\n      type: Boolean,\n      required: true\n    },\n    normalizedCameraAzimuth: {\n      type: Number,\n      required: true\n    },\n    sunHeading: {\n      type: Number,\n      default: undefined\n    },\n    sizedImageDimensions: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      lockCompass: true\n    };\n  },\n  computed: {\n    camAngleAndPositionStyle() {\n      const translateX = this.transformations?.translateX;\n      const translateY = this.transformations?.translateY;\n      const rotation = this.transformations?.rotation;\n      const scale = this.transformations?.scale;\n\n      return {\n        transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})`\n      };\n    },\n    gimbledCameraPanStyle() {\n      if (!this.hasGimble) {\n        return;\n      }\n\n      const gimbledCameraPan = rotate(this.normalizedCameraAzimuth, -this.heading);\n\n      return {\n        transform: `rotate(${-gimbledCameraPan}deg)`\n      };\n    },\n    compassDialStyle() {\n      return { transform: `rotate(${this.north}deg)` };\n    },\n    north() {\n      return this.lockCompass ? rotate(-this.normalizedCameraAzimuth) : 0;\n    },\n    cardinalTextRotateN() {\n      return { transform: `translateY(-27%) rotate(${-this.north}deg)` };\n    },\n    cardinalTextRotateS() {\n      return { transform: `translateY(30%) rotate(${-this.north}deg)` };\n    },\n    cardinalTextRotateE() {\n      return { transform: `translateX(30%) rotate(${-this.north}deg)` };\n    },\n    cardinalTextRotateW() {\n      return { transform: `translateX(-30%) rotate(${-this.north}deg)` };\n    },\n    hasHeading() {\n      return this.heading !== undefined;\n    },\n    hasSunHeading() {\n      return this.sunHeading !== undefined;\n    },\n    sunHeadingStyle() {\n      const rotation = rotate(this.north, this.sunHeading);\n\n      return {\n        transform: `rotate(${rotation}deg)`\n      };\n    },\n    cameraHeadingStyle() {\n      const rotation = rotate(this.north, this.normalizedCameraAzimuth);\n\n      return {\n        transform: `rotate(${rotation}deg)`\n      };\n    },\n    // left half of camera field of view\n    // rotated counter-clockwise from camera pan angle\n    cameraFOVStyleLeftHalf() {\n      return {\n        transform: `rotate(${this.cameraAngleOfView / 2}deg)`\n      };\n    },\n    // right half of camera field of view\n    // rotated clockwise from camera pan angle\n    cameraFOVStyleRightHalf() {\n      return {\n        transform: `rotate(${-this.cameraAngleOfView / 2}deg)`\n      };\n    },\n    compassRoseSizingClasses() {\n      let compassRoseSizingClasses = '';\n      if (this.sizedImageWidth < 300) {\n        compassRoseSizingClasses = '--rose-small --rose-min';\n      } else if (this.sizedImageWidth < 500) {\n        compassRoseSizingClasses = '--rose-small';\n      } else if (this.sizedImageWidth > 1000) {\n        compassRoseSizingClasses = '--rose-max';\n      }\n\n      return compassRoseSizingClasses;\n    },\n    sizedImageWidth() {\n      return this.sizedImageDimensions.width;\n    },\n    sizedImageHeight() {\n      return this.sizedImageDimensions.height;\n    }\n  },\n  watch: {\n    sizedImageDimensions() {\n      this.debounceResizeSvg();\n    }\n  },\n  mounted() {\n    this.debounceResizeSvg = throttle(this.resizeSvg, 100);\n    this.debounceResizeSvg();\n  },\n  methods: {\n    resizeSvg() {\n      const svg = this.$refs.compassRoseSvg;\n      // Component may have been unmounted before final debounce executes, so ensure it still exists.\n      if (svg !== null && svg !== undefined) {\n        svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth);\n        svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight);\n      }\n    },\n    toggleLockCompass() {\n      this.lockCompass = !this.lockCompass;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/compass.scss",
    "content": "/***************************** THEME/UI CONSTANTS AND MIXINS */\n$interfaceKeyColor: #fff;\n$elemBg: rgba(black, 0.7);\n\n@mixin sun($position: 'circle closest-side') {\n  $color: #ff9900;\n  $gradEdgePerc: 60%;\n  background: radial-gradient(\n    #{$position},\n    $color,\n    $color $gradEdgePerc,\n    rgba($color, 0.4) $gradEdgePerc + 5%,\n    transparent\n  );\n}\n\n.c-compass {\n  pointer-events: none; // This allows the image element to receive a browser-level context click\n  position: absolute;\n  left: 0;\n  top: 0;\n  z-index: 3;\n  @include userSelectNone;\n}\n\n/***************************** COMPASS HUD */\n.c-hud {\n  // To be placed within a imagery view, in the bounding box of the image\n  $m: 1px;\n  $padTB: 2px;\n  $padLR: $padTB;\n  color: $interfaceKeyColor;\n  font-size: 0.8em;\n  position: absolute;\n  top: $m;\n  right: $m;\n  left: $m;\n  height: 18px;\n\n  svg,\n  div {\n    position: absolute;\n  }\n\n  &__display {\n    height: 30px;\n    pointer-events: all;\n    position: absolute;\n    top: 0;\n    right: 0;\n    left: 0;\n  }\n\n  &__range {\n    border: 1px solid $interfaceKeyColor;\n    border-top-color: transparent;\n    position: absolute;\n    top: 50%;\n    right: $padLR;\n    bottom: $padTB;\n    left: $padLR;\n  }\n\n  [class*='__dir'] {\n    // NSEW\n    display: inline-block;\n    font-weight: bold;\n    text-shadow: 0 1px 2px black;\n    top: 50%;\n    transform: translate(-50%, -50%);\n    z-index: 2;\n  }\n\n  [class*='__dir--sub'] {\n    font-weight: normal;\n    opacity: 0.5;\n  }\n\n  &__sun {\n    $s: 10px;\n    @include sun('circle farthest-side at bottom');\n    bottom: $padTB + 2px;\n    height: $s;\n    width: $s * 2;\n    opacity: 0.8;\n    transform: translateX(-50%);\n    z-index: 1;\n  }\n}\n\n/***************************** COMPASS SVG */\n.c-compass-rose-svg {\n  $color: $interfaceKeyColor;\n  position: absolute;\n  top: 0;\n  left: 0;\n\n  g,\n  path,\n  rect {\n    // In an SVG, rotation occurs about the center of the SVG, not the element\n    transform-origin: center;\n  }\n\n  .c-cr {\n    &__bg {\n      fill: #000;\n      opacity: 0.8;\n    }\n\n    &__edge {\n      opacity: 0.2;\n    }\n\n    &__sun {\n      opacity: 0.7;\n    }\n\n    &__cam {\n      fill: $interfaceKeyColor;\n      transform-origin: center;\n      transform: scale(0.15);\n    }\n\n    &__cam-fov-l,\n    &__cam-fov-r {\n      // Cam FOV indication\n      opacity: 0.2;\n      fill: #fff;\n    }\n\n    &__nsew-text,\n    &__ticks-major,\n    &__ticks-minor {\n      fill: $color;\n    }\n\n    &__ticks-minor {\n      opacity: 0.5;\n      transform: rotate(45deg);\n    }\n\n    &__spacecraft-body {\n      opacity: 0.3;\n    }\n  }\n}\n\n/***************************** DIRECTION ROSE */\n.w-direction-rose {\n  $s: 10%;\n  $m: 2%;\n  cursor: pointer;\n  pointer-events: all;\n  position: absolute;\n  bottom: $m;\n  left: $m;\n  width: $s;\n  padding-top: $s;\n  z-index: 2;\n\n  &.--rose-min {\n    $s: 30px;\n    width: $s;\n    padding-top: $s;\n    .--hide-min {\n      display: none;\n    }\n  }\n\n  &.--rose-small {\n    .--hide-small {\n      display: none;\n    }\n  }\n\n  &.--rose-max {\n    $s: 100px;\n    width: $s;\n    padding-top: $s;\n  }\n}\n\n/************************** ROVER */\n.cr-vrover {\n  $scale: 0.4;\n  transform-origin: center;\n\n  &__body {\n    fill: $interfaceKeyColor;\n    opacity: 0.3;\n    transform-origin: center 7% !important; // Places rotation center at mast position\n  }\n}\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport Compass from './CompassComponent.vue';\n\nconst COMPASS_ROSE_CLASS = '.c-direction-rose';\nconst COMPASS_HUD_CLASS = '.c-compass__hud';\n\ndescribe('The Compass component', () => {\n  let _destroy;\n  let instance;\n\n  beforeEach(() => {\n    let imageDatum = {\n      heading: 100,\n      roll: 90,\n      pitch: 90,\n      cameraTilt: 100,\n      cameraAzimuth: 90,\n      sunAngle: 30,\n      transformations: {\n        translateX: 0,\n        translateY: 18,\n        rotation: 0,\n        scale: 0.3,\n        cameraAngleOfView: 70\n      }\n    };\n    let propsData = {\n      naturalAspectRatio: 0.9,\n      image: imageDatum,\n      sizedImageDimensions: {\n        width: 100,\n        height: 100\n      }\n    };\n\n    const { vNode, destroy } = mount({\n      components: { Compass },\n      data() {\n        return propsData;\n      },\n      template: `<Compass\n                :image=\"image\"\n                :natural-aspect-ratio=\"naturalAspectRatio\"\n                :sized-image-dimensions=\"sizedImageDimensions\"\n            />`\n    });\n    _destroy = destroy;\n    instance = vNode.componentInstance;\n  });\n\n  afterAll(() => {\n    _destroy();\n  });\n\n  describe('when a heading value and cameraAngleOfView exists on the image', () => {\n    it('should display a compass rose', () => {\n      let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS);\n\n      expect(compassRoseElement).toBeDefined();\n    });\n\n    it('should display a compass HUD', () => {\n      let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS);\n\n      expect(compassHUDElement).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/imagery/components/Compass/utils.js",
    "content": "/**\n *\n * sums an arbitrary number of absolute rotations\n * (meaning rotations relative to one common direction 0)\n * normalizes the rotation to the range [0, 360)\n *\n * @param  {...number} rotations in degrees\n * @returns {number} normalized sum of all rotations - [0, 360) degrees\n */\nexport function rotate(...rotations) {\n  const rotation = rotations.reduce((a, b) => a + b, 0);\n\n  return normalizeCompassDirection(rotation);\n}\n\nexport function inRange(degrees, [min, max]) {\n  const point = rotate(degrees);\n\n  return min > max\n    ? (point >= min && point < 360) || (point <= max && point >= 0)\n    : point >= min && point <= max;\n}\n\nexport function percentOfRange(degrees, [min, max]) {\n  let distance = rotate(degrees);\n  let minRange = min;\n  let maxRange = max;\n\n  if (min > max) {\n    if (distance < max) {\n      distance += 360;\n    }\n\n    maxRange += 360;\n  }\n\n  return (distance - minRange) / (maxRange - minRange);\n}\n\nfunction normalizeCompassDirection(degrees) {\n  const base = degrees % 360;\n\n  return base >= 0 ? base : 360 + base;\n}\n"
  },
  {
    "path": "src/plugins/imagery/components/FilterSettings.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls c-image-controls--filters\"\n    @click=\"handleClose\"\n  >\n    <div class=\"c-image-controls__controls\" @click=\"$event.stopPropagation()\">\n      <span class=\"c-image-controls__sliders\">\n        <div class=\"c-image-controls__slider-wrapper icon-brightness\">\n          <input\n            v-model=\"filters.brightness\"\n            type=\"range\"\n            min=\"0\"\n            max=\"500\"\n            draggable=\"true\"\n            @dragstart.stop.prevent\n            @change=\"notifyFiltersChanged\"\n            @input=\"notifyFiltersChanged\"\n          />\n        </div>\n        <div class=\"c-image-controls__slider-wrapper icon-contrast\">\n          <input\n            v-model=\"filters.contrast\"\n            type=\"range\"\n            min=\"0\"\n            max=\"500\"\n            draggable=\"true\"\n            @dragstart.stop.prevent\n            @change=\"notifyFiltersChanged\"\n            @input=\"notifyFiltersChanged\"\n          />\n        </div>\n      </span>\n      <span class=\"c-image-controls__reset-btn\">\n        <a class=\"s-icon-button icon-reset t-btn-reset\" @click=\"resetFilters\"></a>\n      </span>\n    </div>\n\n    <button class=\"c-click-icon icon-x t-btn-close c-switcher-menu__close-button\"></button>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  emits: ['filter-changed'],\n  data() {\n    return {\n      filters: {\n        brightness: 100,\n        contrast: 100\n      }\n    };\n  },\n  methods: {\n    handleClose(e) {\n      const closeButton = e.target.classList.contains('c-switcher-menu__close-button');\n      if (!closeButton) {\n        e.stopPropagation();\n      }\n    },\n    notifyFiltersChanged() {\n      this.$emit('filter-changed', this.filters);\n    },\n    resetFilters() {\n      this.filters = {\n        brightness: 100,\n        contrast: 100\n      };\n      this.notifyFiltersChanged();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/ImageControls.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover\"\n    role=\"toolbar\"\n    aria-label=\"Image controls\"\n  >\n    <ImageryViewMenuSwitcher\n      :icon-class=\"'icon-brightness'\"\n      :aria-label=\"'Brightness and contrast'\"\n      :title=\"'Brightness and contrast'\"\n    >\n      <FilterSettings @filter-changed=\"updateFilterValues\" />\n    </ImageryViewMenuSwitcher>\n\n    <ImageryViewMenuSwitcher\n      v-if=\"layers.length\"\n      icon-class=\"icon-layers\"\n      aria-label=\"Layers\"\n      title=\"Layers\"\n    >\n      <LayerSettings :layers=\"layers\" @toggle-layer-visibility=\"toggleLayerVisibility\" />\n    </ImageryViewMenuSwitcher>\n\n    <ZoomSettings\n      class=\"--hide-if-less-than-220\"\n      :pan-zoom-locked=\"panZoomLocked\"\n      :zoom-factor=\"zoomFactor\"\n    />\n\n    <ImageryViewMenuSwitcher\n      class=\"--show-if-less-than-220\"\n      :icon-class=\"'icon-magnify'\"\n      :aria-label=\"'Zoom settings'\"\n      :title=\"'Zoom settings'\"\n    >\n      <ZoomSettings\n        :pan-zoom-locked=\"panZoomLocked\"\n        :class=\"'c-control-menu c-menu--has-close-btn'\"\n        :zoom-factor=\"zoomFactor\"\n        :is-menu=\"true\"\n      />\n    </ImageryViewMenuSwitcher>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport FilterSettings from './FilterSettings.vue';\nimport ImageryViewMenuSwitcher from './ImageryViewMenuSwitcher.vue';\nimport LayerSettings from './LayerSettings.vue';\nimport ZoomSettings from './ZoomSettings.vue';\n\nconst DEFAULT_FILTER_VALUES = {\n  brightness: '100',\n  contrast: '100'\n};\n\nconst ZOOM_LIMITS_MAX_DEFAULT = 20;\nconst ZOOM_LIMITS_MIN_DEFAULT = 1;\nconst ZOOM_STEP = 1;\nconst ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;\n\nexport default {\n  components: {\n    FilterSettings,\n    LayerSettings,\n    ImageryViewMenuSwitcher,\n    ZoomSettings\n  },\n  inject: ['openmct', 'domainObject', 'resetImage', 'handlePanZoomUpdate'],\n  provide() {\n    return {\n      resetImage: this.resetImage,\n      zoomIn: this.zoomIn,\n      zoomOut: this.zoomOut,\n      toggleZoomLock: this.toggleZoomLock\n    };\n  },\n  props: {\n    layers: {\n      type: Array,\n      required: true\n    },\n    zoomFactor: {\n      type: Number,\n      required: true\n    },\n    imageUrl: {\n      type: String,\n      default: () => {\n        return '';\n      }\n    }\n  },\n  emits: [\n    'cursors-updated',\n    'pan-zoom-updated',\n    'filters-updated',\n    'start-pan',\n    'toggle-layer-visibility'\n  ],\n  data() {\n    return {\n      altPressed: false,\n      shiftPressed: false,\n      metaPressed: false,\n      panZoomLocked: false,\n      wheelZooming: false,\n      filters: {\n        brightness: 100,\n        contrast: 100\n      }\n    };\n  },\n  computed: {\n    cursorStates() {\n      const isPannable = this.altPressed && this.zoomFactor > 1;\n      const showCursorZoomIn = this.metaPressed && !this.shiftPressed;\n      const showCursorZoomOut = this.metaPressed && this.shiftPressed;\n      const modifierKeyPressed = Boolean(this.metaPressed || this.shiftPressed || this.altPressed);\n\n      return {\n        isPannable,\n        showCursorZoomIn,\n        showCursorZoomOut,\n        modifierKeyPressed\n      };\n    }\n  },\n  watch: {\n    imageUrl(newUrl, oldUrl) {\n      // reset image pan/zoom if newUrl only if not locked\n      if (newUrl && !this.panZoomLocked) {\n        this.resetImage();\n      }\n    },\n    cursorStates(states) {\n      this.$emit('cursors-updated', states);\n    }\n  },\n  mounted() {\n    document.addEventListener('keydown', this.handleKeyDown);\n    document.addEventListener('keyup', this.handleKeyUp);\n    this.clearWheelZoom = _.debounce(this.clearWheelZoom, 600);\n  },\n  beforeUnmount() {\n    document.removeEventListener('keydown', this.handleKeyDown);\n    document.removeEventListener('keyup', this.handleKeyUp);\n  },\n  methods: {\n    toggleZoomLock() {\n      this.panZoomLocked = !this.panZoomLocked;\n    },\n    notifyFiltersChanged() {\n      this.$emit('filters-updated', this.filters);\n    },\n    handleResetFilters() {\n      this.filters = { ...DEFAULT_FILTER_VALUES };\n      this.notifyFiltersChanged();\n    },\n    limitZoomRange(factor) {\n      return Math.min(Math.max(ZOOM_LIMITS_MIN_DEFAULT, factor), ZOOM_LIMITS_MAX_DEFAULT);\n    },\n    // used to increment the zoom without knowledge of current level\n    processZoom(increment, userCoordX, userCoordY) {\n      const newFactor = this.limitZoomRange(this.zoomFactor + increment);\n      this.zoomImage(newFactor, userCoordX, userCoordY);\n    },\n    zoomImage(newScaleFactor, screenClientX, screenClientY) {\n      if (!(newScaleFactor || Number.isInteger(newScaleFactor))) {\n        console.error('Scale factor provided is invalid');\n\n        return;\n      }\n\n      if (newScaleFactor > ZOOM_LIMITS_MAX_DEFAULT) {\n        newScaleFactor = ZOOM_LIMITS_MAX_DEFAULT;\n      }\n\n      if (newScaleFactor <= 0 || newScaleFactor <= ZOOM_LIMITS_MIN_DEFAULT) {\n        return this.resetImage();\n      }\n\n      this.handlePanZoomUpdate({\n        newScaleFactor,\n        screenClientX,\n        screenClientY\n      });\n    },\n    wheelZoom(e) {\n      // only use x,y coordinates on scrolling in\n      if (this.wheelZooming === false && e.deltaY > 0) {\n        this.wheelZooming = true;\n        // grab first x,y coordinates\n        this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION, e.clientX, e.clientY);\n      } else {\n        // ignore subsequent event x,y so scroll drift doesn't occur\n        this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION);\n      }\n\n      // debounced method that will only fire after the scroll series is complete\n      this.clearWheelZoom();\n    },\n    /* debounced method so that wheelZooming state will\n     ** remain true through a zoom event series\n     */\n    clearWheelZoom() {\n      this.wheelZooming = false;\n    },\n    handleKeyDown(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = true;\n      }\n\n      if (event.metaKey) {\n        this.metaPressed = true;\n      }\n\n      if (event.shiftKey) {\n        this.shiftPressed = true;\n      }\n    },\n    handleKeyUp(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = false;\n      }\n\n      this.shiftPressed = false;\n      if (!event.metaKey) {\n        this.metaPressed = false;\n      }\n    },\n    zoomIn() {\n      this.processZoom(ZOOM_STEP);\n    },\n    zoomOut() {\n      this.processZoom(-ZOOM_STEP);\n    },\n    // attached to onClick listener in ImageryView\n    handlePanZoomClick(e) {\n      if (this.altPressed) {\n        return this.$emit('start-pan', e);\n      }\n\n      if (!(this.metaPressed && e.button === 0)) {\n        return;\n      }\n\n      const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);\n      this.zoomImage(newScaleFactor, e.clientX, e.clientY);\n    },\n    toggleLayerVisibility(index) {\n      this.$emit('toggle-layer-visibility', index);\n    },\n    updateFilterValues(filters) {\n      this.filters = filters;\n      this.notifyFiltersChanged();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/ImageThumbnail.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-imagery__thumb c-thumb\"\n    :class=\"{\n      active: active,\n      selected: selected,\n      'real-time': realTime\n    }\"\n    :aria-label=\"ariaLabel\"\n    :title=\"image.formattedTime\"\n    role=\"button\"\n    tabindex=\"0\"\n    @click=\"handleClick\"\n  >\n    <a class=\"c-thumb__image-wrapper\" href=\"\" :download=\"image.imageDownloadName\" @click.prevent>\n      <img\n        ref=\"img\"\n        class=\"c-thumb__image\"\n        :src=\"imageSrc\"\n        fetchpriority=\"low\"\n        @load=\"imageLoadCompleted\"\n      />\n      <i\n        v-show=\"showAnnotationIndicator\"\n        class=\"c-thumb__annotation-indicator icon-status-poll-edit\"\n      >\n      </i>\n    </a>\n    <div v-if=\"viewableArea\" class=\"c-thumb__viewable-area\" :style=\"viewableAreaStyle\"></div>\n    <div class=\"c-thumb__timestamp\">{{ image.formattedTime }}</div>\n  </div>\n</template>\n\n<script>\nimport { encode_url } from '../../../utils/encoding';\n\nconst THUMB_PADDING = 4;\nconst BORDER_WIDTH = 2;\n\nexport default {\n  props: {\n    image: {\n      type: Object,\n      required: true\n    },\n    active: {\n      type: Boolean,\n      required: true\n    },\n    selected: {\n      type: Boolean,\n      required: true\n    },\n    realTime: {\n      type: Boolean,\n      required: true\n    },\n    imageryAnnotations: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    viewableArea: {\n      type: Object,\n      default: function () {\n        return null;\n      }\n    }\n  },\n  emits: ['click'],\n  data() {\n    return {\n      imgWidth: 0,\n      imgHeight: 0\n    };\n  },\n  computed: {\n    imageSrc() {\n      return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;\n    },\n    ariaLabel() {\n      return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;\n    },\n    viewableAreaStyle() {\n      if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {\n        return null;\n      }\n\n      const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;\n      const imgWidth = this.imgWidth;\n      const imgHeight = this.imgHeight;\n\n      let translateX = imgWidth * xOffsetRatio;\n      let translateY = imgHeight * yOffsetRatio;\n      let width = imgWidth * widthRatio;\n      let height = imgHeight * heightRatio;\n\n      if (translateX < 0) {\n        width += translateX;\n        translateX = 0;\n      }\n\n      if (translateX + width > imgWidth) {\n        width = imgWidth - translateX;\n      }\n\n      if (translateX + 2 * BORDER_WIDTH > imgWidth) {\n        translateX = imgWidth - 2 * BORDER_WIDTH;\n      }\n\n      if (translateY < 0) {\n        height += translateY;\n        translateY = 0;\n      }\n\n      if (translateY + height > imgHeight) {\n        height = imgHeight - translateY;\n      }\n\n      if (translateY + 2 * BORDER_WIDTH > imgHeight) {\n        translateY = imgHeight - 2 * BORDER_WIDTH;\n      }\n\n      return {\n        transform: `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,\n        width: `${width}px`,\n        height: `${height}px`\n      };\n    },\n    showAnnotationIndicator() {\n      return this.imageryAnnotations.some((annotation) => {\n        return !annotation._deleted;\n      });\n    }\n  },\n  methods: {\n    handleClick(event) {\n      this.$emit('click', event);\n    },\n    imageLoadCompleted() {\n      if (!this.$refs.img) {\n        return;\n      }\n\n      const { width: imgWidth, height: imgHeight } = this.$refs.img;\n      this.imgWidth = imgWidth;\n      this.imgHeight = imgHeight;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/ImageryTimeView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"imagery\" class=\"c-imagery-tsv js-imagery-tsv\" :style=\"alignmentStyle\">\n    <div ref=\"imageryHolder\" class=\"c-imagery-tsv__contents u-contents\"></div>\n  </div>\n</template>\n\n<script>\nimport { scaleLinear, scaleUtc } from 'd3-scale';\nimport _ from 'lodash';\nimport mount from 'utils/mount';\nimport { inject } from 'vue';\n\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport { useAlignment } from '../../../ui/composables/alignmentContext';\nimport imageryData from '../../imagery/mixins/imageryData.js';\n\nconst AXES_PADDING = 20;\nconst PADDING = 1;\nconst IMAGE_WIDTH_THRESHOLD = 25;\nconst CONTAINER_CLASS = 'c-imagery-tsv-container';\nconst NO_ITEMS_CLASS = 'c-timeline__no-items';\nconst IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';\nconst ID_PREFIX = 'wrapper-';\n\nexport default {\n  mixins: [imageryData],\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  setup() {\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);\n    return { alignmentData };\n  },\n  data() {\n    let timeSystem = this.openmct.time.getTimeSystem();\n    this.metadata = {};\n    this.requestCount = 0;\n\n    return {\n      viewBounds: undefined,\n      height: 0,\n      durationFormatter: undefined,\n      imageHistory: [],\n      timeSystem: timeSystem,\n      keyString: undefined\n    };\n  },\n  computed: {\n    alignmentStyle() {\n      let leftOffset = 0;\n      const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;\n      if (this.alignmentData.leftWidth) {\n        leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;\n      }\n      return {\n        margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`\n      };\n    }\n  },\n  watch: {\n    imageHistory: {\n      handler(newHistory, oldHistory) {\n        this.updatePlotImagery();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n\n    // Why are we doing this? This element causes scroll problems in the swimlane.\n    // this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));\n    // this.canvas.height = 0;\n    // this.canvas.width = 10;\n    // this.canvasContext = this.canvas.getContext('2d');\n    this.setDimensions();\n\n    this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);\n    this.updateViewBounds = this.updateViewBounds.bind(this);\n    this.setTimeContext = this.setTimeContext.bind(this);\n    this.setTimeContext();\n\n    this.updateViewBounds();\n\n    this.resize = _.debounce(this.resize, 400);\n    this.imageryStripResizeObserver = new ResizeObserver(this.resize);\n    this.imageryStripResizeObserver.observe(this.$refs.imagery);\n\n    this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);\n  },\n  beforeUnmount() {\n    if (this.imageryStripResizeObserver) {\n      this.imageryStripResizeObserver.disconnect();\n    }\n\n    this.stopFollowingTimeContext();\n    if (this.unlisten) {\n      this.unlisten();\n    }\n    if (this.destroyImageryContainer) {\n      this.destroyImageryContainer();\n    }\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      this.timeContext.on('timeSystem', this.setScaleAndPlotImagery);\n      this.timeContext.on('boundsChanged', this.updateViewBounds);\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('timeSystem', this.setScaleAndPlotImagery);\n        this.timeContext.off('boundsChanged', this.updateViewBounds);\n      }\n    },\n    expand(imageTimestamp) {\n      const path = this.objectPath[0];\n      this.previewAction.invoke([path], {\n        timestamp: imageTimestamp,\n        objectPath: this.objectPath\n      });\n    },\n    observeForChanges(mutatedObject) {\n      this.updateViewBounds();\n    },\n    resize() {\n      let clientWidth = this.getClientWidth();\n      if (clientWidth !== this.width) {\n        this.setDimensions();\n        this.updateViewBounds();\n      }\n    },\n    getClientWidth() {\n      let clientWidth = this.$refs.imagery.clientWidth;\n\n      if (!clientWidth) {\n        //this is a hack - need a better way to find the parent of this component\n        let parent = this.openmct.layout.$refs.browseObject.$el;\n        if (parent) {\n          clientWidth = parent.getBoundingClientRect().width;\n        }\n      }\n\n      return clientWidth;\n    },\n    updateViewBounds(bounds, isTick) {\n      this.viewBounds = this.timeContext.getBounds();\n\n      if (this.timeSystem === undefined) {\n        this.timeSystem = this.timeContext.getTimeSystem();\n      }\n\n      this.setScaleAndPlotImagery(this.timeSystem, !isTick);\n    },\n    setScaleAndPlotImagery(timeSystem, clearAllImagery) {\n      if (timeSystem !== undefined) {\n        this.timeSystem = timeSystem;\n        this.timeFormatter = this.getFormatter(this.timeSystem.key);\n      }\n\n      this.setScale(this.timeSystem);\n      this.updatePlotImagery(clearAllImagery);\n    },\n    getFormatter(key) {\n      const metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n\n      let metadataValue = metadata.value(key) || { format: key };\n      let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n      return valueFormatter;\n    },\n    updatePlotImagery(clearAllImagery) {\n      this.clearPreviousImagery(clearAllImagery);\n      if (this.xScale) {\n        this.drawImagery();\n      }\n    },\n    clearPreviousImagery(clearAllImagery) {\n      //TODO: Only clear items that are out of bounds\n      let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);\n      noItemsEl.forEach((item) => {\n        item.remove();\n      });\n      let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);\n      imagery.forEach((imageElm) => {\n        if (clearAllImagery) {\n          imageElm.remove();\n        } else {\n          const id = imageElm.getAttributeNS(null, 'id');\n          if (id) {\n            const timestamp = id.replace(ID_PREFIX, '');\n            if (\n              !this.isImageryInBounds({\n                time: timestamp\n              })\n            ) {\n              imageElm.remove();\n            }\n          }\n        }\n      });\n    },\n    setDimensions() {\n      const imageryHolder = this.$refs.imagery;\n      this.width = this.getClientWidth();\n      this.height = Math.round(imageryHolder.getBoundingClientRect().height);\n      this.imageHeight = this.height - 10;\n    },\n    setScale(timeSystem) {\n      if (!this.width) {\n        return;\n      }\n\n      if (timeSystem === undefined) {\n        timeSystem = this.timeContext.getTimeSystem();\n      }\n\n      if (timeSystem.isUTCBased) {\n        this.xScale = scaleUtc();\n        this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);\n      } else {\n        this.xScale = scaleLinear();\n        this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);\n      }\n\n      this.xScale.range([PADDING, this.width - PADDING * 2]);\n    },\n    isImageryInBounds(imageObj) {\n      return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;\n    },\n    getImageryContainer() {\n      let imageryContainer;\n\n      let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);\n      if (existingContainer) {\n        imageryContainer = existingContainer;\n      } else {\n        if (this.destroyImageryContainer) {\n          this.destroyImageryContainer();\n        }\n        const { vNode, destroy } = mount(\n          {\n            components: {\n              SwimLane\n            },\n            provide: {\n              openmct: this.openmct\n            },\n            data() {\n              return {\n                isNested: true\n              };\n            },\n            template: `<swim-lane :is-nested=\"isNested\" :hide-label=\"true\"><template v-slot:object><div class=\"c-imagery-tsv-container\"></div></template></swim-lane>`\n          },\n          {\n            app: this.openmct.app\n          }\n        );\n\n        this.destroyImageryContainer = destroy;\n        const component = vNode.componentInstance;\n        this.$refs.imageryHolder.appendChild(component.$el);\n\n        imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);\n      }\n\n      return imageryContainer;\n    },\n    isImageryWidthAcceptable() {\n      // We're calculating if there is enough space between images to show the thumbnails.\n      // This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but\n      // we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.\n      // TODO: Use telemetry.requestCollection to get sorted telemetry\n      const currentStart = this.viewBounds.start;\n      const currentEnd = this.viewBounds.end;\n      const rectX = this.xScale(currentStart);\n      const rectY = this.xScale(currentEnd);\n      const imageContainerWidth = this.imageHistory.length\n        ? (rectY - rectX) / this.imageHistory.length\n        : 0;\n\n      return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;\n    },\n    drawImagery() {\n      let imageryContainer = this.getImageryContainer();\n      const showImagePlaceholders = this.isImageryWidthAcceptable();\n      let index = 0;\n      if (this.imageHistory.length) {\n        this.imageHistory.forEach((currentImageObject) => {\n          if (this.isImageryInBounds(currentImageObject)) {\n            this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);\n            index = index + 1;\n          }\n        });\n      } else {\n        this.plotNoItems(imageryContainer);\n      }\n    },\n    plotNoItems(containerElement) {\n      let textElement = document.createElement('div');\n      textElement.classList.add(NO_ITEMS_CLASS);\n      textElement.innerHTML = 'No images within timeframe';\n\n      containerElement.appendChild(textElement);\n    },\n    setNSAttributesForElement(element, attributes) {\n      if (!element) {\n        return;\n      }\n\n      Object.keys(attributes).forEach((key) => {\n        element.setAttributeNS(null, key, attributes[key]);\n      });\n    },\n    setStyles(element, styles) {\n      if (!element) {\n        return;\n      }\n\n      Object.keys(styles).forEach((key) => {\n        element.style[key] = styles[key];\n      });\n    },\n    getImageWrapper(item) {\n      const id = `${ID_PREFIX}${item.time}`;\n\n      return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);\n    },\n    plotImagery(item, showImagePlaceholders, containerElement, index) {\n      let existingImageWrapper = this.getImageWrapper(item);\n      //imageWrapper wraps the vertical tick and the image\n      if (existingImageWrapper) {\n        this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);\n      } else {\n        let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);\n        containerElement.appendChild(imageWrapper);\n      }\n    },\n    setImageDisplay(imageElement, showImagePlaceholders) {\n      if (showImagePlaceholders) {\n        imageElement.style.display = 'none';\n      } else {\n        imageElement.style.display = 'block';\n      }\n    },\n    updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) {\n      //Update the x co-ordinates of the image wrapper and the url of image\n      //this is to avoid tearing down all elements completely and re-drawing them\n      this.setNSAttributesForElement(existingImageWrapper, {\n        'data-show-image-placeholders': showImagePlaceholders\n      });\n      existingImageWrapper.style.left = `${this.xScale(image.time)}px`;\n\n      let imageElement = existingImageWrapper.querySelector('img');\n      this.setNSAttributesForElement(imageElement, {\n        src: image.thumbnailUrl || image.url\n      });\n      this.setImageDisplay(imageElement, showImagePlaceholders);\n    },\n    createImageWrapper(index, image, showImagePlaceholders) {\n      const id = `${ID_PREFIX}${image.time}`;\n      let imageWrapper = document.createElement('div');\n      imageWrapper.ariaLabel = id;\n      imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);\n      imageWrapper.style.left = `${this.xScale(image.time)}px`;\n      this.setNSAttributesForElement(imageWrapper, {\n        id,\n        'data-show-image-placeholders': showImagePlaceholders\n      });\n      //create image vertical tick indicator\n      let imageTickElement = document.createElement('div');\n      imageTickElement.classList.add('c-imagery-tsv__image-handle');\n      imageWrapper.appendChild(imageTickElement);\n\n      //create placeholder - this will also hold the actual image\n      let imagePlaceholder = document.createElement('div');\n      imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');\n      imageWrapper.appendChild(imagePlaceholder);\n\n      //create image element\n      let imageElement = document.createElement('img');\n      this.setNSAttributesForElement(imageElement, {\n        src: image.thumbnailUrl || image.url\n      });\n      this.setImageDisplay(imageElement, showImagePlaceholders);\n\n      //handle mousedown event to show the image in a large view\n      imageWrapper.addEventListener('mousedown', (e) => {\n        if (e.button === 0) {\n          this.expand(image.time);\n        }\n      });\n\n      imagePlaceholder.appendChild(imageElement);\n\n      return imageWrapper;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/ImageryView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    tabindex=\"0\"\n    class=\"c-imagery\"\n    @keyup=\"arrowUpHandler\"\n    @keydown.prevent=\"arrowDownHandler\"\n    @mouseover=\"focusElement\"\n  >\n    <div\n      class=\"c-imagery__main-image-wrapper has-local-controls\"\n      :class=\"imageWrapperStyle\"\n      @mousedown=\"handlePanZoomClick\"\n    >\n      <ImageControls\n        v-show=\"!annotationsBeingMarqueed\"\n        ref=\"imageControls\"\n        :zoom-factor=\"zoomFactor\"\n        :image-url=\"imageUrl\"\n        :layers=\"layers\"\n        @filters-updated=\"setFilters\"\n        @cursors-updated=\"setCursorStates\"\n        @start-pan=\"startPan\"\n        @toggle-layer-visibility=\"toggleLayerVisibility\"\n      />\n      <div\n        ref=\"imageBG\"\n        class=\"c-imagery__main-image__bg\"\n        aria-label=\"Background Image\"\n        role=\"button\"\n      >\n        <div v-if=\"zoomFactor > 1\" class=\"c-imagery__hints\">\n          {{ formatImageAltText }}\n        </div>\n        <div\n          ref=\"focusedImageWrapper\"\n          role=\"button\"\n          class=\"image-wrapper\"\n          aria-label=\"Image Wrapper\"\n          :style=\"{\n            width: `${sizedImageWidth}px`,\n            height: `${sizedImageHeight}px`\n          }\"\n          @mousedown=\"handlePanZoomClick\"\n          @dblclick=\"expand\"\n        >\n          <div\n            v-for=\"(layer, index) in visibleLayers\"\n            :key=\"index\"\n            class=\"layer-image s-image-layer c-imagery__layer-image js-layer-image\"\n            :style=\"getVisibleLayerStyles(layer)\"\n          ></div>\n          <img\n            ref=\"focusedImage\"\n            aria-label=\"Focused Image\"\n            class=\"c-imagery__main-image__image js-imageryView-image\"\n            :src=\"imageUrl\"\n            :draggable=\"!isSelectable\"\n            :style=\"{\n              filter: `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`\n            }\"\n            :data-openmct-image-timestamp=\"time\"\n            :data-openmct-object-keystring=\"keyString\"\n            fetchpriority=\"low\"\n          />\n          <div\n            v-show=\"imageUrl\"\n            ref=\"focusedImageElement\"\n            aria-label=\"Focused Image Element\"\n            class=\"c-imagery__main-image__background-image\"\n            :class=\"{ 'is-zooming': isZooming, 'is-panning': isPanning }\"\n            :draggable=\"!isSelectable\"\n            :style=\"focusImageStyles\"\n          ></div>\n          <Compass\n            v-if=\"shouldDisplayCompass\"\n            :image=\"focusedImage\"\n            :sized-image-dimensions=\"sizedImageDimensions\"\n          />\n          <AnnotationsCanvas\n            v-if=\"shouldDisplayAnnotations\"\n            :image=\"focusedImage\"\n            :imagery-annotations=\"imageryAnnotations[focusedImage.time]\"\n            @annotation-marquee-started=\"pauseAndHideImageControls\"\n            @annotation-marquee-finished=\"revealImageControls\"\n            @annotations-changed=\"loadAnnotations\"\n          />\n        </div>\n      </div>\n\n      <button\n        class=\"c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--prev\"\n        title=\"Previous image\"\n        aria-label=\"Previous image\"\n        :disabled=\"isPrevDisabled\"\n        @click=\"prevImage()\"\n      ></button>\n\n      <button\n        class=\"c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--next\"\n        title=\"Next image\"\n        aria-label=\"Next image\"\n        :disabled=\"isNextDisabled\"\n        @click=\"nextImage()\"\n      ></button>\n\n      <div class=\"c-imagery__control-bar\">\n        <div class=\"c-imagery__time\">\n          <div class=\"c-imagery__timestamp u-style-receiver js-style-receiver\">{{ time }}</div>\n\n          <!-- image fresh -->\n          <div\n            v-if=\"canTrackDuration\"\n            :style=\"{\n              'animation-delay': imageFreshnessOptions.fadeOutDelayTime,\n              'animation-duration': imageFreshnessOptions.fadeOutDurationTime\n            }\"\n            :class=\"{ 'c-imagery--new': isImageNew && !refreshCSS }\"\n            class=\"c-imagery__age icon-timer\"\n          >\n            {{ formattedDuration }}\n          </div>\n\n          <!-- spacecraft position fresh -->\n          <div\n            v-if=\"relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh\"\n            class=\"c-imagery__age icon-check c-imagery--new no-animation\"\n          >\n            ROV\n          </div>\n\n          <!-- camera position fresh -->\n          <div\n            v-if=\"relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh\"\n            class=\"c-imagery__age icon-check c-imagery--new no-animation\"\n          >\n            CAM\n          </div>\n        </div>\n        <div class=\"h-local-controls\">\n          <button\n            v-if=\"!isFixed\"\n            class=\"c-button icon-pause pause-play\"\n            :class=\"{ 'is-paused': isPaused }\"\n            aria-label=\"Pause automatic scrolling of image thumbnails\"\n            @click=\"handlePauseButton(!isPaused)\"\n          ></button>\n        </div>\n      </div>\n    </div>\n    <div\n      v-if=\"displayThumbnails\"\n      class=\"c-imagery__thumbs-wrapper\"\n      :class=\"[\n        { 'is-paused': isPaused && !isFixed },\n        { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused },\n        { 'is-small-thumbs': displayThumbnailsSmall },\n        { hide: !displayThumbnails }\n      ]\"\n    >\n      <div\n        ref=\"thumbsWrapper\"\n        class=\"c-imagery__thumbs-scroll-area\"\n        :class=\"[\n          {\n            'animate-scroll': animateThumbScroll\n          }\n        ]\"\n        aria-label=\"Image Thumbnails\"\n        @scroll=\"handleScroll\"\n      >\n        <ImageThumbnail\n          v-for=\"(image, index) in imageHistory\"\n          :key=\"`${image.thumbnailUrl || image.url}-${image.time}-${index}`\"\n          :image=\"image\"\n          :active=\"focusedImageIndex === index\"\n          :imagery-annotations=\"imageryAnnotations[image.time]\"\n          :selected=\"isSelected(index)\"\n          :real-time=\"!isFixed\"\n          :viewable-area=\"focusedImageIndex === index ? viewableArea : null\"\n          @click=\"thumbnailClicked(index)\"\n        />\n      </div>\n\n      <button\n        class=\"c-imagery__auto-scroll-resume-button c-icon-button icon-play\"\n        title=\"Resume automatic scrolling of image thumbnails\"\n        aria-label=\"Resume automatic scrolling of image thumbnails\"\n        @click=\"scrollToRight\"\n      ></button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport moment from 'moment';\nimport { nextTick } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';\nimport imageryData from '@/plugins/imagery/mixins/imageryData.js';\nimport { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';\n\nimport { encode_url } from '../../../utils/encoding';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport AnnotationsCanvas from './AnnotationsCanvas.vue';\nimport Compass from './Compass/CompassComponent.vue';\nimport ImageControls from './ImageControls.vue';\nimport ImageThumbnail from './ImageThumbnail.vue';\nimport RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry.js';\n\nconst REFRESH_CSS_MS = 500;\nconst DURATION_TRACK_MS = 1000;\nconst ARROW_DOWN_DELAY_CHECK_MS = 400;\nconst ARROW_SCROLL_RATE_MS = 100;\nconst THUMBNAIL_CLICKED = true;\n\nconst ONE_MINUTE = 60 * 1000;\nconst FIVE_MINUTES = 5 * ONE_MINUTE;\nconst ONE_HOUR = ONE_MINUTE * 60;\nconst EIGHT_HOURS = 8 * ONE_HOUR;\nconst TWENTYFOUR_HOURS = EIGHT_HOURS * 3;\n\nconst ARROW_RIGHT = 39;\nconst ARROW_LEFT = 37;\n\nconst SCROLL_LATENCY = 250;\n\nconst ZOOM_SCALE_DEFAULT = 1;\nconst SHOW_THUMBS_THRESHOLD_HEIGHT = 200;\nconst SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;\n\nconst IMAGE_CONTAINER_BORDER_WIDTH = 1;\n\nconst DEFAULT_IMAGE_PAN_ALT_TEXT = 'Alt drag to pan';\nconst LINUX_IMAGE_PAN_ALT_TEXT = `Shift+${DEFAULT_IMAGE_PAN_ALT_TEXT}`;\n\nexport default {\n  name: 'ImageryView',\n  components: {\n    Compass,\n    ImageControls,\n    ImageThumbnail,\n    AnnotationsCanvas\n  },\n  mixins: [imageryData],\n  inject: [\n    'openmct',\n    'domainObject',\n    'objectPath',\n    'currentView',\n    'imageFreshnessOptions',\n    'showCompassHUD'\n  ],\n  provide() {\n    return {\n      toggleZoomLock: this.toggleZoomLock,\n      resetImage: this.resetImage,\n      handlePanZoomUpdate: this.handlePanZoomUpdate\n    };\n  },\n  props: {\n    focusedImageTimestamp: {\n      type: Number,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  emits: ['update:focused-image-timestamp'],\n  data() {\n    let timeSystem = this.openmct.time.getTimeSystem();\n    this.metadata = {};\n    this.requestCount = 0;\n\n    return {\n      animateThumbScroll: false,\n      animateZoom: true,\n      annotationsBeingMarqueed: false,\n      autoScroll: true,\n      bounds: {},\n      canTrackDuration: false,\n      cursorStates: {\n        isPannable: false,\n        modifierKeyPressed: false,\n        showCursorZoomIn: false,\n        showCursorZoomOut: false\n      },\n      durationFormatter: undefined,\n      filters: {\n        brightness: 100,\n        contrast: 100\n      },\n      focusedImageIndex: undefined,\n      focusedImageNaturalAspectRatio: undefined,\n      focusedImageRelatedTelemetry: {},\n      forceShowThumbnails: false,\n      imageContainerHeight: undefined,\n      imageContainerWidth: undefined,\n      imageHistory: [],\n      imagePanned: false,\n      imageTranslateX: 0,\n      imageTranslateY: 0,\n      imageViewportHeight: 0,\n      imageViewportWidth: 0,\n      imageryAnnotations: {},\n      isFixed: false,\n      isPaused: false,\n      isZooming: false,\n      keyString: undefined,\n      latestRelatedTelemetry: {},\n      layers: [],\n      lockCompass: true,\n      numericDuration: undefined,\n      pan: null,\n      refreshCSS: false,\n      relatedTelemetry: {},\n      resizingWindow: false,\n      sizedImageHeight: 0,\n      sizedImageWidth: 0,\n      thumbnailClick: THUMBNAIL_CLICKED,\n      timeFormat: '',\n      timeSystem: timeSystem,\n      viewHeight: 0,\n      visibleLayers: [],\n      zoomFactor: ZOOM_SCALE_DEFAULT\n    };\n  },\n  computed: {\n    isPanning() {\n      return Boolean(this.pan);\n    },\n    displayThumbnails() {\n      return this.forceShowThumbnails || this.viewHeight >= SHOW_THUMBS_THRESHOLD_HEIGHT;\n    },\n    displayThumbnailsSmall() {\n      return (\n        this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT &&\n        this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT\n      );\n    },\n    focusImageStyles() {\n      return {\n        filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,\n        backgroundImage: `${\n          this.imageUrl\n            ? `url(${encode_url(this.imageUrl)}),\n                            repeating-linear-gradient(\n                                45deg,\n                                transparent,\n                                transparent 4px,\n                                rgba(125,125,125,.2) 4px,\n                                rgba(125,125,125,.2) 8px\n                            )`\n            : ''\n        }`,\n        transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${\n          this.imageTranslateY / 2\n        }px)`,\n        transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,\n        width: `${this.sizedImageWidth}px`,\n        height: `${this.sizedImageHeight}px`\n      };\n    },\n    time() {\n      return this.formatTime(this.focusedImage);\n    },\n    imageUrl() {\n      return this.formatImageUrl(this.focusedImage);\n    },\n    imageWrapperStyle() {\n      return {\n        cursorZoomIn: this.cursorStates.showCursorZoomIn,\n        cursorZoomOut: this.cursorStates.showCursorZoomOut,\n        pannable: this.cursorStates.isPannable,\n        paused: this.isPaused && !this.isFixed,\n        unsynced: this.isPaused && !this.isFixed,\n        stale: false\n      };\n    },\n    isImageNew() {\n      let cutoff = FIVE_MINUTES;\n      if (this.imageFreshnessOptions) {\n        const { fadeOutDelayTime, fadeOutDurationTime } = this.imageFreshnessOptions;\n        // convert css duration to IS8601 format for parsing\n        const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();\n        const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();\n        const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();\n        const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();\n        cutoff = parsedDuration + parsedDelay;\n      }\n\n      let age = this.numericDuration;\n\n      return age < cutoff && !this.refreshCSS;\n    },\n    isNextDisabled() {\n      let disabled = false;\n\n      if (\n        this.focusedImageIndex === -1 ||\n        this.focusedImageIndex === this.imageHistory.length - 1\n      ) {\n        disabled = true;\n      }\n\n      return disabled;\n    },\n    isPrevDisabled() {\n      let disabled = false;\n\n      if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {\n        disabled = true;\n      }\n\n      return disabled;\n    },\n    isComposedInLayout() {\n      return (\n        this.currentView?.objectPath &&\n        !this.openmct.router.isNavigatedObject(this.currentView.objectPath)\n      );\n    },\n    focusedImage() {\n      return this.imageHistory[this.focusedImageIndex];\n    },\n    parsedSelectedTime() {\n      return this.parseTime(this.focusedImage);\n    },\n    formattedDuration() {\n      let result = 'N/A';\n      let negativeAge = -1;\n      if (!Number.isInteger(this.numericDuration)) {\n        return result;\n      }\n\n      if (this.numericDuration > TWENTYFOUR_HOURS) {\n        negativeAge *= this.numericDuration / TWENTYFOUR_HOURS;\n        result = moment.duration(negativeAge, 'days').humanize(true);\n      } else if (this.numericDuration > EIGHT_HOURS) {\n        negativeAge *= this.numericDuration / ONE_HOUR;\n        result = moment.duration(negativeAge, 'hours').humanize(true);\n      } else if (this.durationFormatter) {\n        result = this.durationFormatter.format(this.numericDuration);\n      }\n\n      return result;\n    },\n    shouldDisplayAnnotations() {\n      const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;\n      const display =\n        this.focusedImage !== undefined &&\n        this.focusedImageNaturalAspectRatio !== undefined &&\n        this.imageContainerWidth !== undefined &&\n        this.imageContainerHeight !== undefined &&\n        imageHeightAndWidth &&\n        this.zoomFactor === 1 &&\n        this.imagePanned !== true;\n\n      return display;\n    },\n    shouldDisplayCompass() {\n      const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;\n      const display =\n        this.focusedImage !== undefined &&\n        this.focusedImageNaturalAspectRatio !== undefined &&\n        this.imageContainerWidth !== undefined &&\n        this.imageContainerHeight !== undefined &&\n        imageHeightAndWidth &&\n        this.zoomFactor === 1 &&\n        this.imagePanned !== true;\n      const hasHeading = this.focusedImage?.heading !== undefined;\n      const hasCameraAngleOfView = this.focusedImage?.transformations?.cameraAngleOfView > 0;\n\n      return display && hasCameraAngleOfView && hasHeading;\n    },\n    isSpacecraftPositionFresh() {\n      let isFresh = undefined;\n      let latest = this.latestRelatedTelemetry;\n      let focused = this.focusedImageRelatedTelemetry;\n\n      if (this.relatedTelemetry.hasRelatedTelemetry) {\n        isFresh = true;\n        for (let key of this.spacecraftPositionKeys) {\n          if (this.relatedTelemetry[key] && latest[key] && focused[key]) {\n            isFresh =\n              isFresh &&\n              Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));\n          } else {\n            isFresh = false;\n          }\n        }\n      }\n\n      return isFresh;\n    },\n    isSpacecraftOrientationFresh() {\n      let isFresh = undefined;\n      let latest = this.latestRelatedTelemetry;\n      let focused = this.focusedImageRelatedTelemetry;\n\n      if (this.relatedTelemetry.hasRelatedTelemetry) {\n        isFresh = true;\n        for (let key of this.spacecraftOrientationKeys) {\n          if (this.relatedTelemetry[key] && latest[key] && focused[key]) {\n            isFresh =\n              isFresh &&\n              Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));\n          } else {\n            isFresh = false;\n          }\n        }\n      }\n\n      return isFresh;\n    },\n    isCameraPositionFresh() {\n      let isFresh = undefined;\n      let latest = this.latestRelatedTelemetry;\n      let focused = this.focusedImageRelatedTelemetry;\n\n      if (this.relatedTelemetry.hasRelatedTelemetry) {\n        isFresh = true;\n\n        // camera freshness relies on spacecraft position freshness\n        if (this.isSpacecraftPositionFresh && this.isSpacecraftOrientationFresh) {\n          for (let key of this.cameraKeys) {\n            if (this.relatedTelemetry[key] && latest[key] && focused[key]) {\n              isFresh =\n                isFresh &&\n                Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));\n            } else {\n              isFresh = false;\n            }\n          }\n        } else {\n          isFresh = false;\n        }\n      }\n\n      return isFresh;\n    },\n    isSelectable() {\n      return true;\n    },\n    sizedImageDimensions() {\n      return {\n        width: this.sizedImageWidth,\n        height: this.sizedImageHeight\n      };\n    },\n    formatImageAltText() {\n      const regexLinux = /Linux/;\n      const navigator = window.navigator.userAgent;\n\n      if (regexLinux.test(navigator)) {\n        return LINUX_IMAGE_PAN_ALT_TEXT;\n      }\n\n      return DEFAULT_IMAGE_PAN_ALT_TEXT;\n    },\n    viewableArea() {\n      if (this.zoomFactor === 1) {\n        return null;\n      }\n\n      const imageWidth = this.sizedImageWidth * this.zoomFactor;\n      const imageHeight = this.sizedImageHeight * this.zoomFactor;\n      const xOffset = (imageWidth - this.imageViewportWidth) / 2;\n      const yOffset = (imageHeight - this.imageViewportHeight) / 2;\n\n      return {\n        widthRatio: this.imageViewportWidth / imageWidth,\n        heightRatio: this.imageViewportHeight / imageHeight,\n        xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,\n        yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight\n      };\n    }\n  },\n  watch: {\n    imageHistory: {\n      async handler(newHistory, oldHistory) {\n        const newSize = newHistory.length;\n        let imageIndex = newSize > 0 ? newSize - 1 : undefined;\n        if (this.focusedImageTimestamp !== undefined) {\n          const foundImageIndex = newHistory.findIndex(\n            (img) => img.time === this.focusedImageTimestamp\n          );\n          if (foundImageIndex > -1) {\n            imageIndex = foundImageIndex;\n          }\n        }\n\n        this.setFocusedImage(imageIndex);\n        this.nextImageIndex = imageIndex;\n\n        if (this.previousFocusedImage && newHistory.length) {\n          const matchIndex = this.matchIndexOfPreviousImage(this.previousFocusedImage, newHistory);\n\n          if (matchIndex > -1) {\n            this.setFocusedImage(matchIndex);\n          } else {\n            this.paused(false);\n          }\n        }\n\n        if (!this.isPaused) {\n          this.setFocusedImage(imageIndex);\n        }\n\n        await this.scrollHandler();\n        if (oldHistory?.length > 0) {\n          this.animateThumbScroll = true;\n        }\n      },\n      deep: true\n    },\n    focusedImage: {\n      handler(newImage, oldImage) {\n        const newTime = newImage?.time;\n        const oldTime = oldImage?.time;\n        const newUrl = newImage?.url;\n        const oldUrl = oldImage?.url;\n\n        // Skip if it's all falsy\n        if (!newTime && !oldTime && !newUrl && !oldUrl) {\n          return;\n        }\n\n        // Skip if it's the same image\n        if (newTime === oldTime && newUrl === oldUrl) {\n          return;\n        }\n\n        // Update image duration and reset age CSS\n        this.trackDuration();\n        this.resetAgeCSS();\n\n        // Reset image dimensions and calculate new dimensions\n        // on new image load\n        this.getImageNaturalDimensions();\n\n        // Get the related telemetry for the new image\n        this.updateRelatedTelemetryForFocusedImage();\n      }\n    },\n    bounds() {\n      this.scrollHandler();\n    },\n    isFixed(newValue) {\n      const isRealTime = !newValue;\n      // if realtime unpause which will focus on latest image\n      if (isRealTime) {\n        this.paused(false);\n      }\n    }\n  },\n  created() {\n    this.abortController = new AbortController();\n  },\n  async mounted() {\n    eventHelpers.extend(this);\n    this.focusedImageWrapper = this.$refs.focusedImageWrapper;\n    this.focusedImageElement = this.$refs.focusedImageElement;\n\n    this.focusedImageElement.addEventListener('transitionstart', this.handleZoomTransitionStart);\n    this.focusedImageElement.addEventListener('transitionend', this.handleZoomTransitionEnd);\n\n    //We only need to use this till the user focuses an image manually\n    if (this.focusedImageTimestamp !== undefined) {\n      this.isPaused = true;\n    }\n\n    this.setTimeContext();\n\n    // related telemetry keys\n    this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];\n    this.spacecraftOrientationKeys = ['heading'];\n    this.cameraKeys = ['cameraPan', 'cameraTilt'];\n    this.sunKeys = ['sunOrientation'];\n    this.transformationsKeys = ['transformations'];\n\n    // related telemetry\n    await this.initializeRelatedTelemetry();\n    await this.updateRelatedTelemetryForFocusedImage();\n    this.trackLatestRelatedTelemetry();\n\n    // for scrolling through images quickly and resizing the object view\n    this.updateRelatedTelemetryForFocusedImage = _.debounce(\n      this.updateRelatedTelemetryForFocusedImage,\n      400\n    );\n\n    // for resizing the object view\n    this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400, { leading: true });\n\n    if (this.$refs.imageBG) {\n      this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);\n      this.imageContainerResizeObserver.observe(this.$refs.imageBG);\n    }\n\n    // For adjusting scroll bar size and position when resizing thumbs wrapper\n    this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);\n    this.handleThumbWindowResizeEnded = _.debounce(\n      this.handleThumbWindowResizeEnded,\n      SCROLL_LATENCY\n    );\n    this.handleThumbWindowResizeStart = _.debounce(\n      this.handleThumbWindowResizeStart,\n      SCROLL_LATENCY\n    );\n    this.scrollToFocused = _.debounce(this.scrollToFocused, 400);\n\n    if (this.$refs.thumbsWrapper) {\n      this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);\n      this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);\n    }\n\n    this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);\n    this.loadVisibleLayers();\n    this.loadAnnotations();\n\n    this.openmct.selection.on('change', this.updateSelection);\n  },\n  beforeUnmount() {\n    this.focusedImageElement.removeEventListener('transitionstart', this.handleZoomTransitionStart);\n    this.focusedImageElement.removeEventListener('transitionend', this.handleZoomTransitionEnd);\n\n    this.abortController.abort();\n    this.persistVisibleLayers();\n    this.stopFollowingTimeContext();\n\n    if (this.thumbWrapperResizeObserver) {\n      this.thumbWrapperResizeObserver.disconnect();\n    }\n\n    if (this.imageContainerResizeObserver) {\n      this.imageContainerResizeObserver.disconnect();\n    }\n\n    if (this.relatedTelemetry.hasRelatedTelemetry) {\n      this.relatedTelemetry.destroy();\n    }\n\n    // unsubscribe from related telemetry\n    if (this.relatedTelemetry.hasRelatedTelemetry) {\n      for (let key of this.relatedTelemetry.keys) {\n        if (this.relatedTelemetry[key] && this.relatedTelemetry[key].unsubscribe) {\n          this.relatedTelemetry[key].unsubscribe();\n        }\n      }\n    }\n\n    // remove all eventListeners\n    this.stopListening();\n\n    Object.keys(this.imageryAnnotations).forEach((time) => {\n      const imageAnnotationsForTime = this.imageryAnnotations[time];\n      imageAnnotationsForTime.forEach((imageAnnotation) => {\n        this.openmct.objects.destroyMutable(imageAnnotation);\n      });\n    });\n\n    this.openmct.selection.off('change', this.updateSelection);\n  },\n  methods: {\n    calculateViewHeight() {\n      this.viewHeight = this.$el.clientHeight;\n    },\n    getVisibleLayerStyles(layer) {\n      return {\n        backgroundImage: `url(${encode_url(layer.source)})`,\n        transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${\n          this.imageTranslateY / 2\n        }px)`,\n        transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`\n      };\n    },\n    pauseAndHideImageControls() {\n      this.annotationsBeingMarqueed = true;\n      this.handlePauseButton(true);\n    },\n    revealImageControls() {\n      this.annotationsBeingMarqueed = false;\n    },\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      //listen\n      this.timeContext.on('timeSystem', this.setModeAndTrackDuration);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setModeAndTrackDuration);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setModeAndTrackDuration);\n      this.setModeAndTrackDuration();\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('timeSystem', this.setModeAndTrackDuration);\n        this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setModeAndTrackDuration);\n        this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setModeAndTrackDuration);\n      }\n    },\n    setModeAndTrackDuration() {\n      this.setIsFixed();\n      this.setCanTrackDuration();\n      this.trackDuration();\n    },\n    setIsFixed() {\n      this.isFixed = this.timeContext.isRealTime() === false;\n    },\n    setCanTrackDuration() {\n      let isRealTime = this.timeContext.isRealTime();\n      this.canTrackDuration = isRealTime && this.timeSystem.isUTCBased;\n    },\n    updateSelection(selection) {\n      const selectionType = selection?.[0]?.[0]?.context?.type;\n      const validSelectionTypes = ['annotation-search-result'];\n\n      if (!validSelectionTypes.includes(selectionType)) {\n        // wrong type of selection\n        return;\n      }\n    },\n    expand() {\n      // check for modifier keys so it doesn't interfere with the layout\n      if (this.cursorStates.modifierKeyPressed) {\n        return;\n      }\n\n      const actionCollection = this.openmct.actions.getActionsCollection(\n        this.objectPath,\n        this.currentView\n      );\n      const visibleActions = actionCollection.getVisibleActions();\n      const viewLargeAction = visibleActions?.find(\n        (action) => action.key === VIEW_LARGE_ACTION_KEY\n      );\n\n      if (viewLargeAction?.appliesTo(this.objectPath, this.currentView)) {\n        viewLargeAction.invoke(this.objectPath, this.currentView);\n      }\n    },\n    async initializeRelatedTelemetry() {\n      this.relatedTelemetry = new RelatedTelemetry(\n        this.openmct,\n        this.domainObject,\n        [\n          ...this.spacecraftPositionKeys,\n          ...this.spacecraftOrientationKeys,\n          ...this.cameraKeys,\n          ...this.sunKeys,\n          ...this.transformationsKeys\n        ],\n        this.timeContext\n      );\n\n      if (this.relatedTelemetry.hasRelatedTelemetry) {\n        await this.relatedTelemetry.load();\n      }\n    },\n    async getMostRecentRelatedTelemetry(key, targetDatum) {\n      if (!this.relatedTelemetry.hasRelatedTelemetry) {\n        throw new Error(`${this.domainObject.name} does not have any related telemetry`);\n      }\n\n      if (!this.relatedTelemetry[key]) {\n        throw new Error(`${key} does not exist on related telemetry`);\n      }\n\n      let mostRecent;\n      let valueKey = this.relatedTelemetry[key].historical.valueKey;\n      let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;\n\n      if (valuesOnTelemetry) {\n        mostRecent = targetDatum[valueKey];\n\n        if (mostRecent) {\n          return mostRecent;\n        } else {\n          console.warn(\n            `Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`\n          );\n\n          return;\n        }\n      }\n\n      mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum);\n\n      return mostRecent[valueKey];\n    },\n    handleZoomTransitionStart() {\n      this.isZooming = true;\n    },\n    handleZoomTransitionEnd() {\n      this.isZooming = false;\n    },\n    loadVisibleLayers() {\n      const layersMetadata = this.imageMetadataValue.layers;\n      if (!layersMetadata) {\n        return;\n      }\n\n      this.layers = layersMetadata;\n      if (this.domainObject.configuration) {\n        const persistedLayers = this.domainObject.configuration.layers;\n        if (!persistedLayers) {\n          this.layers.forEach((layer) => (layer.visible = false));\n          return;\n        }\n\n        layersMetadata.forEach((layer) => {\n          const persistedLayer = persistedLayers.find((object) => object.name === layer.name);\n          if (persistedLayer) {\n            layer.visible = persistedLayer.visible === true;\n          }\n        });\n        this.visibleLayers = this.layers.filter((layer) => layer.visible);\n      } else {\n        this.visibleLayers = [];\n        this.layers.forEach((layer) => {\n          layer.visible = false;\n        });\n      }\n    },\n    async loadAnnotations(existingAnnotations) {\n      if (!this.openmct.annotation.getAvailableTags().length) {\n        // don't bother loading annotations if there are no tags\n        return;\n      }\n      let foundAnnotations = existingAnnotations;\n      if (!foundAnnotations) {\n        // attempt to load\n        foundAnnotations = await this.openmct.annotation.getAnnotations(\n          this.domainObject.identifier,\n          this.abortController.signal\n        );\n      }\n      foundAnnotations.forEach((foundAnnotation) => {\n        const targetId = Object.keys(foundAnnotation.targets)[0];\n        const timeForAnnotation = foundAnnotation.targets[targetId].time;\n        if (!this.imageryAnnotations[timeForAnnotation]) {\n          this.imageryAnnotations[timeForAnnotation] = [];\n        }\n\n        const annotationExtant = this.imageryAnnotations[timeForAnnotation].some(\n          (existingAnnotation) => {\n            return this.openmct.objects.areIdsEqual(\n              existingAnnotation.identifier,\n              foundAnnotation.identifier\n            );\n          }\n        );\n        if (!annotationExtant) {\n          const annotationArray = this.imageryAnnotations[timeForAnnotation];\n          const mutableAnnotation = this.openmct.objects.toMutable(foundAnnotation);\n          annotationArray.push(mutableAnnotation);\n        }\n      });\n    },\n    persistVisibleLayers() {\n      if (\n        this.domainObject.configuration &&\n        this.openmct.objects.supportsMutation(this.domainObject.identifier)\n      ) {\n        this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);\n      }\n\n      this.visibleLayers = [];\n      this.layers = [];\n    },\n    // will subscribe to data for this key if not already done\n    subscribeToDataForKey(key) {\n      if (this.relatedTelemetry[key].isSubscribed) {\n        return;\n      }\n\n      if (this.relatedTelemetry[key].realtimeDomainObject) {\n        this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe(\n          this.relatedTelemetry[key].realtimeDomainObject,\n          (datum) => {\n            this.relatedTelemetry[key].listeners.forEach((callback) => {\n              callback(datum);\n            });\n          }\n        );\n\n        this.relatedTelemetry[key].isSubscribed = true;\n      }\n    },\n    async updateRelatedTelemetryForFocusedImage() {\n      if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) {\n        return;\n      }\n\n      // set data ON image telemetry as well as in focusedImageRelatedTelemetry\n      for (let key of this.relatedTelemetry.keys) {\n        if (\n          this.relatedTelemetry[key] &&\n          this.relatedTelemetry[key].historical &&\n          this.relatedTelemetry[key].requestLatestFor\n        ) {\n          let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;\n          let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);\n\n          if (!valuesOnTelemetry) {\n            this.imageHistory[this.focusedImageIndex][key] = value; // manually add to telemetry\n          }\n\n          this.focusedImageRelatedTelemetry[key] = value;\n        }\n      }\n\n      // set configuration for compass\n      this.transformationsKeys.forEach((key) => {\n        const transformations = this.relatedTelemetry[key];\n\n        if (transformations !== undefined) {\n          this.imageHistory[this.focusedImageIndex][key] = transformations;\n        }\n      });\n    },\n    trackLatestRelatedTelemetry() {\n      [\n        ...this.spacecraftPositionKeys,\n        ...this.spacecraftOrientationKeys,\n        ...this.cameraKeys,\n        ...this.sunKeys\n      ].forEach((key) => {\n        if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {\n          this.relatedTelemetry[key].subscribe((datum) => {\n            let valueKey = this.relatedTelemetry[key].realtime.valueKey;\n            this.latestRelatedTelemetry[key] = datum[valueKey];\n          });\n        }\n      });\n    },\n    focusElement() {\n      if (this.isComposedInLayout) {\n        return false;\n      }\n\n      this.$el.focus();\n    },\n\n    handleScroll() {\n      const thumbsWrapper = this.$refs.thumbsWrapper;\n      if (!thumbsWrapper || this.resizingWindow) {\n        return;\n      }\n\n      const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;\n      const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);\n      this.autoScroll = !disableScroll;\n    },\n    handlePauseButton(newState) {\n      this.paused(newState);\n      if (newState) {\n        // need to set the focused index or the paused focus will drift\n        this.thumbnailClicked(this.focusedImageIndex);\n      }\n    },\n    paused(state) {\n      this.isPaused = Boolean(state);\n\n      if (!this.isPaused) {\n        this.previousFocusedImage = null;\n        this.setFocusedImage(this.nextImageIndex);\n        this.autoScroll = true;\n        this.scrollHandler();\n      }\n    },\n    scrollToFocused() {\n      const thumbsWrapper = this.$refs.thumbsWrapper;\n      if (!thumbsWrapper) {\n        return;\n      }\n\n      let domThumb = thumbsWrapper.children[this.focusedImageIndex];\n      if (!domThumb) {\n        return;\n      }\n\n      // separate scrollTo function had to be implemented since scrollIntoView\n      // caused undesirable behavior in layouts\n      // and could not simply be scoped to the parent element\n      if (this.isComposedInLayout) {\n        const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;\n        this.$refs.thumbsWrapper.scrollLeft =\n          domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2;\n\n        return;\n      }\n\n      domThumb.scrollIntoView({\n        behavior: 'smooth',\n        block: 'center',\n        inline: 'center'\n      });\n    },\n    async scrollToRight() {\n      const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;\n      if (!scrollWidth) {\n        return;\n      }\n\n      await nextTick();\n      if (this.$refs.thumbsWrapper) {\n        this.$refs.thumbsWrapper.scrollLeft = scrollWidth;\n      }\n    },\n    scrollHandler() {\n      if (this.isPaused) {\n        return this.scrollToFocused();\n      } else if (this.autoScroll) {\n        return this.scrollToRight();\n      }\n    },\n    matchIndexOfPreviousImage(previous, imageHistory) {\n      // match logic uses a composite of url and time to account\n      // for example imagery not having fully unique urls\n      return imageHistory.findIndex((x) => x.url === previous.url && x.time === previous.time);\n    },\n    thumbnailClicked(index) {\n      this.setFocusedImage(index);\n      this.paused(true);\n\n      this.setPreviousFocusedImage(index);\n    },\n    setPreviousFocusedImage(index) {\n      this.$emit('update:focused-image-timestamp', undefined);\n      this.previousFocusedImage = this.imageHistory[index]\n        ? JSON.parse(JSON.stringify(this.imageHistory[index]))\n        : undefined;\n    },\n    setFocusedImage(index) {\n      if (!(Number.isInteger(index) && index > -1)) {\n        return;\n      }\n\n      this.focusedImageIndex = index;\n    },\n    trackDuration() {\n      if (this.canTrackDuration) {\n        this.stopDurationTracking();\n        this.updateDuration();\n        this.durationTracker = window.setInterval(this.updateDuration, DURATION_TRACK_MS);\n      } else {\n        this.stopDurationTracking();\n      }\n    },\n    stopDurationTracking() {\n      window.clearInterval(this.durationTracker);\n    },\n    updateDuration() {\n      let currentTime = this.timeContext.isRealTime() ? this.timeContext.now() : undefined;\n      if (currentTime === undefined) {\n        this.numericDuration = currentTime;\n      } else if (Number.isInteger(this.parsedSelectedTime)) {\n        this.numericDuration = currentTime - this.parsedSelectedTime;\n      } else {\n        this.numericDuration = undefined;\n      }\n    },\n    resetAgeCSS() {\n      this.refreshCSS = true;\n      // unable to make this work with nextTick\n      setTimeout(() => {\n        this.refreshCSS = false;\n      }, REFRESH_CSS_MS);\n    },\n    nextImage() {\n      if (this.isNextDisabled) {\n        return;\n      }\n\n      let index = this.focusedImageIndex;\n\n      this.thumbnailClicked(++index);\n      if (index === this.imageHistory.length - 1) {\n        this.paused(false);\n      }\n    },\n    prevImage() {\n      if (this.isPrevDisabled) {\n        return;\n      }\n\n      let index = this.focusedImageIndex;\n\n      if (index === this.imageHistory.length - 1) {\n        this.thumbnailClicked(this.imageHistory.length - 2);\n      } else {\n        this.thumbnailClicked(--index);\n      }\n    },\n    resetImage() {\n      this.imagePanned = false;\n      this.zoomFactor = ZOOM_SCALE_DEFAULT;\n      this.imageTranslateX = 0;\n      this.imageTranslateY = 0;\n    },\n    handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) {\n      if (!(screenClientX || screenClientY)) {\n        return this.updatePanZoom(newScaleFactor, 0, 0);\n      }\n\n      // handle mouse events\n      const imageRect = this.focusedImageWrapper.getBoundingClientRect();\n      const imageContainerX = screenClientX - imageRect.left;\n      const imageContainerY = screenClientY - imageRect.top;\n      const offsetFromCenterX = imageRect.width / 2 - imageContainerX;\n      const offsetFromCenterY = imageRect.height / 2 - imageContainerY;\n\n      this.updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY);\n    },\n    updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY) {\n      const currentScale = this.zoomFactor;\n      const previousTranslateX = this.imageTranslateX;\n      const previousTranslateY = this.imageTranslateY;\n\n      const offsetXInOriginalScale = offsetFromCenterX / currentScale;\n      const offsetYInOriginalScale = offsetFromCenterY / currentScale;\n      const translateX = offsetXInOriginalScale + previousTranslateX;\n      const translateY = offsetYInOriginalScale + previousTranslateY;\n      this.imageTranslateX = translateX;\n      this.imageTranslateY = translateY;\n      this.zoomFactor = newScaleFactor;\n    },\n    handlePanZoomClick(e) {\n      this.$refs.imageControls?.handlePanZoomClick(e);\n    },\n    arrowDownHandler(event) {\n      let key = event.keyCode;\n\n      if (this.isLeftOrRightArrowKey(key)) {\n        this.arrowDown = true;\n        window.clearTimeout(this.arrowDownDelayTimeout);\n        this.arrowDownDelayTimeout = window.setTimeout(() => {\n          this.arrowKeyScroll(this.directionByKey(key));\n        }, ARROW_DOWN_DELAY_CHECK_MS);\n      }\n    },\n    arrowUpHandler(event) {\n      let key = event.keyCode;\n\n      window.clearTimeout(this.arrowDownDelayTimeout);\n\n      if (this.isLeftOrRightArrowKey(key)) {\n        this.arrowDown = false;\n        let direction = this.directionByKey(key);\n        this[direction + 'Image']();\n      }\n    },\n    arrowKeyScroll(direction) {\n      if (this.arrowDown) {\n        this.arrowKeyScrolling = true;\n        this[direction + 'Image']();\n        setTimeout(() => {\n          this.arrowKeyScroll(direction);\n        }, ARROW_SCROLL_RATE_MS);\n      } else {\n        window.clearTimeout(this.arrowDownDelayTimeout);\n        this.arrowKeyScrolling = false;\n        this.scrollToFocused();\n      }\n    },\n    directionByKey(keyCode) {\n      let direction;\n\n      if (keyCode === ARROW_LEFT) {\n        direction = 'prev';\n      }\n\n      if (keyCode === ARROW_RIGHT) {\n        direction = 'next';\n      }\n\n      return direction;\n    },\n    isLeftOrRightArrowKey(keyCode) {\n      return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);\n    },\n    getImageNaturalDimensions() {\n      this.focusedImageNaturalAspectRatio = undefined;\n\n      const img = this.$refs.focusedImage;\n      if (!img) {\n        return;\n      }\n\n      // TODO - should probably cache this\n      img.addEventListener(\n        'load',\n        () => {\n          this.setSizedImageDimensions();\n        },\n        { once: true }\n      );\n    },\n    resizeImageContainer() {\n      if (!this.$refs.imageBG) {\n        return;\n      }\n\n      if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {\n        this.imageContainerWidth = this.$refs.imageBG.clientWidth;\n      }\n\n      if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {\n        this.imageContainerHeight = this.$refs.imageBG.clientHeight;\n      }\n\n      this.setSizedImageDimensions();\n      this.setImageViewport();\n      this.calculateViewHeight();\n      this.scrollHandler();\n    },\n    setSizedImageDimensions() {\n      if (!this.$refs.focusedImage) {\n        return;\n      }\n      this.focusedImageNaturalAspectRatio =\n        this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;\n      if (\n        this.imageContainerWidth / this.imageContainerHeight >\n        this.focusedImageNaturalAspectRatio\n      ) {\n        // container is wider than image\n        this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;\n        this.sizedImageHeight = this.imageContainerHeight;\n      } else {\n        // container is taller than image\n        this.sizedImageWidth = this.imageContainerWidth;\n        this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;\n      }\n    },\n    setImageViewport() {\n      if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {\n        // container is taller than wrapper\n        this.imageViewportWidth = this.sizedImageWidth;\n        this.imageViewportHeight = this.sizedImageHeight;\n      } else {\n        // container is wider than wrapper\n        this.imageViewportWidth = this.imageContainerWidth;\n        this.imageViewportHeight = this.imageContainerHeight;\n      }\n    },\n    handleThumbWindowResizeStart() {\n      if (!this.autoScroll) {\n        return;\n      }\n\n      // To hide resume button while scrolling\n      this.resizingWindow = true;\n      this.handleThumbWindowResizeEnded();\n    },\n    handleThumbWindowResizeEnded() {\n      this.scrollHandler();\n\n      this.calculateViewHeight();\n\n      this.$nextTick(() => {\n        this.resizingWindow = false;\n      });\n    },\n    clearWheelZoom() {\n      this.$refs.imageControls.clearWheelZoom();\n    },\n    wheelZoom(e) {\n      e.preventDefault();\n      this.$refs.imageControls.wheelZoom(e);\n    },\n    startPan(e) {\n      e.preventDefault();\n      if (!this.pan && this.zoomFactor > 1) {\n        this.animateZoom = false;\n        this.imagePanned = true;\n        this.pan = {\n          x: e.clientX,\n          y: e.clientY\n        };\n        this.listenTo(window, 'mouseup', this.onMouseUp, this);\n        this.listenTo(window, 'mousemove', this.trackMousePosition, this);\n      }\n\n      return false;\n    },\n    trackMousePosition(e) {\n      if (!e.altKey) {\n        return this.onMouseUp(e);\n      }\n\n      this.updatePan(e);\n      e.preventDefault();\n    },\n    updatePan(e) {\n      if (!this.pan) {\n        return;\n      }\n\n      const dX = e.clientX - this.pan.x;\n      const dY = e.clientY - this.pan.y;\n      this.pan = {\n        x: e.clientX,\n        y: e.clientY\n      };\n      this.updatePanZoom(this.zoomFactor, dX, dY);\n    },\n    endPan() {\n      this.pan = null;\n      this.animateZoom = true;\n    },\n    onMouseUp(event) {\n      this.stopListening(window, 'mouseup', this.onMouseUp, this);\n      this.stopListening(window, 'mousemove', this.trackMousePosition, this);\n\n      if (this.pan) {\n        return this.endPan(event);\n      }\n    },\n    setFilters(filtersObj) {\n      this.filters = filtersObj;\n    },\n    setCursorStates(states) {\n      this.cursorStates = states;\n    },\n    toggleLayerVisibility(index) {\n      let isVisible = this.layers[index].visible === true;\n      this.layers[index].visible = !isVisible;\n      this.visibleLayers = this.layers.filter((layer) => layer.visible);\n    },\n    isSelected(index) {\n      return this.focusedImageIndex === index && this.isPaused;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/ImageryViewMenuSwitcher.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-switcher-menu\">\n    <button\n      :id=\"id\"\n      class=\"c-button c-button--menu c-switcher-menu__button\"\n      :class=\"iconClass\"\n      :aria-label=\"title\"\n      :title=\"title\"\n      @click=\"toggleMenu\"\n    />\n    <div v-show=\"showMenu\" class=\"c-switcher-menu__content\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { v4 as uuid } from 'uuid';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    iconClass: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    title: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  data() {\n    return {\n      id: uuid(),\n      showMenu: false\n    };\n  },\n  mounted() {\n    document.addEventListener('click', this.hideMenu);\n  },\n  unmounted() {\n    document.removeEventListener('click', this.hideMenu);\n  },\n  methods: {\n    toggleMenu() {\n      this.showMenu = !this.showMenu;\n    },\n    hideMenu(e) {\n      if (this.id === e.target.id) {\n        return;\n      }\n\n      this.showMenu = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/LayerSettings.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls\"\n    @click=\"handleClose\"\n  >\n    <div class=\"c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn\">\n      <ul @click=\"$event.stopPropagation()\">\n        <li v-for=\"(layer, index) in layers\" :key=\"index\">\n          <label>\n            <input\n              :checked=\"layer.visible\"\n              type=\"checkbox\"\n              @change=\"toggleLayerVisibility(index)\"\n            />\n            {{ layer.name }}\n          </label>\n        </li>\n      </ul>\n    </div>\n\n    <button class=\"c-click-icon icon-x t-btn-close c-switcher-menu__close-button\"></button>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    layers: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['toggle-layer-visibility'],\n  methods: {\n    handleClose(e) {\n      const closeButton = e.target.classList.contains('c-switcher-menu__close-button');\n      if (!closeButton) {\n        e.stopPropagation();\n      }\n    },\n    toggleLayerVisibility(index) {\n      this.$emit('toggle-layer-visibility', index);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction copyRelatedMetadata(metadata) {\n  let compare = metadata.comparisonFunction;\n  let copiedMetadata = JSON.parse(JSON.stringify(metadata));\n  copiedMetadata.comparisonFunction = compare;\n\n  return copiedMetadata;\n}\n\nimport IndependentTimeContext from '@/api/time/IndependentTimeContext';\nexport default class RelatedTelemetry {\n  constructor(openmct, domainObject, telemetryKeys, timeContext) {\n    this._openmct = openmct;\n    this._domainObject = domainObject;\n    this.timeContext = timeContext;\n\n    let metadata = this._openmct.telemetry.getMetadata(this._domainObject);\n    let imageHints = metadata.valuesForHints(['image'])[0];\n\n    this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;\n\n    if (this.hasRelatedTelemetry) {\n      this.keys = telemetryKeys;\n\n      this._timeFormatter = undefined;\n      this._timeSystemChange(this.timeContext.getTimeSystem());\n\n      // grab related telemetry metadata\n      for (let key of this.keys) {\n        if (imageHints.relatedTelemetry[key]) {\n          this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);\n        }\n      }\n\n      this.load = this.load.bind(this);\n      this._parseTime = this._parseTime.bind(this);\n      this._timeSystemChange = this._timeSystemChange.bind(this);\n      this.destroy = this.destroy.bind(this);\n\n      this.timeContext.on('timeSystem', this._timeSystemChange);\n    }\n  }\n\n  async load() {\n    if (!this.hasRelatedTelemetry) {\n      throw new Error(\n        'This domain object does not have related telemetry, use \"hasRelatedTelemetry\" to check before loading.'\n      );\n    }\n\n    await Promise.all(\n      this.keys.map(async (key) => {\n        if (this[key]) {\n          if (this[key].historical) {\n            await this._initializeHistorical(key);\n          }\n\n          if (\n            this[key].realtime &&\n            this[key].realtime.telemetryObjectId &&\n            this[key].realtime.telemetryObjectId !== ''\n          ) {\n            await this._initializeRealtime(key);\n          }\n        }\n      })\n    );\n  }\n\n  async _initializeHistorical(key) {\n    if (!this[key].historical.telemetryObjectId) {\n      this[key].historical.hasTelemetryOnDatum = true;\n    } else if (this[key].historical.telemetryObjectId !== '') {\n      this[key].historicalDomainObject = await this._openmct.objects.get(\n        this[key].historical.telemetryObjectId\n      );\n\n      this[key].requestLatestFor = async (datum) => {\n        // We need to create a throwaway time context and pass it along\n        // as a request option. We do this to \"trick\" the Time API\n        // into thinking we are in fixed time mode in order to bypass this logic:\n        // https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59\n        // Context: https://github.com/akhenry/openmct-yamcs/pull/217\n        const ephemeralContext = new IndependentTimeContext(this._openmct, this._openmct.time, [\n          this[key].historicalDomainObject\n        ]);\n\n        // Stop following the global context, stop the clock,\n        // and set bounds.\n        ephemeralContext.resetContext();\n        const newBounds = {\n          start: this.timeContext.getBounds().start,\n          end: this._parseTime(datum)\n        };\n        ephemeralContext.setBounds(newBounds);\n\n        const options = {\n          start: newBounds.start,\n          end: newBounds.end,\n          timeContext: ephemeralContext,\n          strategy: 'latest'\n        };\n        let results = await this._openmct.telemetry.request(\n          this[key].historicalDomainObject,\n          options\n        );\n\n        return results[results.length - 1];\n      };\n    }\n  }\n\n  async _initializeRealtime(key) {\n    this[key].realtimeDomainObject = await this._openmct.objects.get(\n      this[key].realtime.telemetryObjectId\n    );\n    this[key].listeners = [];\n    this[key].subscribe = (callback) => {\n      if (!this[key].isSubscribed) {\n        this._subscribeToDataForKey(key);\n      }\n\n      if (!this[key].listeners.includes(callback)) {\n        this[key].listeners.push(callback);\n\n        return () => {\n          this[key].listeners.remove(callback);\n        };\n      } else {\n        return () => {};\n      }\n    };\n  }\n\n  _subscribeToDataForKey(key) {\n    if (this[key].isSubscribed) {\n      return;\n    }\n\n    if (this[key].realtimeDomainObject) {\n      this[key].unsubscribe = this._openmct.telemetry.subscribe(\n        this[key].realtimeDomainObject,\n        (datum) => {\n          this[key].listeners.forEach((callback) => {\n            callback(datum);\n          });\n        }\n      );\n\n      this[key].isSubscribed = true;\n    }\n  }\n\n  _parseTime(datum) {\n    return this._timeFormatter.parse(datum);\n  }\n\n  _timeSystemChange(system) {\n    let key = system.key;\n    let metadata = this._openmct.telemetry.getMetadata(this._domainObject);\n    let metadataValue = metadata.value(key) || { format: key };\n    this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);\n  }\n\n  destroy() {\n    this.timeContext.off('timeSystem', this._timeSystemChange);\n    for (let key of this.keys) {\n      if (this[key] && this[key].unsubscribe) {\n        this[key].unsubscribe();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/imagery/components/ZoomSettings.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-image-controls__controls-wrapper\" @click=\"handleClose\">\n    <div class=\"c-image-controls__control c-image-controls__zoom\">\n      <div class=\"c-button-set c-button-set--strip-h\">\n        <button\n          class=\"c-button t-btn-zoom-out icon-minus\"\n          title=\"Zoom out\"\n          aria-label=\"Zoom out\"\n          @click=\"zoomOut\"\n        ></button>\n\n        <button\n          class=\"c-button t-btn-zoom-in icon-plus\"\n          title=\"Zoom in\"\n          aria-label=\"Zoom in\"\n          @click=\"zoomIn\"\n        ></button>\n\n        <button\n          class=\"c-button t-btn-zoom-lock\"\n          title=\"Lock current zoom and pan across all images\"\n          aria-label=\"Lock current zoom and pan across all images\"\n          :class=\"{ 'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked }\"\n          @click=\"toggleZoomLock\"\n        ></button>\n\n        <button\n          class=\"c-button icon-reset t-btn-zoom-reset\"\n          title=\"Remove zoom and pan\"\n          aria-label=\"Remove zoom and pan\"\n          @click=\"resetImage\"\n        ></button>\n      </div>\n      <div class=\"c-image-controls__zoom-factor\">x{{ formattedZoomFactor }}</div>\n    </div>\n    <button\n      v-if=\"isMenu\"\n      class=\"c-click-icon icon-x t-btn-close c-switcher-menu__close-button\"\n      aria-label=\"Close menu\"\n    ></button>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['zoomIn', 'zoomOut', 'toggleZoomLock', 'resetImage'],\n  props: {\n    zoomFactor: {\n      type: Number,\n      required: true\n    },\n    panZoomLocked: {\n      type: Boolean,\n      required: true\n    },\n    isMenu: {\n      type: Boolean,\n      required: false\n    }\n  },\n  computed: {\n    formattedZoomFactor() {\n      return Number.parseFloat(this.zoomFactor).toPrecision(2);\n    }\n  },\n  methods: {\n    handleClose(e) {\n      const closeButton = e.target.classList.contains('c-switcher-menu__close-button');\n      if (!closeButton) {\n        e.stopPropagation();\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/imagery/components/imagery-view.scss",
    "content": "@use 'sass:math';\n\n@keyframes fade-out {\n  from {\n    background-color: rgba($colorOk, 0.5);\n  }\n  to {\n    background-color: rgba($colorOk, 0);\n    color: inherit;\n  }\n}\n\n.c-imagery {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  overflow: hidden;\n\n  &:focus {\n    outline: none;\n  }\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__main-image-wrapper {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n\n    &.unsynced {\n      @include sUnsynced();\n    }\n\n    &.cursor-zoom-in {\n      cursor: zoom-in;\n    }\n\n    &.cursor-zoom-out {\n      cursor: zoom-out;\n    }\n\n    &.pannable {\n      @include cursorGrab();\n    }\n  }\n\n  .image-wrapper {\n    overflow: visible clip;\n    background-image: repeating-linear-gradient(\n      45deg,\n      transparent,\n      transparent 4px,\n      rgba(125, 125, 125, 0.2) 4px,\n      rgba(125, 125, 125, 0.2) 8px\n    );\n  }\n\n  .image-wrapper {\n    overflow: visible clip;\n    background-image: repeating-linear-gradient(\n      45deg,\n      transparent,\n      transparent 4px,\n      rgba(125, 125, 125, 0.2) 4px,\n      rgba(125, 125, 125, 0.2) 8px\n    );\n  }\n\n  &__main-image {\n    &__bg {\n      background-color: $colorPlotBg;\n      border: 1px solid transparent;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      flex: 1 1 auto;\n      height: 0;\n      overflow: hidden;\n    }\n    &__background-image {\n      // Actually does the image display\n      background-position: center;\n      background-repeat: no-repeat;\n      background-size: contain;\n      height: 100%; //fallback value\n    }\n    &__image {\n      // Present to allow Save As... image\n      position: absolute;\n      height: 100%;\n      width: 100%;\n      opacity: 0;\n    }\n\n    &__image-save-proxy {\n      height: 100%;\n      width: 100%;\n      z-index: 10;\n    }\n  }\n\n  &__hints {\n    $m: $interiorMargin;\n    background: rgba(black, 0.2);\n    border-radius: $smallCr;\n    padding: 2px $interiorMargin;\n    pointer-events: none;\n    position: absolute;\n    right: $m;\n    top: $m;\n    opacity: 0.9;\n    z-index: 2;\n  }\n\n  &__control-bar,\n  &__time {\n    display: flex;\n    align-items: baseline;\n\n    > * + * {\n      margin-left: $interiorMarginSm;\n    }\n  }\n\n  &__control-bar {\n    margin-top: 2px;\n    padding: $interiorMarginSm 0;\n    justify-content: space-between;\n  }\n\n  &__time {\n    flex: 0 1 auto;\n    overflow: hidden;\n  }\n\n  &__timestamp,\n  &__age {\n    @include ellipsize();\n    flex: 0 1 auto;\n  }\n\n  &__timestamp {\n    flex-shrink: 10;\n  }\n\n  &__age {\n    border-radius: $smallCr;\n    display: flex;\n    flex-shrink: 0;\n    align-items: center;\n    padding: 2px $interiorMarginSm;\n\n    &:before {\n      font-size: 0.9em;\n      opacity: 0.5;\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &--new {\n    // New imagery\n    $bgColor: $colorOk;\n    color: $colorOkFg;\n    background-color: rgba($bgColor, 0.5);\n    animation-name: fade-out;\n    animation-timing-function: ease-in;\n    animation-iteration-count: 1;\n    animation-fill-mode: forwards;\n    &.no-animation {\n      animation: none;\n    }\n  }\n\n  &__layer-image {\n    pointer-events: none;\n    z-index: 1;\n  }\n\n  &__thumbs-wrapper {\n    display: flex; // Uses row layout\n    justify-content: flex-end;\n\n    &.is-autoscroll-off {\n      background: $colorInteriorBorder;\n      [class*='__auto-scroll-resume-button'] {\n        display: block;\n      }\n    }\n\n    &.is-paused {\n      background: rgba($colorPausedBg, 0.4);\n    }\n  }\n\n  &__thumbs-scroll-area {\n    flex: 0 1 auto;\n    display: flex;\n    flex-direction: row;\n    height: 145px;\n    overflow-x: auto;\n    overflow-y: hidden;\n    margin-bottom: 1px;\n    padding-bottom: $interiorMarginSm;\n    &.animate-scroll {\n      scroll-behavior: smooth;\n    }\n  }\n\n  &__auto-scroll-resume-button {\n    display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off\n    flex: 0 0 auto;\n    font-size: 0.8em;\n    margin: $interiorMarginSm;\n  }\n\n  .c-control-menu {\n    // Controls on left of flex column layout, close btn on right\n    @include menuOuter();\n\n    border-radius: $controlCr;\n    display: flex;\n    align-items: flex-start;\n    flex-direction: row;\n    justify-content: space-between;\n    padding: $interiorMargin;\n    width: max-content;\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  .c-switcher-menu {\n    display: contents;\n\n    &__content {\n      // Menu panel\n      top: 28px;\n      position: absolute;\n\n      .c-so-view & {\n        top: 25px;\n      }\n    }\n  }\n}\n\n.--width-less-than-220 .--show-if-less-than-220.c-switcher-menu {\n  display: contents !important;\n}\n\n.s-image-layer {\n  position: absolute;\n  height: 100%;\n  width: 100%;\n  opacity: 1;\n  background-size: contain;\n  background-repeat: no-repeat;\n  background-position: center;\n}\n\n/*************************************** THUMBS */\n.c-thumb {\n  $w: $imageThumbsD;\n  display: flex;\n  flex-direction: column;\n  padding: 4px;\n  min-width: $w;\n  width: $w;\n\n  &.active {\n    background: $colorSelectedBg;\n    color: $colorSelectedFg;\n  }\n  &:hover {\n    background: $colorThumbHoverBg;\n  }\n  &.selected {\n    // fixed time - selected bg will match active bg color\n    background: $colorSelectedBg;\n    color: $colorSelectedFg;\n    &.real-time {\n      // real time - bg orange when selected\n      background: $colorPausedBg !important;\n      color: $colorPausedFg !important;\n    }\n  }\n\n  &__image {\n    background-color: rgba($colorBodyFg, 0.2);\n    width: 100%;\n  }\n\n  &__annotation-indicator {\n    color: $colorClickIconButton;\n    position: absolute;\n    top: 6px;\n    right: 8px;\n  }\n\n  &__timestamp {\n    flex: 0 0 auto;\n    padding: 2px 3px;\n  }\n\n  &__viewable-area {\n    position: absolute;\n    border: 2px yellow solid;\n    left: 0;\n    top: 0;\n  }\n}\n\n.is-small-thumbs {\n  .c-imagery__thumbs-scroll-area {\n    height: 60px; // Allow room for scrollbar\n  }\n\n  .c-thumb {\n    $w: math.div($imageThumbsD, 2);\n    min-width: $w;\n    width: $w;\n\n    &__timestamp {\n      display: none;\n    }\n  }\n}\n\n/*************************************** IMAGERY LOCAL CONTROLS*/\n.c-imagery {\n  .h-local-controls--overlay-content {\n    display: flex;\n    flex-direction: row;\n    position: absolute;\n    left: $interiorMargin;\n    top: $interiorMargin;\n    z-index: 10;\n    background: $colorLocalControlOvrBg;\n    border-radius: $basicCr;\n    align-items: center;\n    padding: $interiorMargin $interiorMargin;\n\n    .s-status-taking-snapshot & {\n      display: none;\n    }\n  }\n  [class*='--menus-aligned'] {\n    > * + * {\n      button {\n        margin-left: $interiorMarginSm;\n      }\n    }\n  }\n}\n\n.c-image-controls {\n  &__controls-wrapper {\n    // Wraps __controls and __close-btn\n    display: flex;\n  }\n\n  &__controls {\n    display: flex;\n    align-items: stretch;\n\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n\n    [class*='c-button'] {\n      flex: 0 0 auto;\n    }\n  }\n\n  &__control,\n  &__input {\n    display: flex;\n    align-items: center;\n\n    &:before {\n      color: rgba($colorMenuFg, 0.5);\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &__zoom {\n    > * + * {\n      margin-left: $interiorMargin;\n    } // Is this used?\n  }\n\n  &--filters {\n    // Styles specific to the brightness and contrast controls\n    .c-image-controls {\n      &__controls {\n        width: 80px; // About the minimum this element can be; cannot size based on % due to markup structure\n      }\n\n      &__sliders {\n        display: flex;\n        flex: 1 1 auto;\n        flex-direction: column;\n        width: 100%;\n\n        > * + * {\n          margin-top: 11px;\n        }\n\n        input[type='range'] {\n          display: block;\n          width: 100%;\n        }\n      }\n\n      &__slider-wrapper {\n        display: flex;\n        align-items: center;\n\n        &:before {\n          margin-right: $interiorMargin;\n        }\n      }\n\n      &__reset-btn {\n        // Span that holds bracket graphics and button\n        $bc: $scrollbarTrackColorBg;\n        flex: 0 0 auto;\n\n        &:before,\n        &:after {\n          border-right: 1px solid $bc;\n          content: '';\n          display: block;\n          width: 5px;\n          height: 4px;\n        }\n\n        &:before {\n          border-top: 1px solid $bc;\n          margin-bottom: 2px;\n        }\n\n        &:after {\n          border-bottom: 1px solid $bc;\n          margin-top: 2px;\n        }\n\n        .c-icon-link {\n          color: $colorBtnFg;\n        }\n      }\n    }\n  }\n}\n\n/*************************************** BUTTONS */\n.c-button.pause-play {\n  // Pause icon set by default in markup\n  justify-self: end;\n\n  &.is-paused {\n    background: $colorPausedBg !important;\n    color: $colorPausedFg;\n\n    &:before {\n      content: $glyph-icon-play;\n    }\n  }\n\n  .s-status-taking-snapshot & {\n    display: none;\n  }\n}\n\n.c-imagery__prev-next-button {\n  pointer-events: all;\n  position: absolute;\n  top: 50%;\n  transform: translateY(-75%); // 75% due to transform: rotation approach to the button\n\n  &.c-nav {\n    position: absolute;\n\n    &--prev {\n      left: 0;\n    }\n    &--next {\n      right: 0;\n    }\n  }\n\n  .s-status-taking-snapshot & {\n    display: none;\n  }\n}\n\n.c-nav {\n  @include cArrowButtonBase($colorBg: rgba($colorLocalControlOvrBg, 0.1), $colorFg: $colorBtnBg);\n  @include cArrowButtonSizing($dimOuter: 48px);\n  border-radius: $controlCr;\n\n  .--width-less-than-600 & {\n    @include cArrowButtonSizing($dimOuter: 32px);\n  }\n}\n\n/*************************************** IMAGERY IN TIMESTRIP VIEWS */\n.c-imagery-tsv {\n  $m: $interiorMargin;\n  @include abs();\n  // We need overflow: hidden this because an image thumb can extend to the right past the time frame edge\n  overflow: hidden;\n\n  &-container {\n    background: $colorPlotBg;\n    //box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size\n    position: absolute;\n    top: $m; right: 0; bottom: $m; left: 0;\n  }\n\n  .c-imagery-tsv__image-wrapper {\n    $m: $interiorMarginSm;\n    cursor: pointer;\n    position: absolute;\n    top: $m; bottom: $m;\n    display: flex;\n    z-index: 1;\n\n    img {\n      align-self: flex-end;\n    }\n\n    &:hover {\n      z-index: 2;\n\n      .c-imagery-tsv {\n        &__image-handle {\n          box-shadow: rgba($colorEventLine, 0.5) 0 0 0px 4px;\n          transition: none;\n        }\n\n        &__image-placeholder img {\n          filter: none;\n        }\n      }\n\n      img {\n        // img can be `display: none` when there's not enough space between tick lines\n        display: block !important;\n      }\n    }\n  }\n\n  &__image-placeholder {\n    background-color: deeppink; //pushBack($colorBodyBg, 0.3);\n    $m: $interiorMargin;\n    display: block;\n    position: absolute;\n    top: $m; right: auto; bottom: $m; left: 0;\n\n    img {\n      filter: brightness(0.8);\n      height: 100%;\n    }\n  }\n\n\n  &__image-handle {\n    $lineW: $eventLineW;\n    $hitAreaW: 7px;\n    background-color: $colorEventLine;\n    transition: box-shadow 250ms ease-out;\n    top: 0; bottom: 0;\n    width: $lineW;\n    z-index: 3;\n\n    &:before {\n      // Extend hit area\n      content: '';\n      display: block;\n      position: absolute;\n      top: 0; bottom: 0;\n      z-index: 0;\n      width: $hitAreaW;\n      transform: translateX(($hitAreaW - $lineW) * -0.5);\n    }\n  }\n\n  &__no-items {\n    fill: $colorBodyFg !important;\n  }\n}\n\n// DON'T THINK THIS IS BEING USED\n.c-image-canvas {\n  pointer-events: auto; // This allows the image element to receive a browser-level context click\n  position: absolute;\n  left: 0;\n  top: 0;\n  z-index: 2;\n}\n"
  },
  {
    "path": "src/plugins/imagery/lib/eventHelpers.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @type {EventHelpers}\n */\nconst helperFunctions = {\n  listenTo: function (object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    const listener = {\n      object: object,\n      event: event,\n      callback: callback,\n      context: context,\n      _cb: context ? callback.bind(context) : callback\n    };\n    if (object.$watch && event.indexOf('change:') === 0) {\n      const scopePath = event.replace('change:', '');\n      listener.unlisten = object.$watch(scopePath, listener._cb, true);\n    } else if (object.$on) {\n      listener.unlisten = object.$on(event, listener._cb);\n    } else if (object.addEventListener) {\n      object.addEventListener(event, listener._cb);\n    } else {\n      object.on(event, listener._cb, listener.context);\n    }\n\n    this._listeningTo.push(listener);\n  },\n\n  stopListening: function (object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    this._listeningTo\n      .filter(function (listener) {\n        if (object && object !== listener.object) {\n          return false;\n        }\n\n        if (event && event !== listener.event) {\n          return false;\n        }\n\n        if (callback && callback !== listener.callback) {\n          return false;\n        }\n\n        if (context && context !== listener.context) {\n          return false;\n        }\n\n        return true;\n      })\n      .map(function (listener) {\n        if (listener.unlisten) {\n          listener.unlisten();\n        } else if (listener.object.removeEventListener) {\n          listener.object.removeEventListener(listener.event, listener._cb);\n        } else {\n          listener.object.off(listener.event, listener._cb, listener.context);\n        }\n\n        return listener;\n      })\n      .forEach(function (listener) {\n        this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);\n      }, this);\n  },\n\n  extend: function (object) {\n    object.listenTo = helperFunctions.listenTo;\n    object.stopListening = helperFunctions.stopListening;\n  }\n};\n\nexport default helperFunctions;\n/**\n * @typedef {Object} EventHelpers\n * @property {(object: any, event: string, callback: Function, context?: any) => void} listenTo\n * @property {(object: any, event?: string, callback?: Function, context?: any) => void} stopListening\n */\n"
  },
  {
    "path": "src/plugins/imagery/mixins/imageryData.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst DEFAULT_DURATION_FORMATTER = 'duration';\nconst IMAGE_HINT_KEY = 'image';\nconst IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';\nconst IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';\nimport { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';\n\nexport default {\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  mounted() {\n    // listen\n    this.boundsChanged = this.boundsChanged.bind(this);\n    this.timeSystemChanged = this.timeSystemChanged.bind(this);\n    this.setDataTimeContext = this.setDataTimeContext.bind(this);\n    this.openmct.objectViews.on('clearData', this.dataCleared);\n\n    // Get metadata and formatters\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);\n\n    this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] };\n    this.imageFormatter = this.getFormatter(this.imageMetadataValue.key);\n\n    this.imageThumbnailMetadataValue = {\n      ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0]\n    };\n    this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key\n      ? this.getFormatter(this.imageThumbnailMetadataValue.key)\n      : null;\n\n    this.durationFormatter = this.getFormatter(\n      this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER\n    );\n    this.imageDownloadNameMetadataValue = {\n      ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]\n    };\n\n    // initialize\n    this.timeKey = this.timeSystem.key;\n    this.timeFormatter = this.getFormatter(this.timeKey);\n    this.setDataTimeContext();\n    this.loadTelemetry();\n  },\n  beforeUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n      delete this.unsubscribe;\n    }\n\n    this.stopFollowingDataTimeContext();\n    this.openmct.objectViews.off('clearData', this.dataCleared);\n\n    this.telemetryCollection.off('add', this.dataAdded);\n    this.telemetryCollection.off('remove', this.dataRemoved);\n    this.telemetryCollection.off('clear', this.dataCleared);\n\n    this.telemetryCollection.destroy();\n  },\n  methods: {\n    dataAdded(addedItems, addedItemIndices) {\n      const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum));\n      let newImageHistory = this.imageHistory.slice();\n      normalizedDataToAdd.forEach((datum, index) => {\n        newImageHistory.splice(addedItemIndices[index] ?? -1, 0, datum);\n      });\n      //Assign just once so imageHistory watchers don't get called too often\n      this.imageHistory = newImageHistory;\n    },\n    dataCleared() {\n      this.imageHistory = [];\n    },\n    dataRemoved(removed) {\n      const removedTimestamps = {};\n      removed.forEach((_removed) => {\n        const removedTimestamp = this.parseTime(_removed);\n        removedTimestamps[removedTimestamp] = true;\n      });\n\n      this.imageHistory = this.imageHistory.filter((image) => {\n        const imageTimestamp = this.parseTime(image);\n\n        return !removedTimestamps[imageTimestamp];\n      });\n    },\n    setDataTimeContext() {\n      this.stopFollowingDataTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);\n    },\n    stopFollowingDataTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);\n        this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);\n      }\n    },\n    formatImageUrl(datum) {\n      if (!datum) {\n        return;\n      }\n\n      return this.imageFormatter.format(datum);\n    },\n    formatImageThumbnailUrl(datum) {\n      if (!datum || !this.imageThumbnailFormatter) {\n        return;\n      }\n\n      return this.imageThumbnailFormatter.format(datum);\n    },\n    formatTime(datum) {\n      if (!datum) {\n        return;\n      }\n\n      const dateTimeStr = this.timeFormatter.format(datum);\n\n      // Replace ISO \"T\" with a space to allow wrapping\n      return dateTimeStr.replace('T', ' ');\n    },\n    getImageDownloadName(datum) {\n      let imageDownloadName = '';\n      if (datum) {\n        const key = this.imageDownloadNameMetadataValue.key;\n        imageDownloadName = datum[key];\n      }\n\n      return imageDownloadName;\n    },\n    parseTime(datum) {\n      if (!datum) {\n        return;\n      }\n\n      return this.timeFormatter.parse(datum);\n    },\n    loadTelemetry() {\n      this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {\n        timeContext: this.timeContext\n      });\n      this.telemetryCollection.on('add', this.dataAdded);\n      this.telemetryCollection.on('remove', this.dataRemoved);\n      this.telemetryCollection.on('clear', this.dataCleared);\n      this.telemetryCollection.load();\n    },\n    boundsChanged(bounds, isTick) {\n      if (isTick) {\n        return;\n      }\n\n      this.bounds = bounds; // setting bounds for ImageryView watcher\n    },\n    timeSystemChanged() {\n      this.timeSystem = this.timeContext.getTimeSystem();\n      this.timeKey = this.timeSystem.key;\n      this.timeFormatter = this.getFormatter(this.timeKey);\n      this.durationFormatter = this.getFormatter(\n        this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER\n      );\n    },\n    normalizeDatum(datum) {\n      const formattedTime = this.formatTime(datum);\n      const url = this.formatImageUrl(datum);\n      const thumbnailUrl = this.formatImageThumbnailUrl(datum);\n      const time = this.parseTime(formattedTime);\n      const imageDownloadName = this.getImageDownloadName(datum);\n\n      return {\n        ...datum,\n        formattedTime,\n        url,\n        thumbnailUrl,\n        time,\n        imageDownloadName\n      };\n    },\n    getFormatter(key) {\n      const metadataValue = this.metadata.value(key) || { format: key };\n      const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n      return valueFormatter;\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/imagery/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport OpenImageInNewTabAction from './actions/OpenImageInNewTabAction.js';\nimport SaveImageAsAction from './actions/SaveImageAsAction.js';\nimport ImageryTimestripViewProvider from './ImageryTimestripViewProvider.js';\nimport ImageryViewProvider from './ImageryViewProvider.js';\n\nexport default function (options) {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options));\n    openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));\n    openmct.actions.register(new OpenImageInNewTabAction(openmct));\n    openmct.actions.register(new SaveImageAsAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/imagery/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport {\n  createMouseEvent,\n  createOpenMct,\n  resetApplicationState,\n  simulateKeyEvent\n} from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { PREVIEW_ACTION_KEY } from '../../ui/preview/PreviewAction.js';\n\nconst ONE_MINUTE = 1000 * 60;\nconst TEN_MINUTES = ONE_MINUTE * 10;\nconst MAIN_IMAGE_CLASS = '.js-imageryView-image';\nconst NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';\nconst REFRESH_CSS_MS = 500;\n\nfunction formatThumbnail(url) {\n  return url.replace('logo-openmct.svg', 'logo-nasa.svg');\n}\n\nfunction getImageInfo(doc) {\n  let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];\n  let timestamp = imageElement.dataset.openmctImageTimestamp;\n  let identifier = imageElement.dataset.openmctObjectKeystring;\n  let url = imageElement.src;\n\n  return {\n    timestamp,\n    identifier,\n    url\n  };\n}\n\nfunction isNew(doc) {\n  let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS);\n\n  return newIcon.length !== 0;\n}\n\nfunction generateTelemetry(start, count) {\n  let telemetry = [];\n  for (let i = 1, l = count + 1; i < l; i++) {\n    let stringRep = i + 'minute';\n    let logo = 'images/logo-openmct.svg';\n\n    telemetry.push({\n      name: stringRep + ' Imagery',\n      utc: start + i * ONE_MINUTE,\n      url: location.host + '/' + logo + '?time=' + stringRep,\n      timeId: stringRep,\n      value: 100\n    });\n  }\n\n  return telemetry;\n}\n\ndescribe('The Imagery View Layouts', () => {\n  const imageryKey = 'example.imagery';\n  const imageryForTimeStripKey = 'example.imagery.time-strip.view';\n  const START = Date.now();\n  const COUNT = 10;\n\n  let originalRouterPath;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let previewAction;\n\n  let openmct;\n  let parent;\n  let child;\n  let historicalProvider;\n  let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);\n  let imageryObject = {\n    identifier: {\n      namespace: '',\n      key: 'imageryId'\n    },\n    name: 'Example Imagery',\n    type: 'example.imagery',\n    location: 'parentId',\n    modified: 0,\n    persisted: 0,\n    configuration: {\n      layers: [\n        {\n          name: '16:9',\n          visible: true\n        }\n      ]\n    },\n    telemetry: {\n      values: [\n        {\n          name: 'Image',\n          key: 'url',\n          format: 'image',\n          layers: [\n            {\n              source: location.host + '/images/bg-splash.jpg',\n              name: '16:9'\n            }\n          ],\n          hints: {\n            image: 1,\n            priority: 3\n          },\n          source: 'url'\n        },\n        {\n          name: 'Image Thumbnail',\n          key: 'thumbnail-url',\n          format: 'thumbnail',\n          hints: {\n            thumbnail: 1,\n            priority: 3\n          },\n          source: 'url'\n        },\n        {\n          name: 'Name',\n          key: 'name',\n          source: 'name',\n          hints: {\n            priority: 0\n          }\n        },\n        {\n          name: 'Time',\n          key: 'utc',\n          format: 'utc',\n          hints: {\n            domain: 2,\n            priority: 1\n          },\n          source: 'utc'\n        },\n        {\n          name: 'Local Time',\n          key: 'local',\n          format: 'local-format',\n          hints: {\n            domain: 1,\n            priority: 2\n          },\n          source: 'local'\n        }\n      ]\n    }\n  };\n\n  // this setups up the app\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    historicalProvider = {\n      request: () => {\n        return Promise.resolve(imageTelemetry);\n      }\n    };\n    spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(imageTelemetry);\n\n      return telemetryPromise;\n    });\n\n    previewAction = openmct.actions.getAction(PREVIEW_ACTION_KEY);\n\n    parent = document.createElement('div');\n    parent.style.width = '640px';\n    parent.style.height = '480px';\n\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n\n    parent.appendChild(child);\n    document.body.appendChild(parent);\n\n    spyOn(window, 'ResizeObserver').and.returnValue({\n      observe() {},\n      disconnect() {}\n    });\n\n    spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject));\n\n    originalRouterPath = openmct.router.path;\n\n    openmct.telemetry.addFormat({\n      key: 'thumbnail',\n      format: formatThumbnail\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    openmct.router.path = originalRouterPath;\n    document.body.removeChild(parent);\n\n    await resetApplicationState(openmct);\n  });\n\n  it('should provide an imagery time strip view when in a time strip', () => {\n    openmct.router.path = [\n      {\n        identifier: {\n          key: 'test-timestrip',\n          namespace: ''\n        },\n        type: 'time-strip'\n      }\n    ];\n\n    let applicableViews = openmct.objectViews.get(imageryObject, [\n      imageryObject,\n      {\n        identifier: {\n          key: 'test-timestrip',\n          namespace: ''\n        },\n        type: 'time-strip'\n      }\n    ]);\n    let imageryView = applicableViews.find(\n      (viewProvider) => viewProvider.key === imageryForTimeStripKey\n    );\n\n    expect(imageryView).toBeDefined();\n  });\n\n  it('should provide an imagery view only for imagery producing objects', () => {\n    let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);\n    let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey);\n\n    expect(imageryView).toBeDefined();\n  });\n\n  it('should not provide an imagery view when in a time strip', () => {\n    openmct.router.path = [\n      {\n        identifier: {\n          key: 'test-timestrip',\n          namespace: ''\n        },\n        type: 'time-strip'\n      }\n    ];\n\n    let applicableViews = openmct.objectViews.get(imageryObject, [\n      imageryObject,\n      {\n        identifier: {\n          key: 'test-timestrip',\n          namespace: ''\n        },\n        type: 'time-strip'\n      }\n    ]);\n    let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey);\n\n    expect(imageryView).toBeUndefined();\n  });\n\n  it('should provide an imagery view when navigated to in the composition of a time strip', () => {\n    openmct.router.path = [imageryObject];\n\n    let applicableViews = openmct.objectViews.get(imageryObject, [\n      imageryObject,\n      {\n        identifier: {\n          key: 'test-timestrip',\n          namespace: ''\n        },\n        type: 'time-strip'\n      }\n    ]);\n    let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey);\n\n    expect(imageryView).toBeDefined();\n  });\n\n  describe('imagery view', () => {\n    let applicableViews;\n    let imageryViewProvider;\n    let imageryView;\n\n    beforeEach(() => {\n      openmct.time.timeSystem('utc', {\n        start: START - 5 * ONE_MINUTE,\n        end: START + 5 * ONE_MINUTE\n      });\n\n      applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);\n      imageryViewProvider = applicableViews.find((viewProvider) => viewProvider.key === imageryKey);\n      imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);\n      imageryView.show(child);\n\n      imageryView._getInstance().$refs.ImageryContainer.forceShowThumbnails = true;\n\n      return nextTick();\n    });\n\n    it('on mount should show the the most recent image', async () => {\n      //Looks like we need nextTick here so that computed properties settle down\n      await nextTick();\n      await nextTick();\n      await nextTick();\n      const imageInfo = getImageInfo(parent);\n      expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);\n    });\n\n    it('on mount should show any image layers', async () => {\n      //Looks like we need nextTick here so that computed properties settle down\n      await nextTick();\n      await nextTick();\n      const layerEls = parent.querySelectorAll('.js-layer-image');\n      expect(layerEls.length).toEqual(1);\n    });\n\n    it('should show the clicked thumbnail as the main image', async () => {\n      //Looks like we need nextTick here so that computed properties settle down\n      await nextTick();\n      await nextTick();\n      const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);\n      parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();\n      await nextTick();\n      const imageInfo = getImageInfo(parent);\n\n      expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);\n    });\n\n    xit('should show that an image is new', (done) => {\n      openmct.time.clock('local', {\n        start: -1000,\n        end: 1000\n      });\n\n      nextTick(() => {\n        // used in code, need to wait to the 500ms here too\n        setTimeout(() => {\n          const imageIsNew = isNew(parent);\n          expect(imageIsNew).toBeTrue();\n          done();\n        }, REFRESH_CSS_MS);\n      });\n    });\n\n    it('should show that an image is not new', async () => {\n      await nextTick();\n      await nextTick();\n      const target = formatThumbnail(imageTelemetry[4].url);\n      parent.querySelectorAll(`img[src='${target}']`)[0].click();\n\n      await nextTick();\n      const imageIsNew = isNew(parent);\n\n      expect(imageIsNew).toBeFalse();\n    });\n\n    it('should navigate via arrow keys', async () => {\n      await nextTick();\n      await nextTick();\n      const keyOpts = {\n        element: parent.querySelector('.c-imagery'),\n        key: 'ArrowLeft',\n        keyCode: 37,\n        type: 'keyup'\n      };\n\n      simulateKeyEvent(keyOpts);\n\n      await nextTick();\n      const imageInfo = getImageInfo(parent);\n      expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);\n    });\n\n    it('should navigate via numerous arrow keys', async () => {\n      await nextTick();\n      await nextTick();\n      const element = parent.querySelector('.c-imagery');\n      const type = 'keyup';\n      const leftKeyOpts = {\n        element,\n        type,\n        key: 'ArrowLeft',\n        keyCode: 37\n      };\n      const rightKeyOpts = {\n        element,\n        type,\n        key: 'ArrowRight',\n        keyCode: 39\n      };\n\n      // left thrice\n      simulateKeyEvent(leftKeyOpts);\n      simulateKeyEvent(leftKeyOpts);\n      simulateKeyEvent(leftKeyOpts);\n      // right once\n      simulateKeyEvent(rightKeyOpts);\n\n      await nextTick();\n      const imageInfo = getImageInfo(parent);\n      expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);\n    });\n    it('shows an auto scroll button when scroll to left', (done) => {\n      nextTick(() => {\n        // to mock what a scroll would do\n        imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;\n        nextTick(() => {\n          let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');\n          expect(autoScrollButton).toBeTruthy();\n          done();\n        });\n      });\n    });\n    it('scrollToRight is called when clicking on auto scroll button', async () => {\n      await nextTick();\n      // use spyon to spy the scroll function\n      spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');\n      imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;\n      await nextTick();\n      parent.querySelector('.c-imagery__auto-scroll-resume-button').click();\n      expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);\n    });\n    xit('should change the image zoom factor when using the zoom buttons', async () => {\n      await nextTick();\n      let imageSizeBefore;\n      let imageSizeAfter;\n\n      // test clicking the zoom in button\n      imageSizeBefore = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      parent.querySelector('.t-btn-zoom-in').click();\n      await nextTick();\n      imageSizeAfter = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      expect(imageSizeAfter.height).toBeGreaterThan(imageSizeBefore.height);\n      expect(imageSizeAfter.width).toBeGreaterThan(imageSizeBefore.width);\n      // test clicking the zoom out button\n      imageSizeBefore = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      parent.querySelector('.t-btn-zoom-out').click();\n      await nextTick();\n      imageSizeAfter = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);\n      expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);\n    });\n    xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {\n      await nextTick();\n      // test clicking the zoom reset button\n      // zoom in to scale up the image dimensions\n      parent.querySelector('.t-btn-zoom-in').click();\n      await nextTick();\n      let imageSizeBefore = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      await nextTick();\n      parent.querySelector('.t-btn-zoom-reset').click();\n      let imageSizeAfter = parent\n        .querySelector('.c-imagery_main-image_background-image')\n        .getBoundingClientRect();\n      expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);\n      expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);\n      done();\n    });\n\n    it('should display the viewable area when zoom factor is greater than 1', async () => {\n      await nextTick();\n      await nextTick();\n      expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);\n\n      parent.querySelector('.t-btn-zoom-in').click();\n      await nextTick();\n      expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);\n\n      parent.querySelector('.t-btn-zoom-reset').click();\n      await nextTick();\n      expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);\n    });\n\n    it('should reset the brightness and contrast when clicking the reset button', async () => {\n      const viewInstance = imageryView._getInstance();\n      await nextTick();\n\n      // Save the original brightness and contrast values\n      const origBrightness = viewInstance.$refs.ImageryContainer.filters.brightness;\n      const origContrast = viewInstance.$refs.ImageryContainer.filters.contrast;\n\n      // Change them to something else (default: 100)\n      viewInstance.$refs.ImageryContainer.setFilters({\n        brightness: 200,\n        contrast: 200\n      });\n      await nextTick();\n\n      // Verify that the values actually changed\n      expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(200);\n      expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(200);\n\n      // Click the reset button\n      parent.querySelector('.t-btn-reset').click();\n      await nextTick();\n\n      // Verify that the values were reset\n      expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(origBrightness);\n      expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(origContrast);\n    });\n  });\n\n  describe('imagery time strip view', () => {\n    let applicableViews;\n    let imageryTimestripViewProvider;\n    let imageryTimeView;\n    let componentView;\n\n    beforeEach(() => {\n      openmct.time.timeSystem('utc', {\n        start: START - 5 * ONE_MINUTE,\n        end: START + 5 * ONE_MINUTE\n      });\n\n      const mockClock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n      mockClock.key = 'mockClock';\n      mockClock.currentValue.and.returnValue(1);\n\n      openmct.time.addClock(mockClock);\n      openmct.time.clock('mockClock', {\n        start: START - 5 * ONE_MINUTE,\n        end: START + 5 * ONE_MINUTE\n      });\n\n      openmct.router.path = [\n        {\n          identifier: {\n            key: 'test-timestrip',\n            namespace: ''\n          },\n          type: 'time-strip'\n        }\n      ];\n\n      applicableViews = openmct.objectViews.get(imageryObject, [\n        imageryObject,\n        {\n          identifier: {\n            key: 'test-timestrip',\n            namespace: ''\n          },\n          type: 'time-strip'\n        }\n      ]);\n      imageryTimestripViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === imageryForTimeStripKey\n      );\n      imageryTimeView = imageryTimestripViewProvider.view(imageryObject, [\n        imageryObject,\n        {\n          identifier: {\n            key: 'test-timestrip',\n            namespace: ''\n          },\n          type: 'time-strip'\n        }\n      ]);\n      imageryTimeView.show(child);\n\n      componentView = imageryTimeView.getComponent().$refs.root;\n      spyOn(previewAction, 'invoke').and.callThrough();\n\n      return nextTick();\n    });\n\n    afterEach(() => {\n      openmct.time.setClock('local');\n    });\n\n    it('on mount should show imagery within the given bounds', async () => {\n      await nextTick();\n      await nextTick();\n      const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');\n      expect(imageElements.length).toEqual(5);\n    });\n\n    it('should show the clicked thumbnail as the preview image', async () => {\n      await nextTick();\n      await nextTick();\n      const mouseDownEvent = createMouseEvent('mousedown');\n      let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);\n      imageWrapper[2].dispatchEvent(mouseDownEvent);\n      await nextTick();\n      const timestamp = imageWrapper[2].id.replace('wrapper-', '');\n      // Make sure the function was called\n      expect(previewAction.invoke).toHaveBeenCalled();\n\n      // Get the arguments of the first call\n      const firstArg = previewAction.invoke.calls.mostRecent().args[0];\n      const secondArg = previewAction.invoke.calls.mostRecent().args[1];\n\n      // Compare the first argument\n      expect(firstArg).toEqual([componentView.objectPath[0]]);\n\n      // Compare the \"timestamp\" property of the second argument\n      expect(secondArg.timestamp).toEqual(Number(timestamp));\n\n      // Compare the \"objectPath\" property of the second argument\n      expect(secondArg.objectPath).toEqual(componentView.objectPath);\n    });\n\n    it('should remove images when clock advances', async () => {\n      openmct.time.tick(ONE_MINUTE * 2);\n      await nextTick();\n      await nextTick();\n      const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');\n      expect(imageElements.length).toEqual(4);\n    });\n\n    it('should remove images when start bounds shorten', async () => {\n      openmct.time.timeSystem('utc', {\n        start: START,\n        end: START + 5 * ONE_MINUTE\n      });\n      await nextTick();\n      await nextTick();\n      const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');\n      expect(imageElements.length).toEqual(1);\n    });\n\n    it('should remove images when end bounds shorten', async () => {\n      openmct.time.timeSystem('utc', {\n        start: START - 5 * ONE_MINUTE,\n        end: START - 2 * ONE_MINUTE\n      });\n      await nextTick();\n      await nextTick();\n      const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');\n      expect(imageElements.length).toEqual(4);\n    });\n\n    it('should remove images when both bounds shorten', async () => {\n      openmct.time.timeSystem('utc', {\n        start: START - 2 * ONE_MINUTE,\n        end: START + 2 * ONE_MINUTE\n      });\n      await nextTick();\n      await nextTick();\n      const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');\n      expect(imageElements.length).toEqual(3);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/importFromJSONAction/ImportFromJSONAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { parseKeyString } from 'objectUtils';\nimport { filter__proto__ } from 'utils/sanitization';\nimport { v4 as uuid } from 'uuid';\n\nconst IMPORT_FROM_JSON_ACTION_KEY = 'import.JSON';\n\nclass ImportFromJSONAction {\n  constructor(openmct) {\n    this.name = 'Import from JSON';\n    this.key = IMPORT_FROM_JSON_ACTION_KEY;\n    this.description = '';\n    this.cssClass = 'icon-import';\n    this.group = 'import';\n    this.priority = 2;\n\n    this.openmct = openmct;\n  }\n\n  // Public\n  /**\n   *\n   * @param {Object} objectPath\n   * @returns {boolean}\n   */\n  appliesTo(objectPath) {\n    const domainObject = objectPath[0];\n    const locked = domainObject && domainObject.locked;\n    const persistable = this.openmct.objects.isPersistable(domainObject.identifier);\n    const TypeDefinition = this.openmct.types.get(domainObject.type);\n    const definition = TypeDefinition.definition;\n    const creatable = definition && definition.creatable;\n\n    if (locked || !persistable || !creatable) {\n      return false;\n    }\n\n    return domainObject !== undefined && this.openmct.composition.get(domainObject);\n  }\n  /**\n   *\n   * @param {Object} objectPath\n   */\n  invoke(objectPath) {\n    this._showForm(objectPath[0]);\n  }\n  /**\n   *\n   * @param {Object} object\n   * @param {Object} changes\n   */\n\n  onSave(object, changes) {\n    const selectFile = changes.selectFile;\n    const jsonTree = selectFile.body;\n    const objectTree = JSON.parse(jsonTree, filter__proto__);\n\n    this._importObjectTree(object, objectTree);\n  }\n\n  /**\n   * @private\n   * @param {Object} parent\n   * @param {Object} tree\n   * @param {Object} seen\n   * @param {Array} objectsToCreate tracks objects from import json that will need to be created\n   */\n  _deepInstantiate(parent, tree, seen, objectsToCreate) {\n    const objectIdentifiers = this._getObjectReferenceIds(parent);\n\n    if (objectIdentifiers.length) {\n      const parentId = this.openmct.objects.makeKeyString(parent.identifier);\n      seen.push(parentId);\n\n      for (const childId of objectIdentifiers) {\n        const keystring = this.openmct.objects.makeKeyString(childId);\n        if (!tree[keystring] || seen.includes(keystring)) {\n          continue;\n        }\n\n        const newModel = tree[keystring];\n        delete newModel.persisted;\n\n        objectsToCreate.push(newModel);\n\n        // make sure there weren't any errors saving\n        if (newModel) {\n          this._deepInstantiate(newModel, tree, seen, objectsToCreate);\n        }\n      }\n    }\n  }\n\n  /**\n   * @private\n   * @param {Object} parent\n   * @returns [identifiers]\n   */\n  _getObjectReferenceIds(parent) {\n    let objectIdentifiers = [];\n    let itemObjectReferences = [];\n    const objectStyles = parent?.configuration?.objectStyles;\n    const parentComposition = this.openmct.composition.get(parent);\n\n    if (parentComposition) {\n      objectIdentifiers = Array.from(parent.composition);\n    }\n\n    //conditional object styles are not saved on the composition, so we need to check for them\n    if (objectStyles) {\n      const parentObjectReference = objectStyles.conditionSetIdentifier;\n\n      if (parentObjectReference) {\n        objectIdentifiers.push(parentObjectReference);\n      }\n\n      function hasConditionSetIdentifier(item) {\n        return Boolean(item.conditionSetIdentifier);\n      }\n\n      itemObjectReferences = Object.values(objectStyles)\n        .filter(hasConditionSetIdentifier)\n        .map((item) => item.conditionSetIdentifier);\n    }\n\n    return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences]));\n  }\n\n  /**\n   * Generates a map of old IDs to new IDs for efficient lookup during tree walking.\n   * This function considers cases where original namespaces are blank and updates those IDs as well.\n   *\n   * @param {Object} tree - The object tree containing the old IDs.\n   * @param {string} newNamespace - The namespace for the new IDs.\n   * @returns {Object} A map of old IDs to new IDs.\n   */\n  _generateIdMap(tree, newNamespace) {\n    const idMap = {};\n    const keys = Object.keys(tree.openmct);\n\n    for (const oldIdKey of keys) {\n      const oldId = parseKeyString(oldIdKey);\n      const newId = {\n        namespace: newNamespace,\n        key: uuid()\n      };\n      const newIdKeyString = this.openmct.objects.makeKeyString(newId);\n\n      // Update the map with the old and new ID key strings.\n      idMap[oldIdKey] = newIdKeyString;\n\n      // If the old namespace is blank, also map the non-namespaced ID.\n      if (!oldId.namespace) {\n        const nonNamespacedOldIdKey = oldId.key;\n        idMap[nonNamespacedOldIdKey] = newIdKeyString;\n      }\n    }\n\n    return idMap;\n  }\n\n  /**\n   * Walks through the object tree and updates IDs according to the provided ID map.\n   * @param {Object} obj - The current object being visited in the tree.\n   * @param {Object} idMap - A map of old IDs to new IDs for rewriting.\n   * @param {Object} importDialog - Optional progress dialog for import.\n   * @returns {Promise<Object>} The object with updated IDs.\n   */\n  async _walkAndRewriteIds(obj, idMap, importDialog) {\n    // How many rewrites to do before yielding to the event loop\n    const UI_UPDATE_INTERVAL = 300;\n    // The percentage of the progress dialog to allocate to rewriting IDs\n    const PERCENT_OF_DIALOG = 80;\n    if (obj === null || obj === undefined) {\n      return obj;\n    }\n\n    if (typeof obj === 'string') {\n      const possibleId = idMap[obj];\n      if (possibleId) {\n        return possibleId;\n      } else {\n        return obj;\n      }\n    }\n\n    if (Object.hasOwn(obj, 'key') && Object.hasOwn(obj, 'namespace')) {\n      const oldId = this.openmct.objects.makeKeyString(obj);\n      const possibleId = idMap[oldId];\n\n      if (possibleId) {\n        const newIdParts = possibleId.split(':');\n        if (newIdParts.length >= 2) {\n          // new ID is namespaced, so update both the namespace and key\n          obj.namespace = newIdParts[0];\n          obj.key = newIdParts[1];\n        } else {\n          // old ID was not namespaced, so update the key only\n          obj.namespace = '';\n          obj.key = newIdParts[0];\n        }\n      }\n      return obj;\n    }\n\n    if (Array.isArray(obj)) {\n      for (let i = 0; i < obj.length; i++) {\n        obj[i] = await this._walkAndRewriteIds(obj[i], idMap); // Process each item in the array\n      }\n      return obj;\n    }\n\n    if (typeof obj === 'object') {\n      const newObj = {};\n\n      const keys = Object.keys(obj);\n      let processedCount = 0;\n      for (const key of keys) {\n        const value = obj[key];\n        const possibleId = idMap[key];\n        const newKey = possibleId || key;\n\n        newObj[newKey] = await this._walkAndRewriteIds(value, idMap);\n\n        // Optionally update the importDialog here, after each property has been processed\n        if (importDialog) {\n          processedCount++;\n          if (processedCount % UI_UPDATE_INTERVAL === 0) {\n            // yield to the event loop to allow the UI to update\n            await new Promise((resolve) => setTimeout(resolve, 0));\n            const percentPersisted = Math.ceil(PERCENT_OF_DIALOG * (processedCount / keys.length));\n            const message = `Rewriting ${processedCount} of ${keys.length} imported objects.`;\n            importDialog.updateProgress(percentPersisted, message);\n          }\n        }\n      }\n\n      return newObj;\n    }\n\n    // Return the input as-is for types that are not objects, strings, or arrays\n    return obj;\n  }\n\n  /**\n   * @private\n   * @param {Object} tree\n   * @returns {Promise<Object>}\n   */\n  async _generateNewIdentifiers(tree, newNamespace, importDialog) {\n    const idMap = this._generateIdMap(tree, newNamespace);\n    tree.rootId = idMap[tree.rootId];\n    tree.openmct = await this._walkAndRewriteIds(tree.openmct, idMap, importDialog);\n    return tree;\n  }\n  /**\n   * @private\n   * @param {Object} domainObject\n   * @param {Object} objTree\n   */\n  async _importObjectTree(domainObject, objTree) {\n    // make rewriting objects IDs 80% of the progress bar\n    const importDialog = this.openmct.overlays.progressDialog({\n      progressPerc: 0,\n      message: `Importing ${Object.keys(objTree.openmct).length} objects`,\n      iconClass: 'info',\n      title: 'Importing'\n    });\n    const objectsToCreate = [];\n    const namespace = domainObject.identifier.namespace;\n    const tree = await this._generateNewIdentifiers(objTree, namespace, importDialog);\n    const rootId = tree.rootId;\n\n    const rootObj = tree.openmct[rootId];\n    delete rootObj.persisted;\n    objectsToCreate.push(rootObj);\n    if (this.openmct.composition.checkPolicy(domainObject, rootObj)) {\n      this._deepInstantiate(rootObj, tree.openmct, [], objectsToCreate);\n\n      try {\n        let persistedObjects = 0;\n        // make saving objects objects 20% of the progress bar\n        await Promise.all(\n          objectsToCreate.map(async (objectToCreate) => {\n            persistedObjects++;\n            const percentPersisted =\n              Math.ceil(20 * (persistedObjects / objectsToCreate.length)) + 80;\n            const message = `Saving ${persistedObjects} of ${objectsToCreate.length} imported objects.`;\n            importDialog.updateProgress(percentPersisted, message);\n            await this._instantiate(objectToCreate);\n          })\n        );\n      } catch (error) {\n        this.openmct.notifications.error('Error saving objects');\n\n        throw error;\n      } finally {\n        importDialog.dismiss();\n      }\n\n      const compositionCollection = this.openmct.composition.get(domainObject);\n      let domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString);\n      compositionCollection.add(rootObj);\n    } else {\n      importDialog.dismiss();\n      const cannotImportDialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: \"We're sorry, but you cannot import that object type into this object.\",\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: function () {\n              cannotImportDialog.dismiss();\n            }\n          }\n        ]\n      });\n    }\n  }\n  /**\n   * @private\n   * @param {Object} model\n   * @returns {Object}\n   */\n  _instantiate(model) {\n    return this.openmct.objects.save(model);\n  }\n\n  /**\n   * @private\n   * @param {Object} domainObject\n   */\n  _showForm(domainObject) {\n    const formStructure = {\n      title: this.name,\n      sections: [\n        {\n          rows: [\n            {\n              name: 'Select File',\n              key: 'selectFile',\n              control: 'file-input',\n              required: true,\n              text: 'Select File...',\n              validate: this._validateJSON,\n              type: 'application/json'\n            }\n          ]\n        }\n      ]\n    };\n\n    this.openmct.forms.showForm(formStructure).then((changes) => {\n      let onSave = this.onSave.bind(this);\n      onSave(domainObject, changes);\n    });\n  }\n  /**\n   * @private\n   * @param {Object} data\n   * @returns {boolean}\n   */\n  _validateJSON(data) {\n    const value = data.value;\n    const objectTree = value && value.body;\n    let json;\n    let success = true;\n    try {\n      json = JSON.parse(objectTree);\n    } catch (e) {\n      success = false;\n    }\n\n    if (success && (!json.openmct || !json.rootId)) {\n      success = false;\n    }\n\n    if (!success) {\n      this.openmct.notifications.error(\n        'Invalid File: The selected file was either invalid JSON or was not formatted properly for import into Open MCT.'\n      );\n    }\n\n    return success;\n  }\n}\n\nexport { IMPORT_FROM_JSON_ACTION_KEY };\n\nexport default ImportFromJSONAction;\n"
  },
  {
    "path": "src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nlet openmct;\nlet importFromJSONAction;\nlet folderObject;\nlet unObserve;\n\ndescribe('The import JSON action', function () {\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    importFromJSONAction = openmct.actions.getAction('import.JSON');\n    folderObject = {\n      composition: [],\n      name: 'Unnamed Folder',\n      type: 'folder',\n      location: '9f6c9dae-51c3-401d-92f1-c812de942922',\n      modified: 1637021471624,\n      persisted: 1637021471624,\n      id: '84438cda-a071-48d1-b9bf-d77bd53e59ba',\n      identifier: {\n        namespace: '',\n        key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'\n      }\n    };\n  });\n\n  afterEach(() => {\n    importFromJSONAction = undefined;\n    folderObject = undefined;\n    unObserve?.();\n    unObserve = undefined;\n\n    return resetApplicationState(openmct);\n  });\n\n  it('has import as JSON action', () => {\n    expect(importFromJSONAction).toBeDefined();\n  });\n\n  it('applies to return true for objects with composition', function () {\n    const objectPath = [folderObject];\n\n    spyOn(openmct.composition, 'get').and.returnValue(true);\n\n    expect(importFromJSONAction.appliesTo(objectPath)).toBe(true);\n  });\n\n  it('applies to return false for objects without composition', function () {\n    const domainObject = {\n      telemetry: {\n        period: 10,\n        amplitude: 1,\n        offset: 0,\n        dataRateInHz: 1,\n        phase: 0,\n        randomness: 0\n      },\n      name: 'Unnamed Sine Wave Generator',\n      type: 'generator',\n      location: '84438cda-a071-48d1-b9bf-d77bd53e59ba',\n      modified: 1637021471172,\n      identifier: {\n        namespace: '',\n        key: 'c102b6e1-3c81-4618-926a-56cc310925f6'\n      },\n      persisted: 1637021471172\n    };\n\n    const objectPath = [domainObject];\n\n    spyOn(openmct.types, 'get').and.returnValue({});\n    spyOn(openmct.composition, 'get').and.returnValue(false);\n\n    expect(importFromJSONAction.appliesTo(objectPath)).toBe(false);\n  });\n\n  it('calls showForm on invoke ', function () {\n    const objectPath = [folderObject];\n\n    spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({}));\n    spyOn(importFromJSONAction, 'onSave').and.returnValue(Promise.resolve({}));\n    importFromJSONAction.invoke(objectPath);\n\n    expect(openmct.forms.showForm).toHaveBeenCalled();\n  });\n\n  it('protects against prototype pollution', (done) => {\n    spyOn(openmct.forms, 'showForm').and.callFake(returnResponseWithPrototypePollution);\n\n    unObserve = openmct.objects.observe(folderObject, '*', callback);\n\n    importFromJSONAction.invoke([folderObject]);\n\n    function callback(newObject) {\n      const hasPollutedProto =\n        Object.prototype.hasOwnProperty.call(newObject, '__proto__') ||\n        Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(newObject), 'toString');\n\n      expect(hasPollutedProto).toBeFalse();\n\n      done();\n    }\n\n    function returnResponseWithPrototypePollution() {\n      const pollutedResponse = {\n        selectFile: {\n          name: 'imported object',\n\n          body: '{\"openmct\":{\"c28d230d-e909-4a3e-9840-d9ef469dda70\":{\"identifier\":{\"key\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[],\"configuration\":{\"series\":[]},\"modified\":1695837546833,\"location\":\"mine\",\"created\":1695837546833,\"persisted\":1695837546833,\"__proto__\":{\"toString\":\"foobar\"}}},\"rootId\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\"}'\n        }\n      };\n\n      return Promise.resolve(pollutedResponse);\n    }\n  });\n  it('preserves the integrity of the namespace and key during import', async () => {\n    const incomingObject = {\n      openmct: {\n        '7323f02a-06ac-438d-bd58-6d6e33b8741e': {\n          name: 'Some Folder',\n          type: 'folder',\n          composition: [\n            {\n              key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6',\n              namespace: ''\n            }\n          ],\n          modified: 1710843256162,\n          location: 'mine',\n          created: 1710843243471,\n          persisted: 1710843256162,\n          identifier: {\n            namespace: '',\n            key: '7323f02a-06ac-438d-bd58-6d6e33b8741e'\n          }\n        },\n        '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6': {\n          name: 'Some Clock',\n          type: 'clock',\n          configuration: {\n            baseFormat: 'YYYY/MM/DD hh:mm:ss',\n            use24: 'clock12',\n            timezone: 'UTC'\n          },\n          modified: 1710843256152,\n          location: '7323f02a-06ac-438d-bd58-6d6e33b8741e',\n          created: 1710843256152,\n          persisted: 1710843256152,\n          identifier: {\n            namespace: '',\n            key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6'\n          }\n        }\n      },\n      rootId: '7323f02a-06ac-438d-bd58-6d6e33b8741e'\n    };\n\n    const targetDomainObject = {\n      identifier: {\n        namespace: 'starJones',\n        key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'\n      },\n      type: 'folder'\n    };\n    spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));\n    spyOn(openmct.overlays, 'progressDialog').and.callFake(() => {\n      return {\n        updateProgress: () => {},\n        dismiss: () => {}\n      };\n    });\n    try {\n      await importFromJSONAction.onSave(targetDomainObject, {\n        selectFile: { body: JSON.stringify(incomingObject) }\n      });\n\n      for (const callArgs of openmct.objects.save.calls.allArgs()) {\n        const savedObject = callArgs[0]; // Assuming the first argument is the object being saved.\n        expect(savedObject.identifier.key.includes(':')).toBeFalse(); // Ensure no colon in the key.\n        expect(savedObject.identifier.namespace).toBe(targetDomainObject.identifier.namespace);\n      }\n    } catch (error) {\n      fail(error);\n    }\n  });\n});\n"
  },
  {
    "path": "src/plugins/importFromJSONAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ImportFromJSONAction from './ImportFromJSONAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new ImportFromJSONAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/DataVisualization.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-data-visualization-inspect-properties c-inspector__data-pivot c-data-visualization-inspector__flex-column\"\n  >\n    <div class=\"c-inspect-properties\">\n      <div class=\"c-inspect-properties__header\">Data Visualization</div>\n    </div>\n\n    <div v-if=\"isLoading\" class=\"c-inspector__data-pivot-placeholder\">Loading...</div>\n\n    <div v-else-if=\"hasDataRanges\">\n      <div\n        v-if=\"selectedDataRange !== undefined && hasDescription\"\n        class=\"c-inspector__data-pivot-coordinates-wrapper\"\n      >\n        <span\n          class=\"c-tree__item__type-icon c-object-label__type-icon\"\n          :class=\"description.icon\"\n        ></span>\n        <span class=\"c-inspector__data-pivot-coordinates\">\n          {{ description.text }}\n        </span>\n      </div>\n\n      <select v-model=\"selectedDataRangeIndex\" class=\"c-inspector__data-pivot-range-selector\">\n        <option\n          v-for=\"(dataRange, index) in descendingDataRanges\"\n          :key=\"index\"\n          :value=\"index\"\n          :selected=\"selectedDataRangeIndex === index\"\n        >\n          {{ displayDataRange(dataRange) }}\n        </option>\n      </select>\n    </div>\n\n    <div\n      v-else-if=\"dataRanges && dataRanges.length === 0\"\n      class=\"c-inspector__data-pivot-placeholder\"\n    >\n      No data for the current {{ description.name }}\n    </div>\n\n    <div v-else-if=\"hasPlaceholderText\" class=\"c-inspector__data-pivot-placeholder\">\n      {{ placeholderText }}\n    </div>\n\n    <template v-if=\"selectedBounds !== undefined\">\n      <NumericDataInspectorView\n        :bounds=\"selectedBounds\"\n        :telemetry-keys=\"plotTelemetryKeys\"\n        :no-numeric-data-text=\"noNumericDataText\"\n      />\n      <ImageryInspectorView\n        v-if=\"hasImagery\"\n        :bounds=\"selectedBounds\"\n        :telemetry-keys=\"imageryTelemetryKeys\"\n      />\n    </template>\n  </div>\n</template>\n<script>\nimport ImageryInspectorView from './ImageryInspectorView.vue';\nimport NumericDataInspectorView from './NumericDataInspectorView.vue';\n\nconst TIMESTAMP_VIEW_BUFFER = 30 * 1000;\nconst timestampBufferText = `${TIMESTAMP_VIEW_BUFFER / 1000} seconds`;\n\nexport default {\n  components: {\n    NumericDataInspectorView,\n    ImageryInspectorView\n  },\n  inject: ['timeFormatter', 'placeholderText', 'plotOptions', 'imageryOptions'],\n  props: {\n    description: {\n      type: Object,\n      default: () => {}\n    },\n    dataRanges: {\n      type: Array,\n      default: () => undefined\n    },\n    plotTelemetryKeys: {\n      type: Array,\n      default: () => []\n    },\n    isLoading: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      selectedDataRangeIndex: 0\n    };\n  },\n  computed: {\n    hasPlaceholderText() {\n      return this.placeholderText.length > 0;\n    },\n    descendingDataRanges() {\n      return this.dataRanges?.slice().reverse();\n    },\n    hasDescription() {\n      return this.description?.text?.length > 0;\n    },\n    hasDataRanges() {\n      return this.dataRanges?.length > 0;\n    },\n    selectedDataRange() {\n      if (!this.hasDataRanges || this.selectedDataRangeIndex === undefined) {\n        return;\n      }\n\n      return this.descendingDataRanges[this.selectedDataRangeIndex];\n    },\n    selectedBounds() {\n      if (this.selectedDataRange === undefined) {\n        return;\n      }\n\n      const { start, end } = this.selectedDataRange.bounds;\n\n      if (start === end) {\n        return {\n          start: start - TIMESTAMP_VIEW_BUFFER,\n          end: end + TIMESTAMP_VIEW_BUFFER\n        };\n      }\n\n      return this.selectedDataRange.bounds;\n    },\n    imageryTelemetryKeys() {\n      return this.imageryOptions?.telemetryKeys;\n    },\n    hasImagery() {\n      return this.imageryTelemetryKeys?.length;\n    },\n    noNumericDataText() {\n      return this.plotOptions?.noNumericDataText;\n    }\n  },\n  methods: {\n    shortDate(date) {\n      return date.slice(0, date.indexOf('.')).replace('T', ' ');\n    },\n    displayDataRange(dataRange) {\n      const startTime = dataRange.bounds.start;\n      const endTime = dataRange.bounds.end;\n      if (startTime === endTime) {\n        return `${this.shortDate(this.timeFormatter.format(startTime))} +/- ${timestampBufferText}`;\n      }\n      return `${this.shortDate(this.timeFormatter.format(startTime))} - ${this.shortDate(\n        this.timeFormatter.format(endTime)\n      )}`;\n    },\n    isSelectedDataRange(dataRange, index) {\n      const selectedDataRange = this.descendingDataRanges[index];\n\n      return (\n        dataRange.bounds.start === selectedDataRange.bounds.start &&\n        dataRange.bounds.end === selectedDataRange.bounds.end\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/ImageryInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    v-if=\"camerasWithImagesInBounds.length > 0\"\n    class=\"c-inspect-properties c-inspector__imagery-view\"\n  >\n    <div class=\"c-inspect-properties__header\">Imagery View</div>\n    <div\n      v-for=\"(camera, index) in camerasWithImagesInBounds\"\n      :key=\"index\"\n      class=\"c-imagery-view__camera-image-set\"\n    >\n      <TelemetryFrame :bounds=\"bounds\" :telemetry-object=\"camera\">\n        <div class=\"c-imagery-view__camera-image-list\">\n          <span\n            v-for=\"(cameraImage, imageIndex) in camera.imagesInBounds\"\n            :key=\"imageIndex\"\n            class=\"c-imagery-view__camera-image\"\n          >\n            <img :src=\"cameraImage.value\" />\n            <span class=\"c-imagery-view__camera-image-timestamp\">\n              {{ cameraImage.timestamp }}\n            </span>\n          </span>\n        </div>\n      </TelemetryFrame>\n    </div>\n  </div>\n</template>\n\n<script>\nimport TelemetryFrame from './TelemetryFrame.vue';\n\nexport default {\n  components: {\n    TelemetryFrame\n  },\n  inject: ['openmct'],\n  props: {\n    bounds: {\n      type: Object,\n      default: () => {}\n    },\n    telemetryKeys: {\n      type: Array,\n      required: true\n    }\n  },\n  data() {\n    return {\n      camerasWithImagesInBounds: []\n    };\n  },\n  watch: {\n    bounds() {\n      this.getCameraImagesInBounds();\n    }\n  },\n  mounted() {\n    this.getCameraImagesInBounds();\n  },\n  methods: {\n    async getCameraImagesInBounds() {\n      this.camerasWithImagesInBounds = [];\n      this.cameraImagesList = [];\n      const { start, end } = this.bounds;\n      const cameraObjectPromises = [];\n      this.telemetryKeys.forEach((telemetryKey) => {\n        const cameraPromise = this.openmct.objects.get(telemetryKey);\n        cameraObjectPromises.push(cameraPromise);\n      });\n      const cameraObjects = await Promise.all(cameraObjectPromises);\n\n      const cameraTelemetryPromises = [];\n      cameraObjects.forEach((cameraObject) => {\n        const cameraTelemetryPromise = this.openmct.telemetry.request(cameraObject, {\n          start,\n          end\n        });\n        cameraTelemetryPromises.push(cameraTelemetryPromise);\n      });\n      const cameraImages = await Promise.all(cameraTelemetryPromises);\n\n      cameraObjects.forEach((cameraObject, index) => {\n        cameraObject.images = cameraImages[index];\n      });\n\n      cameraObjects.forEach((cameraObject) => {\n        if (cameraObject.images.length > 0) {\n          const imagesInBounds = cameraObject.images.filter((imageDetails) => {\n            if (!imageDetails.timestamp) {\n              return false;\n            }\n            const timestamp = Date.parse(imageDetails.timestamp);\n            return timestamp >= start && timestamp <= end;\n          });\n          if (imagesInBounds.length > 0) {\n            cameraObject.imagesInBounds = imagesInBounds;\n            this.camerasWithImagesInBounds.push(cameraObject);\n          }\n        }\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/InspectorDataVisualizationComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-inspector__properties c-data-visualization-inspector__properties c-data-visualization-inspector__flex-column\"\n  >\n    <DataVisualization\n      :data-ranges=\"dataRanges\"\n      :plot-telemetry-keys=\"plotTelemetryKeys\"\n      :description=\"description\"\n      :is-loading=\"isLoading\"\n    />\n  </div>\n</template>\n\n<script>\nimport DataVisualization from './DataVisualization.vue';\n\nexport default {\n  components: {\n    DataVisualization\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    context: {\n      type: Object,\n      required: true\n    }\n  },\n  computed: {\n    dataRanges() {\n      return this.context.dataRanges;\n    },\n    plotTelemetryKeys() {\n      return this.context.telemetryKeys;\n    },\n    description() {\n      return this.context.description;\n    },\n    isLoading() {\n      return Boolean(this.context.loading);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/InspectorDataVisualizationViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport InspectorDataVisualizationComponent from './InspectorDataVisualizationComponent.vue';\n\nexport default function InspectorDataVisualizationViewProvider(openmct, configuration) {\n  const {\n    type = 'mmgis',\n    name = 'Data Visualization',\n    placeholderText = '',\n    plotOptions,\n    imageryOptions\n  } = configuration;\n\n  return {\n    key: 'inspectorDataVisualizationView',\n    name,\n\n    canView(selection) {\n      const domainObject = selection?.[0]?.[0]?.context?.item;\n\n      return domainObject?.type === type;\n    },\n\n    view(selection) {\n      let _destroy = null;\n\n      const context = selection[0][0].context;\n      const domainObject = context.item;\n      const dataVisualizationContext = context?.dataVisualization ?? {};\n      const timeFormatter =\n        openmct.telemetry.getFormatter('iso') || openmct.telemetry.getFormatter('utc');\n\n      return {\n        show(element) {\n          const { destroy } = mount(\n            {\n              components: {\n                InspectorDataVisualization: InspectorDataVisualizationComponent\n              },\n              provide: {\n                openmct,\n                domainObject,\n                timeFormatter,\n                placeholderText,\n                plotOptions,\n                imageryOptions\n              },\n              data() {\n                return {\n                  context: dataVisualizationContext\n                };\n              },\n              template: `<InspectorDataVisualization :context=\"context\" />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority() {\n          return openmct.priority.HIGH;\n        },\n        destroy() {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/NumericDataInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__numeric-data\">\n    <div class=\"c-inspect-properties\">\n      <div class=\"c-inspect-properties__header\">Numeric Data</div>\n    </div>\n    <div ref=\"numericDataView\">\n      <TelemetryFrame\n        v-for=\"plotObject of plotObjects\"\n        :key=\"plotObject.identifier.key\"\n        :bounds=\"bounds\"\n        :telemetry-object=\"plotObject\"\n        :path=\"[plotObject]\"\n        :render-when-visible=\"plotObject.renderWhenVisible\"\n      >\n        <Plot />\n      </TelemetryFrame>\n    </div>\n\n    <div v-if=\"!hasNumericData\">\n      {{ noNumericDataText }}\n    </div>\n  </div>\n</template>\n<script>\nimport VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';\nimport Plot from '../plot/PlotView.vue';\nimport TelemetryFrame from './TelemetryFrame.vue';\n\nexport default {\n  components: {\n    TelemetryFrame,\n    Plot\n  },\n  inject: ['openmct', 'domainObject', 'timeFormatter'],\n  props: {\n    bounds: {\n      type: Object,\n      required: true\n    },\n    telemetryKeys: {\n      type: Array,\n      default: () => []\n    },\n    noNumericDataText: {\n      type: String,\n      default: 'No Numeric Data to display.'\n    }\n  },\n  data() {\n    return {\n      plotObjects: []\n    };\n  },\n  computed: {\n    hasNumericData() {\n      return this.plotObjects.length > 0;\n    }\n  },\n  watch: {\n    telemetryKeys: {\n      handler() {\n        this.renderNumericData();\n      },\n      deep: true\n    },\n    bounds: {\n      handler() {\n        this.renderNumericData();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.renderNumericData();\n  },\n  beforeUnmount() {\n    this.clearPlots();\n  },\n  methods: {\n    renderNumericData() {\n      this.clearPlots();\n\n      this.unregisterTimeContextList = [];\n      this.visibilityObservers = [];\n\n      this.telemetryKeys.forEach(async (telemetryKey) => {\n        const plotObject = await this.openmct.objects.get(telemetryKey);\n        const visibilityObserver = new VisibilityObserver(\n          this.$refs.numericDataView,\n          this.openmct.element\n        );\n        plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;\n\n        this.visibilityObservers.push(visibilityObserver);\n        this.plotObjects.push(plotObject);\n        this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));\n      });\n    },\n    setIndependentTimeContextForComponent(plotObject) {\n      const keyString = this.openmct.objects.makeKeyString(plotObject.identifier);\n\n      // get an independent time context for object\n      this.openmct.time.getContextForView([plotObject]);\n      // set the time context of the object to the selected time range\n      return this.openmct.time.addIndependentContext(keyString, this.bounds);\n    },\n    clearPlots() {\n      if (this.visibilityObservers?.length) {\n        this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());\n        delete this.visibilityObservers;\n      }\n\n      if (this.plotObjects?.length) {\n        this.plotObjects.splice(0, this.plotObjects.length);\n      }\n\n      if (this.unregisterTimeContextList?.length) {\n        this.unregisterTimeContextList.forEach((unregisterTimeContext) => unregisterTimeContext());\n        delete this.unregisterTimeContextList;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/TelemetryFrame.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-telemetry-frame\">\n    <div class=\"c-telemetry-frame__title-bar\">\n      <span class=\"c-telemetry-frame__title\">\n        <span class=\"c-telemetry-frame__title-icon icon-telemetry\"></span>\n        <span class=\"title-text\">{{ telemetryObject.name }}</span>\n      </span>\n      <button\n        ref=\"menu-button\"\n        title=\"More actions\"\n        aria-label=\"More actions\"\n        class=\"l-browse-bar__actions c-icon-button icon-3-dots\"\n        @click=\"toggleMenu\"\n      ></button>\n    </div>\n    <div\n      v-if=\"showMenu\"\n      class=\"c-menu c-menu__inspector-telemetry-options\"\n      aria-label=\"Telemetry Options\"\n      @blur=\"showMenu = false\"\n    >\n      <ul>\n        <li\n          v-if=\"telemetryObject.type === 'yamcs.telemetry'\"\n          role=\"menuitem\"\n          title=\"View Full Screen\"\n          class=\"icon-eye-open\"\n          @click=\"previewTelemetry\"\n        >\n          View Full Screen\n        </li>\n        <li\n          role=\"menuitem\"\n          title=\"Open in a new browser tab\"\n          class=\"icon-new-window\"\n          @click=\"openInNewTab\"\n        >\n          Open In New Tab\n        </li>\n      </ul>\n    </div>\n    <slot></slot>\n  </div>\n</template>\n\n<script>\nimport { NEW_TAB_ACTION_KEY } from '@/plugins/openInNewTabAction/openInNewTabAction.js';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nexport default {\n  inject: ['openmct'],\n  provide() {\n    return {\n      domainObject: this.telemetryObject,\n      objectPath: this.path,\n      renderWhenVisible: this.renderWhenVisible\n    };\n  },\n  props: {\n    bounds: {\n      type: Object,\n      default: () => {}\n    },\n    telemetryObject: {\n      type: Object,\n      default: () => {}\n    },\n    path: {\n      type: Array,\n      default: () => []\n    },\n    renderWhenVisible: {\n      type: Function,\n      required: true\n    }\n  },\n  data() {\n    return {\n      showMenu: false\n    };\n  },\n  methods: {\n    toggleMenu() {\n      this.showMenu = !this.showMenu;\n    },\n    async getTelemetryPath() {\n      const telemetryObjectKeyString = this.openmct.objects.makeKeyString(\n        this.telemetryObject.identifier\n      );\n\n      const telemetryPath = await this.openmct.objects.getOriginalPath(telemetryObjectKeyString);\n      return telemetryPath;\n    },\n    async openInNewTab() {\n      const telemetryPath = await this.getTelemetryPath();\n      const sourceTelemObject = telemetryPath[0];\n      const timeBounds = this.bounds;\n      const urlParams = {\n        'tc.startBound': timeBounds?.start,\n        'tc.endBound': timeBounds?.end,\n        'tc.mode': 'fixed'\n      };\n      const newTabAction = this.openmct.actions.getAction(NEW_TAB_ACTION_KEY);\n      // No view context needed, so pass undefined.\n      // The urlParams arg will override the global time bounds with the data visualization\n      // plot bounds.\n      newTabAction.invoke([sourceTelemObject], undefined, urlParams);\n      this.showMenu = false;\n    },\n    previewTelemetry() {\n      const previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n      previewAction.invoke([this.telemetryObject]);\n      this.showMenu = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/inspector-data-visualization.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n //InspectorDataVisualizationComponent\n .c-data-visualization-inspector__flex-column {\n  display: flex;\n  flex-direction: column;\n}\n\n.c-data-visualization-inspector__flex-row {\n  display: flex;\n  flex-direction: row;\n}\n\n.c-data-visualization-inspect-properties + .c-data-visualization-inspect-properties {\n  margin-top: 10px;\n}\n\n// DataVisualization\n.c-inspector__data-pivot-placeholder {\n  margin-top: 8px;\n}\n\n.c-inspector__data-pivot-coordinates-wrapper {\n  display: flex;\n  align-items: center;\n  margin-top: 10px;\n}\n\n.c-inspector__data-pivot-coordinates {\n  margin-left: 6px;\n  text-transform: capitalize;\n}\n\n.c-inspector__data-pivot-range-selector {\n  margin: 10px auto;\n  height: 25px;\n  max-width: 100%;\n}\n\n.c-inspector__imagery-view {\n  margin-top: 10px;\n}\n\n.c-imagery-view__camera-image-set {\n  grid-column: 1/3;\n}\n\n.c-imagery-view__camera-image-list {\n  display: grid;\n  grid-auto-flow: column;\n  grid-gap: 10px;\n  grid-auto-columns: min-content;\n  overflow: auto;\n  white-space: nowrap;\n  margin-top: 5px;\n}\n\n.c-imagery-view__camera-image {\n  display: inline-block;\n}\n\n.c-imagery-view__camera-image img {\n  width: 70px;\n  height: 70px;\n}\n\n.c-imagery-view__camera-image-timestamp {\n  white-space: break-spaces;\n}\n\n// Telemetry Frame\n.c-telemetry-frame {\n  margin: 8px 0px;\n\n  &__title-bar {\n    display: flex;\n    align-items: center;\n    margin: 6px 0px;\n  }\n  \n  &__title {\n    flex: 1;\n    font-size: 1.2em;\n  }\n  \n  &__title-icon {\n    margin-right: 4px;\n  }\n}\n\n.c-telemetry-frame .c-menu {\n  position: absolute;\n  right: 0px;\n}\n\n.c-inspector__data-pivot .c-plot {\n  position: relative;\n  min-height: 150px;\n  max-height: 200px;\n}\n\n.c-inspector__data-pivot .c-plot .c-plot--stacked-container {\n  min-height: 150px;\n}\n\n.c-inspector__numeric-data .c-inspect-properties__header {\n  margin-bottom: 10px;\n}\n"
  },
  {
    "path": "src/plugins/inspectorDataVisualization/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport InspectorDataVisualizationViewProvider from './InspectorDataVisualizationViewProvider.js';\n\nexport default function (options) {\n  return function (openmct) {\n    openmct.inspectorViews.addProvider(\n      new InspectorDataVisualizationViewProvider(openmct, options)\n    );\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__properties c-inspect-properties\" aria-label=\"Tags Inspector\">\n    <div class=\"c-inspect-properties__header\">Tags</div>\n    <div v-if=\"shouldShowTagsEditor\" class=\"c-inspect-properties__section\">\n      <TagEditor\n        :targets=\"targetDetails\"\n        :target-domain-objects=\"targetDomainObjects\"\n        :domain-object=\"domainObject\"\n        :annotations=\"loadedAnnotations\"\n        :annotation-type=\"annotationType\"\n        :on-tag-change=\"onAnnotationChange\"\n      />\n    </div>\n    <div v-else class=\"c-inspect-properties__row--span-all\">\n      {{ noTagsMessage }}\n    </div>\n  </div>\n</template>\n\n<script>\nimport TagEditor from './tags/TagEditor.vue';\n\nexport default {\n  components: {\n    TagEditor\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      selection: null,\n      lastLocalAnnotationCreations: {},\n      unobserveEntries: {},\n      loadedAnnotations: []\n    };\n  },\n  computed: {\n    hasAnnotations() {\n      return Boolean(this.loadedAnnotations && this.loadedAnnotations.length);\n    },\n    nonTagAnnotations() {\n      if (!this.loadedAnnotations) {\n        return [];\n      }\n\n      return this.loadedAnnotations.filter((annotation) => {\n        return !annotation.tags;\n      });\n    },\n    tagAnnotations() {\n      if (!this.loadedAnnotations) {\n        return [];\n      }\n\n      return this.loadedAnnotations.filter((annotation) => {\n        return !annotation.tags;\n      });\n    },\n    multiSelection() {\n      return this.selection && this.selection.length > 1;\n    },\n    noAnnotationsMessage() {\n      return this.multiSelection\n        ? 'No annotations to display for multiple items'\n        : 'No annotations to display for this item';\n    },\n    noTagsMessage() {\n      return this.multiSelection\n        ? 'No tags to display for multiple items'\n        : 'No tags to display for this item';\n    },\n    domainObject() {\n      return this?.selection?.[0]?.[0]?.context?.item;\n    },\n    targetDetails() {\n      return this?.selection?.[0]?.[0]?.context?.targetDetails ?? [];\n    },\n    shouldShowTagsEditor() {\n      const showingTagsEditor = this.targetDetails?.length > 0;\n\n      if (showingTagsEditor) {\n        return true;\n      }\n\n      return false;\n    },\n    targetDomainObjects() {\n      return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? [];\n    },\n    selectedAnnotations() {\n      return this?.selection?.[0]?.[0]?.context?.annotations;\n    },\n    annotationType() {\n      return this?.selection?.[0]?.[0]?.context?.annotationType;\n    },\n    annotationFilter() {\n      return this?.selection?.[0]?.[0]?.context?.annotationFilter;\n    },\n    onAnnotationChange() {\n      return this?.selection?.[0]?.[0]?.context?.onAnnotationChange;\n    }\n  },\n  async mounted() {\n    this.abortController = null;\n    this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);\n    this.openmct.selection.on('change', this.updateSelection);\n    await this.updateSelection(this.openmct.selection.get());\n  },\n  beforeUnmount() {\n    this.openmct.annotation.off('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);\n    this.openmct.selection.off('change', this.updateSelection);\n    const unobserveEntryFunctions = Object.values(this.unobserveEntries);\n    unobserveEntryFunctions.forEach((unobserveEntry) => {\n      unobserveEntry();\n    });\n  },\n  methods: {\n    loadNewAnnotations(annotationsToLoad) {\n      if (!annotationsToLoad || !annotationsToLoad.length) {\n        this.loadedAnnotations.splice(0);\n\n        return;\n      }\n\n      const sortedAnnotations = annotationsToLoad.sort((annotationA, annotationB) => {\n        return annotationB.modified - annotationA.modified;\n      });\n\n      const mutableAnnotations = sortedAnnotations.map((annotation) => {\n        return this.openmct.objects.toMutable(annotation);\n      });\n\n      if (sortedAnnotations.length < this.loadedAnnotations.length) {\n        this.loadedAnnotations = this.loadedAnnotations.slice(0, mutableAnnotations.length);\n      }\n\n      for (let index = 0; index < mutableAnnotations.length; index += 1) {\n        this.loadedAnnotations[index] = mutableAnnotations[index];\n      }\n    },\n    updateSelection(selection) {\n      const unobserveEntryFunctions = Object.values(this.unobserveEntries);\n      unobserveEntryFunctions.forEach((unobserveEntry) => {\n        unobserveEntry();\n      });\n      this.unobserveEntries = {};\n\n      this.selection = selection;\n      this.targetDomainObjects.forEach((targetObject) => {\n        const targetKey = targetObject.keyString;\n        this.lastLocalAnnotationCreations[targetKey] = targetObject?.annotationLastCreated ?? 0;\n        if (!this.unobserveEntries[targetKey]) {\n          this.unobserveEntries[targetKey] = this.openmct.objects.observe(\n            targetObject,\n            '*',\n            this.targetObjectChanged\n          );\n        }\n      });\n      this.loadNewAnnotations(this.selectedAnnotations);\n    },\n    async targetObjectChanged(target) {\n      const targetID = this.openmct.objects.makeKeyString(target.identifier);\n      const lastLocalAnnotationCreation = this.lastLocalAnnotationCreations[targetID] ?? 0;\n      if (lastLocalAnnotationCreation < target.annotationLastCreated) {\n        this.lastLocalAnnotationCreations[targetID] = target.annotationLastCreated;\n        await this.loadAnnotationForTargetObject(target);\n      }\n    },\n    async loadAnnotationForTargetObject(target) {\n      // If the user changes targets while annotations are loading,\n      // abort the previous request.\n      if (this.abortController !== null) {\n        this.abortController.abort();\n      }\n\n      this.abortController = new AbortController();\n\n      try {\n        const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(\n          target.identifier,\n          this.abortController.signal\n        );\n        const filteredAnnotationsForSelection = allAnnotationsForTarget.filter((annotation) =>\n          this.openmct.annotation.areAnnotationTargetsEqual(\n            this.annotationType,\n            this.targetDetails,\n            annotation.targets\n          )\n        );\n        this.loadNewAnnotations(filteredAnnotationsForSelection);\n      } catch (err) {\n        if (err.name !== 'AbortError') {\n          throw err;\n        }\n      } finally {\n        this.abortController = null;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/AnnotationsViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Annotations from './AnnotationsInspectorView.vue';\n\nexport default function AnnotationsViewProvider(openmct) {\n  return {\n    key: 'annotationsView',\n    name: 'Annotations',\n    canView: function (selection) {\n      const availableTags = openmct.annotation.getAvailableTags();\n      const selectionContext = selection?.[0]?.[0]?.context;\n      const domainObject = selectionContext?.item;\n      const isLayoutItem = selectionContext?.layoutItem;\n\n      if (availableTags.length < 1 || isLayoutItem || !domainObject || openmct.editor.isEditing()) {\n        return false;\n      }\n\n      const isAnnotatableType = openmct.annotation.isAnnotatableType(domainObject.type);\n      const metadata = openmct.telemetry.getMetadata(domainObject);\n      const hasImagery = metadata?.valuesForHints(['image']).length > 0;\n      const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';\n      const hasNumericTelemetry = openmct.telemetry.hasNumericTelemetry(domainObject);\n\n      return isAnnotatableType || hasImagery || hasNumericTelemetry || isNotebookEntry;\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      const selectionContext = selection?.[0]?.[0]?.context;\n      const isImageSelection = selectionContext?.type === 'clicked-on-image-selection';\n      const domainObject = selectionContext?.item;\n      const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';\n      const isConditionSet = domainObject?.type === 'conditionSet';\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Annotations\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: `<Annotations />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          if (isNotebookEntry || isImageSelection) {\n            return openmct.priority.HIGHEST;\n          }\n\n          if (isConditionSet) {\n            return openmct.priority.LOW;\n          }\n\n          return openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/tags/TagEditor.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"TagEditor\" class=\"c-tag-applier\">\n    <TagSelection\n      v-for=\"(addedTag, index) in addedTags\"\n      :key=\"index\"\n      :class=\"{ 'w-tag-wrapper--tag-selector': addedTag.newTag }\"\n      :selected-tag=\"addedTag.newTag ? null : addedTag\"\n      :new-tag=\"addedTag.newTag\"\n      :added-tags=\"addedTags\"\n      @tag-removed=\"tagRemoved\"\n      @tag-added=\"tagAdded\"\n    />\n    <button\n      v-show=\"!userAddingTag && !maxTagsAdded\"\n      class=\"c-tag-applier__add-btn c-icon-button c-icon-button--major icon-plus\"\n      :class=\"TagEditorClassNames.ADD_TAG_BUTTON\"\n      title=\"Add new tag\"\n      @click=\"addTag\"\n    >\n      <div class=\"c-icon-button__label c-tag-btn__label\" :class=\"TagEditorClassNames.ADD_TAG_LABEL\">\n        Add Tag\n      </div>\n    </button>\n  </div>\n</template>\n\n<script>\nimport { toRaw } from 'vue';\n\nimport TagEditorClassNames from './TagEditorClassNames.js';\nimport TagSelection from './TagSelection.vue';\n\nexport default {\n  components: {\n    TagSelection\n  },\n  inject: ['openmct'],\n  props: {\n    annotations: {\n      type: Array,\n      required: true\n    },\n    annotationType: {\n      type: String,\n      required: false,\n      default: null\n    },\n    domainObject: {\n      type: Object,\n      required: true,\n      default: null\n    },\n    targets: {\n      type: Array,\n      required: true,\n      default: null\n    },\n    targetDomainObjects: {\n      type: Array,\n      required: true,\n      default: null\n    },\n    onTagChange: {\n      type: Function,\n      required: false,\n      default: null\n    }\n  },\n  emits: ['tags-updated'],\n  data() {\n    return {\n      addedTags: [],\n      userAddingTag: false,\n      TagEditorClassNames: TagEditorClassNames\n    };\n  },\n  computed: {\n    availableTags() {\n      return this.openmct.annotation.getAvailableTags();\n    },\n    maxTagsAdded() {\n      const availableTags = this.openmct.annotation.getAvailableTags();\n\n      return !(\n        availableTags &&\n        availableTags.length &&\n        this.addedTags.length < availableTags.length\n      );\n    }\n  },\n  watch: {\n    annotations: {\n      handler() {\n        this.annotationsChanged();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.annotationsChanged();\n  },\n  unmounted() {\n    document.body.removeEventListener('click', this.tagCanceled);\n  },\n  methods: {\n    annotationsChanged() {\n      if (this.annotations) {\n        this.tagsChanged();\n      }\n    },\n    annotationDeletionListener(changedAnnotation) {\n      const matchingAnnotation = this.annotations.find((possibleMatchingAnnotation) => {\n        return this.openmct.objects.areIdsEqual(\n          possibleMatchingAnnotation.identifier,\n          changedAnnotation.identifier\n        );\n      });\n      if (matchingAnnotation) {\n        matchingAnnotation._deleted = changedAnnotation._deleted;\n        this.userAddingTag = false;\n        this.tagsChanged();\n      }\n    },\n    tagsChanged() {\n      // gather tags from annotations\n      const tagsFromAnnotations = this.annotations\n        .flatMap((annotation) => {\n          if (annotation._deleted) {\n            return [];\n          } else {\n            return annotation.tags;\n          }\n        })\n        .filter((tag, index, array) => {\n          return array.indexOf(tag) === index;\n        });\n\n      if (tagsFromAnnotations.length !== this.addedTags.length) {\n        this.addedTags = this.addedTags.slice(0, tagsFromAnnotations.length);\n      }\n\n      for (let index = 0; index < tagsFromAnnotations.length; index += 1) {\n        this.addedTags[index] = tagsFromAnnotations[index];\n      }\n    },\n    addTag() {\n      const newTagValue = {\n        newTag: true\n      };\n      this.addedTags.push(newTagValue);\n      this.userAddingTag = true;\n      document.body.addEventListener('click', this.tagCanceled);\n    },\n    async tagRemoved(tagToRemove) {\n      // Soft delete annotations that match tag instead (that aren't already deleted)\n      const annotationsToDelete = this.annotations.filter((annotation) => {\n        return annotation.tags.includes(tagToRemove) && !annotation._deleted;\n      });\n      if (annotationsToDelete) {\n        await this.openmct.annotation.deleteAnnotations(annotationsToDelete);\n        this.$emit('tags-updated', annotationsToDelete);\n        if (this.onTagChange) {\n          this.userAddingTag = false;\n          this.onTagChange(this.annotations);\n        }\n      }\n    },\n    tagCanceled(event) {\n      if (this.$refs.TagEditor) {\n        const clickedInsideTagEditor = this.$refs.TagEditor.contains(event.target);\n        if (!clickedInsideTagEditor) {\n          // Hide TagSelection and show \"Add Tag\" button\n          this.userAddingTag = false;\n          this.tagsChanged();\n        }\n      }\n    },\n    async tagAdded(newTag) {\n      // Either undelete an annotation, or create one (1) new annotation\n      let existingAnnotation = this.annotations.find((annotation) => {\n        return annotation.tags.includes(newTag);\n      });\n\n      if (!existingAnnotation) {\n        const contentText = `${this.annotationType} tag`;\n\n        // need to get raw version of target domain objects for comparisons to work\n        const rawTargetDomainObjects = this.targetDomainObjects.map((targetDomainObject) => {\n          return toRaw(targetDomainObject);\n        });\n        const annotationCreationArguments = {\n          name: contentText,\n          existingAnnotation,\n          contentText: contentText,\n          targets: toRaw(this.targets),\n          targetDomainObjects: rawTargetDomainObjects,\n          domainObject: toRaw(this.domainObject),\n          annotationType: toRaw(this.annotationType),\n          tags: [newTag]\n        };\n        existingAnnotation = await this.openmct.annotation.create(annotationCreationArguments);\n      } else if (existingAnnotation._deleted) {\n        this.openmct.annotation.unDeleteAnnotation(existingAnnotation);\n      }\n\n      this.userAddingTag = false;\n      document.body.removeEventListener('click', this.tagCanceled);\n\n      this.$emit('tags-updated', existingAnnotation);\n      if (this.onTagChange) {\n        this.onTagChange([existingAnnotation]);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/tags/TagEditorClassNames.js",
    "content": "const TagEditorClassNames = Object.freeze({\n  REMOVE_TAG: 'js-remove-tag',\n  AUTOCOMPLETE_INPUT: 'js-autocomplete__input',\n  ADD_TAG_BUTTON: 'js-add-tag-button',\n  ADD_TAG_LABEL: 'js-add-tag-label',\n  TAG_OPTION: 'js-tag-option'\n});\n\nexport default TagEditorClassNames;\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/tags/TagSelection.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"w-tag-wrapper has-tag-applier\">\n    <template v-if=\"newTag\">\n      <AutoCompleteField\n        v-if=\"newTag\"\n        ref=\"tagSelection\"\n        :model=\"availableTagModel\"\n        :place-holder-text=\"'Type to select tag'\"\n        class=\"c-tag-selection\"\n        :item-css-class=\"`icon-circle ${TagEditorClassNames.TAG_OPTION}`\"\n        @on-change=\"tagSelected\"\n      />\n    </template>\n    <template v-else>\n      <div\n        class=\"c-tag\"\n        :class=\"{ 'c-tag-edit': !readOnly }\"\n        :style=\"{ background: selectedBackgroundColor, color: selectedForegroundColor }\"\n      >\n        <button\n          v-show=\"!readOnly\"\n          class=\"c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle\"\n          :class=\"TagEditorClassNames.REMOVE_TAG\"\n          :style=\"{ textShadow: selectedBackgroundColor + ' 0 0 4px' }\"\n          :aria-label=\"`Remove tag ${selectedTagLabel}`\"\n          @click=\"removeTag\"\n        ></button>\n        <div class=\"c-tag__label\" aria-label=\"Tag\">{{ selectedTagLabel }}</div>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport AutoCompleteField from '../../../../api/forms/components/controls/AutoCompleteField.vue';\nimport TagEditorClassNames from './TagEditorClassNames.js';\n\nexport default {\n  components: {\n    AutoCompleteField\n  },\n  inject: ['openmct'],\n  props: {\n    addedTags: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    selectedTag: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    newTag: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['tag-removed', 'tag-added'],\n  data() {\n    return { TagEditorClassNames: TagEditorClassNames };\n  },\n  computed: {\n    availableTagModel() {\n      const availableTags = this.openmct.annotation\n        .getAvailableTags()\n        .filter((tag) => {\n          return !this.addedTags.includes(tag.id);\n        })\n        .map((tag) => {\n          return {\n            name: tag.label,\n            color: tag.backgroundColor,\n            id: tag.id\n          };\n        });\n\n      return {\n        options: availableTags\n      };\n    },\n    selectedBackgroundColor() {\n      const selectedTag = this.getAvailableTagByID(this.selectedTag);\n      if (selectedTag) {\n        return selectedTag.backgroundColor;\n      } else {\n        // missing available tag color, use default\n        return '#00000';\n      }\n    },\n    selectedForegroundColor() {\n      const selectedTag = this.getAvailableTagByID(this.selectedTag);\n      if (selectedTag) {\n        return selectedTag.foregroundColor;\n      } else {\n        // missing available tag color, use default\n        return '#FFFFF';\n      }\n    },\n    selectedTagLabel() {\n      const selectedTag = this.getAvailableTagByID(this.selectedTag);\n      if (selectedTag) {\n        return selectedTag.label;\n      } else {\n        // missing available tag color, use default\n        return '¡UNKNOWN!';\n      }\n    }\n  },\n  methods: {\n    getAvailableTagByID(tagID) {\n      return this.openmct.annotation.getAvailableTags().find((tag) => {\n        return tag.id === tagID;\n      });\n    },\n    removeTag() {\n      this.$emit('tag-removed', this.selectedTag);\n    },\n    tagSelected(autoField) {\n      const tagAdded = autoField.model.options.find((option) => {\n        if (option.name === autoField.value) {\n          return true;\n        }\n\n        return false;\n      });\n      if (tagAdded) {\n        this.$emit('tag-added', tagAdded.id);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/annotations/tags/tags.scss",
    "content": "@mixin tagHolder() {\n  align-items: center;\n  display: flex;\n  flex-wrap: wrap;\n\n  > * {\n    $m: $interiorMarginSm;\n\n    margin: 0 $m $m 0;\n  }\n}\n\n/******************************* TAGS */\n.c-tag {\n  /* merge conflict in 5247\n    border-radius: 10px; //TODO: convert to theme constant\n    display: inline-flex;\n    padding: 1px 10px; //TODO: convert to theme constant\n\n    > * + * {\n        margin-left: $interiorMargin;\n    }\n\n    &__remove-btn {\n        color: inherit !important;\n        display: none;\n        opacity: 0;\n        overflow: hidden;\n        padding: 1px !important;\n        @include transition(opacity);\n        width: 0;\n */\n  border-radius: $tagBorderRadius;\n  display: inline-flex;\n  overflow: hidden;\n  padding: 1px 6px; //TODO: convert to theme constant\n  transition: $transIn;\n\n  &__remove-btn {\n    color: inherit !important;\n    opacity: 0;\n    padding: 0; // Overrides default <button> padding\n    position: absolute;\n    right: 2px;\n    transition: $transIn;\n    width: 0;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  /* SEARCH RESULTS */\n  &.--is-not-search-match {\n    opacity: 0.5;\n  }\n}\n\n.c-tag-holder {\n  @include tagHolder;\n}\n\n.w-tag-wrapper {\n  $m: $interiorMarginSm;\n\n  margin: 0 $m $m 0;\n}\n\n/******************************* TAGS IN INSPECTOR / TAG SELECTION & APPLICATION */\n.c-tag-applier {\n  /* merge conflict in fix-repaint-5247\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    align-items: center;\n\n    > * + * {\n        margin-left: $interiorMargin;\n    }\n\n    &__add-btn {\n        &:before {\n            font-size: 0.9em;\n        }\n    }\n\n    .c-tag {\n        flex-direction: row;\n        align-items: center;\n        padding-right: 3px !important;\n\n        &__remove-btn {\n            display: block;\n        }\n*/\n  $tagApplierPadding: 3px 6px;\n  @include tagHolder;\n  grid-column: 1 / 3;\n\n  &__tags {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n  }\n\n  &__add-btn {\n    border-radius: $tagBorderRadius;\n    padding: 3px 10px 3px 4px;\n\n    &:before {\n      font-size: 0.9em;\n    }\n  }\n\n  .c-tag {\n    flex-direction: row;\n    align-items: center;\n    padding: $tagApplierPadding;\n\n    > * + * {\n      margin-left: $interiorMarginSm;\n    }\n  }\n\n  .c-tag-selection {\n    .c-input--autocomplete__input {\n      min-height: auto !important;\n      padding: $tagApplierPadding;\n    }\n  }\n\n  .c-tag-btn__label {\n    overflow: visible !important;\n  }\n\n  /******************************* HOVERS */\n  .has-tag-applier {\n    /* merge conflict in fix-repaint-5247\n    $p: opacity, width;\n    // Apply this class to all components that should trigger tag removal btn on hover\n    .c-tag__remove-btn {\n        @include transition($prop: $p, $dur: $transOutTime);\n    }\n\n    &:hover {\n        .c-tag__remove-btn {\n            width: 1.1em;\n            opacity: 0.7;\n        }\n    }\n}\n*/\n    // Apply this class to all components that should trigger tag removal btn on hover\n    &:hover {\n      .c-tag {\n        @include userSelectNone();\n        transition: $transOut;\n      }\n      .c-tag__label {\n        opacity: 0.7;\n      }\n      .c-tag__remove-btn {\n        width: 1.3em;\n        opacity: 0.8;\n        padding: 2px !important;\n        transition: $transOut;\n        right: 5%;\n        text-align: center;\n        z-index: 2;\n\n        &:hover {\n          opacity: 1;\n\n          & ~ * {\n            // This sibling selector further dims the label\n            // to make the remove button stand out\n            opacity: 0.4;\n          }\n        }\n      }\n    }\n  }\n}\n\n.c-tag-applier__add-btn.c-icon-button.c-icon-button--major.icon-plus {\n  color: $colorKey !important;\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/ElementItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <li\n    v-if=\"allowDrag\"\n    draggable=\"true\"\n    :aria-label=\"`${elementObject.name} Element Item`\"\n    :aria-grabbed=\"hover\"\n    @dragstart=\"emitDragStartEvent\"\n    @dragenter=\"onDragenter\"\n    @dragover.prevent\n    @dragleave=\"onDragleave\"\n    @drop=\"emitDropEvent\"\n  >\n    <div\n      class=\"c-tree__item c-elements-pool__item js-elements-pool__item\"\n      :class=\"{\n        'is-context-clicked': contextClickActive,\n        hover: hover,\n        'is-alias': isAlias\n      }\"\n    >\n      <span class=\"c-elements-pool__grippy c-grippy c-grippy--vertical-drag\"></span>\n      <ObjectLabel\n        :domain-object=\"elementObject\"\n        :object-path=\"[elementObject, domainObject]\"\n        @context-click-active=\"setContextClickState\"\n      />\n      <slot name=\"content\"></slot>\n    </div>\n  </li>\n  <li v-else :aria-label=\"`${elementObject.name} Element Item`\">\n    <div\n      class=\"c-tree__item c-elements-pool__item js-elements-pool__item\"\n      :class=\"{\n        'is-context-clicked': contextClickActive,\n        hover: hover,\n        'is-alias': isAlias\n      }\"\n    >\n      <ObjectLabel\n        :domain-object=\"elementObject\"\n        :object-path=\"[elementObject, domainObject]\"\n        @context-click-active=\"setContextClickState\"\n      />\n    </div>\n  </li>\n</template>\n\n<script>\nimport ObjectLabel from '../../../ui/components/ObjectLabel.vue';\n\nexport default {\n  components: {\n    ObjectLabel\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    index: {\n      type: Number,\n      required: true,\n      default: () => {\n        return 0;\n      }\n    },\n    elementObject: {\n      type: Object,\n      required: true,\n      default: () => {\n        return {};\n      }\n    },\n    allowDrop: {\n      type: Boolean\n    },\n    allowDrag: {\n      type: Boolean\n    }\n  },\n  emits: ['drop-custom', 'dragstart-custom'],\n  data() {\n    return {\n      contextClickActive: false,\n      hover: false\n    };\n  },\n  computed: {\n    isAlias() {\n      return (\n        this.elementObject.location !==\n        this.openmct.objects.makeKeyString(this.domainObject.identifier)\n      );\n    }\n  },\n  methods: {\n    emitDropEvent(event) {\n      this.$emit('drop-custom', event);\n      this.hover = false;\n    },\n    emitDragStartEvent(event) {\n      this.$emit('dragstart-custom', this.index);\n    },\n    onDragenter(event) {\n      if (this.allowDrop) {\n        this.hover = true;\n        this.dragElement = event.target.parentElement;\n      }\n    },\n    onDragleave(event) {\n      if (event.target.parentElement === this.dragElement) {\n        this.hover = false;\n        delete this.dragElement;\n      }\n    },\n    setContextClickState(state) {\n      this.contextClickActive = state;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/ElementItemGroup.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-elements-pool__group\"\n    :class=\"{\n      hover: hover\n    }\"\n    :allow-drop=\"allowDrop\"\n    @dragover.prevent\n    @dragenter=\"onDragEnter\"\n    @dragleave.stop=\"onDragLeave\"\n    @drop=\"emitDrop\"\n  >\n    <ul>\n      <div>\n        <span class=\"c-elements-pool__grippy c-grippy c-grippy--vertical-drag\"></span>\n        <div class=\"c-tree__item__type-icon c-object-label__type-icon\">\n          <span class=\"is-status__indicator\"></span>\n        </div>\n        <div\n          class=\"c-tree__item__name c-object-label__name\"\n          :aria-label=\"`Element Item Group ${label}`\"\n        >\n          {{ label }}\n        </div>\n      </div>\n      <slot></slot>\n    </ul>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    parentObject: {\n      type: Object,\n      required: true,\n      default: () => {\n        return {};\n      }\n    },\n    label: {\n      type: String,\n      required: true,\n      default: () => {\n        return '';\n      }\n    },\n    allowDrop: {\n      type: Boolean\n    }\n  },\n  emits: ['drop-group'],\n  data() {\n    return {\n      dragCounter: 0\n    };\n  },\n  computed: {\n    hover() {\n      return this.dragCounter > 0;\n    }\n  },\n  methods: {\n    emitDrop(event) {\n      this.dragCounter = 0;\n      this.$emit('drop-group', event);\n    },\n    onDragEnter(event) {\n      this.dragCounter++;\n    },\n    onDragLeave(event) {\n      this.dragCounter--;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/ElementsPool.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-elements-pool\">\n    <Search\n      class=\"c-elements-pool__search\"\n      :value=\"currentSearch\"\n      @input=\"applySearch\"\n      @clear=\"applySearch\"\n    />\n    <div class=\"c-elements-pool__elements\">\n      <ul\n        v-if=\"elements.length > 0\"\n        id=\"inspector-elements-tree\"\n        class=\"c-tree c-elements-pool__tree\"\n      >\n        <ElementItem\n          v-for=\"(element, index) in elements\"\n          :key=\"element.identifier.key\"\n          :index=\"index\"\n          :element-object=\"element\"\n          :allow-drag=\"isEditing\"\n          :allow-drop=\"allowDrop\"\n          @dragstart-custom=\"moveFrom(index)\"\n          @drop-custom=\"moveTo(index)\"\n        >\n          <template #content=\"slotProps\">\n            <slot name=\"content\" :index=\"index\" v-bind=\"slotProps\"></slot>\n          </template>\n        </ElementItem>\n        <li class=\"js-last-place\" @drop=\"moveToIndex(elements.length)\"></li>\n      </ul>\n      <div v-if=\"elements.length === 0\">No contained elements</div>\n      <slot name=\"custom\"></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport useIsEditing from 'utils/vue/useIsEditing.js';\nimport { inject } from 'vue';\n\nimport Search from '../../../ui/components/SearchComponent.vue';\nimport ElementItem from './ElementItem.vue';\n\nexport default {\n  components: {\n    Search,\n    ElementItem\n  },\n  setup() {\n    const openmct = inject('openmct');\n    const domainObject = inject('domainObject');\n    const { isEditing } = useIsEditing(openmct);\n\n    return {\n      openmct,\n      domainObject,\n      isEditing\n    };\n  },\n  data() {\n    return {\n      elements: [],\n      currentSearch: '',\n      selection: [],\n      contextClickTracker: {},\n      allowDrop: false\n    };\n  },\n  watch: {\n    isEditing() {\n      this.showSelection(this.openmct.selection.get());\n    }\n  },\n  mounted() {\n    let selection = this.openmct.selection.get();\n    if (selection && selection.length > 0) {\n      this.showSelection(selection);\n    }\n\n    this.openmct.selection.on('change', this.showSelection);\n  },\n  unmounted() {\n    this.openmct.selection.off('change', this.showSelection);\n\n    if (this.compositionUnlistener) {\n      this.compositionUnlistener();\n    }\n  },\n  methods: {\n    showSelection(selection) {\n      if (_.isEqual(this.selection, selection)) {\n        return;\n      }\n\n      this.selection = selection;\n      this.elements = [];\n      this.elementsCache = {};\n      this.listeners = [];\n\n      if (this.compositionUnlistener) {\n        this.compositionUnlistener();\n      }\n\n      if (this.domainObject) {\n        this.composition = this.openmct.composition.get(this.domainObject);\n\n        if (this.composition) {\n          this.composition.load();\n\n          this.composition.on('add', this.addElement);\n          this.composition.on('remove', this.removeElement);\n          this.composition.on('reorder', this.reorderElements);\n\n          this.compositionUnlistener = () => {\n            this.composition.off('add', this.addElement);\n            this.composition.off('remove', this.removeElement);\n            this.composition.off('reorder', this.reorderElements);\n            delete this.compositionUnlistener;\n          };\n        }\n      }\n    },\n    addElement(element) {\n      let keyString = this.openmct.objects.makeKeyString(element.identifier);\n      this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));\n      this.applySearch(this.currentSearch);\n    },\n    reorderElements() {\n      this.applySearch(this.currentSearch);\n    },\n    removeElement(identifier) {\n      let keyString = this.openmct.objects.makeKeyString(identifier);\n      delete this.elementsCache[keyString];\n      this.applySearch(this.currentSearch);\n    },\n    applySearch(input) {\n      this.currentSearch = input;\n      this.elements = this.domainObject.composition\n        .map((id) => this.elementsCache[this.openmct.objects.makeKeyString(id)])\n        .filter((element) => {\n          return (\n            element !== undefined && element.name.toLowerCase().search(this.currentSearch) !== -1\n          );\n        });\n    },\n    moveTo(moveToIndex) {\n      if (this.allowDrop) {\n        this.composition.reorder(this.moveFromIndex, moveToIndex);\n        this.allowDrop = false;\n      }\n    },\n    moveFrom(index) {\n      this.allowDrop = true;\n      this.moveFromIndex = index;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/ElementsViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport ElementsPool from './ElementsPool.vue';\n\nconst CUSTOM_ELEMENTS_VIEW_PROVIDER_TYPES = ['time-strip', 'telemetry.plot.overlay'];\n\nexport default function ElementsViewProvider(openmct) {\n  return {\n    key: 'elementsView',\n    name: 'Elements',\n    canView: function (selection) {\n      const hasValidSelection = selection?.length;\n      const isFolder = selection?.[0]?.[0]?.context?.item?.type === 'folder';\n      const type = selection?.[0]?.[0]?.context?.item?.type;\n\n      const hasCustomElementsViewProvider = CUSTOM_ELEMENTS_VIEW_PROVIDER_TYPES.includes(type);\n\n      return hasValidSelection && !hasCustomElementsViewProvider && !isFolder;\n    },\n    view: function (selection) {\n      let _destroy = null;\n      const domainObject = selection?.[0]?.[0]?.context?.item;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                ElementsPool\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: `<ElementsPool />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        showTab: function (isEditing) {\n          const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));\n\n          return hasComposition;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/PlotElementsPool.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-elements-pool is-object-type-telemetry-plot-overlay\">\n    <Search\n      class=\"c-elements-pool__search\"\n      :value=\"currentSearch\"\n      @input=\"applySearch\"\n      @clear=\"applySearch\"\n    />\n    <div class=\"c-elements-pool__elements\">\n      <ul\n        v-show=\"hasElements\"\n        id=\"inspector-elements-tree\"\n        class=\"c-tree c-elements-pool__tree js-elements-pool__tree\"\n      >\n        <div class=\"c-elements-pool__instructions\">\n          Select and drag an element to move it into a different axis.\n        </div>\n        <ElementItemGroup\n          v-for=\"(yAxis, index) in yAxes\"\n          :key=\"`element-group-yaxis-${yAxis.id}`\"\n          :parent-object=\"parentObject\"\n          :allow-drop=\"allowDrop\"\n          :label=\"`Y Axis ${yAxis.id}`\"\n          @drop-group=\"moveTo($event, 0, yAxis.id)\"\n        >\n          <li class=\"js-first-place\" @drop=\"moveTo($event, 0, yAxis.id)\"></li>\n          <ElementItem\n            v-for=\"(element, elemIndex) in yAxis.elements\"\n            :key=\"element.identifier.key\"\n            :index=\"elemIndex\"\n            :element-object=\"element\"\n            :parent-object=\"parentObject\"\n            :allow-drop=\"allowDrop\"\n            @dragstart-custom=\"moveFrom($event, yAxis.id)\"\n            @drop-custom=\"moveTo($event, index, yAxis.id)\"\n          />\n          <li\n            v-if=\"yAxis.elements.length > 0\"\n            class=\"js-last-place\"\n            @drop=\"moveTo($event, yAxis.elements.length, yAxis.id)\"\n          ></li>\n        </ElementItemGroup>\n      </ul>\n      <div v-show=\"!hasElements\">No contained elements</div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport Search from '../../../ui/components/SearchComponent.vue';\nimport configStore from '../../plot/configuration/ConfigStore.js';\nimport ElementItem from './ElementItem.vue';\nimport ElementItemGroup from './ElementItemGroup.vue';\n\nconst Y_AXIS_1 = 1;\n\nexport default {\n  components: {\n    Search,\n    ElementItemGroup,\n    ElementItem\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      yAxes: [],\n      isEditing: this.openmct.editor.isEditing(),\n      parentObject: undefined,\n      currentSearch: '',\n      selection: [],\n      contextClickTracker: {},\n      allowDrop: false\n    };\n  },\n  computed: {\n    hasElements() {\n      for (const yAxis of this.yAxes) {\n        if (yAxis.elements.length > 0) {\n          return true;\n        }\n      }\n\n      return false;\n    }\n  },\n  mounted() {\n    const selection = this.openmct.selection.get();\n    if (selection && selection.length > 0) {\n      this.showSelection(selection);\n    }\n\n    this.openmct.selection.on('change', this.showSelection);\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n    this.openmct.selection.off('change', this.showSelection);\n\n    this.unlistenComposition();\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n      this.showSelection(this.openmct.selection.get());\n    },\n    showSelection(selection) {\n      if (_.isEqual(this.selection, selection)) {\n        return;\n      }\n\n      this.selection = selection;\n      this.elementsCache = {};\n      this.listeners = [];\n      this.parentObject = selection && selection[0] && selection[0][0].context.item;\n\n      this.unlistenComposition();\n\n      if (this.parentObject && this.parentObject.type === 'telemetry.plot.overlay') {\n        this.setYAxisIds();\n        this.composition = this.openmct.composition.get(this.parentObject);\n\n        if (this.composition) {\n          this.composition.load();\n          this.registerCompositionListeners();\n        }\n      }\n    },\n    unlistenComposition() {\n      if (this.compositionUnlistener) {\n        this.compositionUnlistener();\n      }\n    },\n    registerCompositionListeners() {\n      this.composition.on('add', this.addElement);\n      this.composition.on('remove', this.removeElement);\n      this.composition.on('reorder', this.reorderElements);\n\n      this.compositionUnlistener = () => {\n        this.composition.off('add', this.addElement);\n        this.composition.off('remove', this.removeElement);\n        this.composition.off('reorder', this.reorderElements);\n        delete this.compositionUnlistener;\n      };\n    },\n    setYAxisIds() {\n      const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);\n      this.config = configStore.get(configId);\n      // Clear the yAxes array and repopulate it with the current YAxis elements\n      this.yAxes.splice(0);\n      this.yAxes.push({\n        id: this.config.yAxis.id,\n        elements: this.parentObject.configuration.series.filter(\n          (series) => series.yAxisId === this.config.yAxis.id\n        )\n      });\n      if (this.config.additionalYAxes) {\n        this.config.additionalYAxes.forEach((yAxis) => {\n          this.yAxes.push({\n            id: yAxis.id,\n            elements: this.parentObject.configuration.series.filter(\n              (series) => series.yAxisId === yAxis.id\n            )\n          });\n        });\n      }\n    },\n    addElement(element) {\n      // Get the index of the corresponding element in the series list\n      const seriesIndex = this.parentObject.configuration.series.findIndex((series) =>\n        this.openmct.objects.areIdsEqual(series.identifier, element.identifier)\n      );\n      const keyString = this.openmct.objects.makeKeyString(element.identifier);\n\n      const wasDraggedOntoPlot =\n        this.parentObject.configuration.series[seriesIndex].yAxisId === undefined;\n      const yAxisId = wasDraggedOntoPlot\n        ? Y_AXIS_1\n        : this.parentObject.configuration.series[seriesIndex].yAxisId;\n\n      if (wasDraggedOntoPlot) {\n        const insertIndex = this.yAxes[0].elements.length;\n        // Insert the element at the end of the first YAxis bucket\n        this.composition.reorder(seriesIndex, insertIndex);\n      }\n\n      // Store the element in the cache and set its yAxisId\n      this.elementsCache[keyString] = element;\n      if (this.elementsCache[keyString].yAxisId !== yAxisId) {\n        // Mutate the YAxisId on the domainObject itself\n        this.updateCacheAndMutate(element, yAxisId);\n      }\n\n      this.applySearch(this.currentSearch);\n    },\n    reorderElements() {\n      this.applySearch(this.currentSearch);\n    },\n    removeElement(identifier) {\n      const keyString = this.openmct.objects.makeKeyString(identifier);\n      delete this.elementsCache[keyString];\n      this.applySearch(this.currentSearch);\n    },\n    applySearch(input) {\n      this.currentSearch = input;\n      this.yAxes.forEach((yAxis) => {\n        yAxis.elements = this.filterForSearchAndAxis(input, yAxis.id);\n      });\n    },\n    filterForSearchAndAxis(input, yAxisId) {\n      return this.parentObject.composition\n        .map((id) => this.elementsCache[this.openmct.objects.makeKeyString(id)])\n        .filter((element) => {\n          return (\n            element !== undefined &&\n            element.name.toLowerCase().search(input) !== -1 &&\n            element.yAxisId === yAxisId\n          );\n        });\n    },\n    moveFrom(elementIndex, groupIndex) {\n      this.allowDrop = true;\n      this.moveFromIndex = elementIndex;\n      this.moveFromYAxisId = groupIndex;\n    },\n    moveTo(event, moveToIndex, moveToYAxisId) {\n      // FIXME: If the user starts the drag by clicking outside of the <object-label/> element,\n      // domain object information will not be set on the dataTransfer data. To prevent errors,\n      // we simply short-circuit here if the data is not set.\n      const serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object');\n      if (!serializedDomainObject) {\n        return;\n      }\n\n      const domainObject = JSON.parse(serializedDomainObject);\n      this.updateCacheAndMutate(domainObject, moveToYAxisId);\n\n      const moveFromIndex = this.moveFromIndex;\n\n      this.moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId);\n    },\n    updateCacheAndMutate(domainObject, yAxisId) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      const index = this.parentObject.configuration.series.findIndex(\n        (series) => series.identifier.key === domainObject.identifier.key\n      );\n\n      // Handle the case of dragging an element directly into the Elements Pool\n      if (!this.elementsCache[keyString]) {\n        // Update the series list locally so our CompositionAdd handler can\n        // take care of the rest.\n        this.parentObject.configuration.series.push({\n          identifier: domainObject.identifier,\n          yAxisId\n        });\n        this.composition.add(domainObject);\n        this.elementsCache[keyString] = domainObject;\n      }\n\n      this.elementsCache[keyString].yAxisId = yAxisId;\n      const shouldMutate = this.parentObject.configuration.series?.[index]?.yAxisId !== yAxisId;\n      if (shouldMutate) {\n        this.openmct.objects.mutate(\n          this.parentObject,\n          `configuration.series[${index}].yAxisId`,\n          yAxisId\n        );\n      }\n    },\n    moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId) {\n      if (!this.allowDrop) {\n        return;\n      }\n\n      // Find the corresponding indexes of the from/to yAxes in the yAxes list\n      const moveFromYAxisIndex = this.yAxes.findIndex((yAxis) => yAxis.id === this.moveFromYAxisId);\n      const moveToYAxisIndex = this.yAxes.findIndex((yAxis) => yAxis.id === moveToYAxisId);\n\n      // Calculate the actual indexes of the elements in the composition array\n      // based on which bucket and index they are being moved from/to.\n      // Then, trigger a composition reorder.\n      for (let yAxisId = 0; yAxisId < moveFromYAxisIndex; yAxisId++) {\n        const lesserYAxisBucketLength = this.yAxes[yAxisId].elements.length;\n        // Add the lengths of preceding buckets to calculate the actual 'from' index\n        moveFromIndex = moveFromIndex + lesserYAxisBucketLength;\n      }\n\n      for (let yAxisId = 0; yAxisId < moveToYAxisIndex; yAxisId++) {\n        const greaterYAxisBucketLength = this.yAxes[yAxisId].elements.length;\n        // Add the lengths of subsequent buckets to calculate the actual 'to' index\n        moveToIndex = moveToIndex + greaterYAxisBucketLength;\n      }\n\n      // Adjust the index by 1 if we're moving from one bucket to another\n      if (this.moveFromYAxisId !== moveToYAxisId && moveToIndex > 0) {\n        moveToIndex--;\n      }\n\n      // Reorder the composition array according to the calculated indexes\n      this.composition.reorder(moveFromIndex, moveToIndex);\n\n      this.allowDrop = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/PlotElementsViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport PlotElementsPool from './PlotElementsPool.vue';\n\nexport default function PlotElementsViewProvider(openmct) {\n  return {\n    key: 'plotElementsView',\n    name: 'Elements',\n    canView: function (selection) {\n      return selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      const domainObject = selection?.[0]?.[0]?.context?.item;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlotElementsPool\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: `<PlotElementsPool />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        showTab: function (isEditing) {\n          const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));\n\n          return hasComposition && isEditing;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/elements/elements.scss",
    "content": ".c-elements-pool {\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  flex: 1 1 auto !important;\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &.is-object-type-telemetry-plot-overlay {\n    .c-grippy {\n      display: none;\n    }\n    .c-object-label {\n      &:before {\n        // Grippy\n        content: '';\n        @include grippy($colorItemTreeVC, $dir: 'Y');\n        $d: 9px;\n        width: $d;\n        height: $d;\n        display: block;\n        margin-right: $interiorMargin;\n      }\n    }\n  }\n\n  &__item {\n    &.is-alias {\n      // Object is an alias to an original.\n      [class*='__type-icon'] {\n        @include isAlias();\n      }\n    }\n  }\n\n  &__search {\n    flex: 0 0 auto;\n  }\n\n  &__group {\n    flex: 1 1 auto;\n    margin-top: $interiorMarginLg;\n  }\n\n  &__elements {\n    flex: 1 1 auto;\n    overflow: auto;\n  }\n\n  &__instructions {\n    display: flex;\n    font-style: italic;\n  }\n\n  .c-grippy {\n    $d: 9px;\n    flex: 0 0 auto;\n    margin-right: $interiorMarginSm;\n    transform: translateY(-2px);\n    width: $d;\n    height: $d;\n  }\n\n  &.is-context-clicked {\n    box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;\n  }\n\n  .hover {\n    background-color: $colorItemTreeSelectedBg;\n  }\n}\n\n.js-last-place {\n  height: 10px;\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport AnnotationsViewProvider from './annotations/AnnotationsViewProvider.js';\nimport ElementsViewProvider from './elements/ElementsViewProvider.js';\nimport PlotElementsViewProvider from './elements/PlotElementsViewProvider.js';\nimport PropertiesViewProvider from './properties/PropertiesViewProvider.js';\nimport StylesInspectorViewProvider from './styles/StylesInspectorViewProvider.js';\n\nexport default function InspectorViewsPlugin() {\n  return function install(openmct) {\n    openmct.inspectorViews.addProvider(new PropertiesViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new ElementsViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new PlotElementsViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new StylesInspectorViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new AnnotationsViewProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/properties/DetailText.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <li class=\"c-inspect-properties__row\" :aria-label=\"`${detail.name} inspector properties`\">\n    <div class=\"c-inspect-properties__label\" aria-label=\"inspector property name\">\n      {{ detail.name }}\n    </div>\n    <div class=\"c-inspect-properties__value\" aria-label=\"inspector property value\">\n      {{ detail.value }}\n    </div>\n  </li>\n</template>\n\n<script>\nexport default {\n  props: {\n    detail: {\n      type: Object,\n      required: true\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/properties/LocationComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspect-properties c-inspect-properties--location\">\n    <div class=\"c-inspect-properties__header\" title=\"The location of this linked object.\">\n      Original Location\n    </div>\n    <ul class=\"c-inspect-properties__section\">\n      <li class=\"c-inspect-properties__row\">\n        <ul class=\"c-inspect-properties__value c-location\">\n          <li\n            v-for=\"pathObject in orderedPathBreadCrumb\"\n            :key=\"pathObject.key\"\n            class=\"c-location__item\"\n          >\n            <ObjectLabel\n              :domain-object=\"pathObject.domainObject\"\n              :object-path=\"pathObject.objectPath\"\n            />\n          </li>\n        </ul>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport ObjectLabel from '../../../ui/components/ObjectLabel.vue';\n\nexport default {\n  components: {\n    ObjectLabel\n  },\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      default: undefined\n    },\n    parentDomainObject: {\n      type: Object,\n      default: undefined\n    }\n  },\n  data() {\n    return {\n      pathBreadCrumb: []\n    };\n  },\n  computed: {\n    orderedPathBreadCrumb() {\n      return this.pathBreadCrumb.slice().reverse();\n    }\n  },\n  async mounted() {\n    this.nameChangeListeners = {};\n    await this.createPathBreadCrumb();\n  },\n  unmounted() {\n    Object.values(this.nameChangeListeners).forEach((unlisten) => {\n      unlisten();\n    });\n  },\n  methods: {\n    updateObjectPathName(keyString, newName) {\n      this.pathBreadCrumb = this.pathBreadCrumb.map((pathObject) => {\n        if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {\n          return {\n            ...pathObject,\n            domainObject: { ...pathObject.domainObject, name: newName }\n          };\n        }\n        return pathObject;\n      });\n    },\n    removeNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString]();\n        delete this.nameChangeListeners[keyString];\n      }\n    },\n    addNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (!this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString] = this.openmct.objects.observe(\n          domainObject,\n          'name',\n          this.updateObjectPathName.bind(this, keyString)\n        );\n      }\n    },\n    async createPathBreadCrumb() {\n      if (!this.domainObject && this.parentDomainObject) {\n        this.setPathBreadCrumb([this.parentDomainObject]);\n      } else {\n        const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n        const originalPath = await this.openmct.objects.getOriginalPath(keyString);\n        const originalPathWithoutSelf = originalPath.slice(1, -1);\n\n        this.setPathBreadCrumb(originalPathWithoutSelf);\n      }\n    },\n    setPathBreadCrumb(path) {\n      const pathBreadCrumb = path.map((domainObject, index, pathArray) => {\n        const key = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n        return {\n          domainObject,\n          key,\n          objectPath: pathArray.slice(index)\n        };\n      });\n\n      this.pathBreadCrumb.forEach((pathObject) => {\n        this.removeNameListenerFor(pathObject.domainObject);\n      });\n\n      this.pathBreadCrumb = pathBreadCrumb;\n\n      this.pathBreadCrumb.forEach((pathObject) => {\n        this.addNameListenerFor(pathObject.domainObject);\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/properties/PropertiesComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div>\n    <div\n      class=\"c-inspector__properties c-inspect-properties\"\n      aria-label=\"Inspector Properties Details\"\n    >\n      <div class=\"c-inspect-properties__header\">Details</div>\n      <ul v-if=\"hasDetails\" class=\"c-inspect-properties__section\">\n        <Component\n          :is=\"getComponent(detail)\"\n          v-for=\"detail in details\"\n          :key=\"detail.name\"\n          :detail=\"detail\"\n        />\n      </ul>\n      <div v-else class=\"c-inspect-properties__row--span-all\">\n        {{ noDetailsMessage }}\n      </div>\n    </div>\n\n    <Location\n      v-if=\"hasLocation\"\n      :domain-object=\"domainObject\"\n      :parent-domain-object=\"parentDomainObject\"\n    />\n  </div>\n</template>\n\n<script>\nimport Moment from 'moment';\n\nimport DetailText from './DetailText.vue';\nimport Location from './LocationComponent.vue';\n\nexport default {\n  components: {\n    DetailText,\n    Location\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      selection: []\n    };\n  },\n  computed: {\n    details() {\n      return this.customDetails ?? this.domainObjectDetails;\n    },\n    customDetails() {\n      return this.context?.details;\n    },\n    domainObject() {\n      return this.context?.item;\n    },\n    parentDomainObject() {\n      return this.selection?.[0]?.[1]?.context?.item;\n    },\n    type() {\n      if (this.domainObject === undefined) {\n        return;\n      }\n\n      return this.openmct.types.get(this.domainObject.type);\n    },\n    domainObjectDetails() {\n      if (this.domainObject === undefined) {\n        return;\n      }\n\n      const UNKNOWN_USER = 'Unknown';\n      const title = this.domainObject.name;\n      const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;\n      const createdTimestamp = this.domainObject.created;\n      const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;\n      const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;\n      const locked = this.domainObject.locked;\n      const lockedBy = this.domainObject.lockedBy ?? UNKNOWN_USER;\n      const modifiedTimestamp = this.domainObject.modified\n        ? this.domainObject.modified\n        : this.domainObject.created;\n      const notes = this.domainObject.notes;\n      const version = this.domainObject.version;\n\n      const details = [\n        {\n          name: 'Title',\n          value: title\n        },\n        {\n          name: 'Type',\n          value: typeName\n        },\n        {\n          name: 'Created By',\n          value: createdBy\n        },\n        {\n          name: 'Modified By',\n          value: modifiedBy\n        }\n      ];\n\n      if (notes) {\n        details.push({\n          name: 'Notes',\n          value: notes\n        });\n      }\n\n      if (createdTimestamp !== undefined) {\n        const formattedCreatedTimestamp =\n          Moment.utc(createdTimestamp).format('YYYY-MM-DD[\\n]HH:mm:ss') + ' UTC';\n\n        details.push({\n          name: 'Created',\n          value: formattedCreatedTimestamp\n        });\n      }\n\n      if (modifiedTimestamp !== undefined) {\n        const formattedModifiedTimestamp =\n          Moment.utc(modifiedTimestamp).format('YYYY-MM-DD[\\n]HH:mm:ss') + ' UTC';\n\n        details.push({\n          name: 'Modified',\n          value: formattedModifiedTimestamp\n        });\n      }\n\n      if (locked === true) {\n        details.push({\n          name: 'Locked By',\n          value: lockedBy\n        });\n      }\n\n      if (version) {\n        details.push({\n          name: 'Version',\n          value: version\n        });\n      }\n\n      return [...details, ...this.typeProperties];\n    },\n    context() {\n      return this.selection?.[0]?.[0]?.context;\n    },\n    hasDetails() {\n      return Boolean(this.details?.length && !this.multiSelection);\n    },\n    multiSelection() {\n      return this.selection && this.selection.length > 1;\n    },\n    noDetailsMessage() {\n      return this.multiSelection\n        ? 'No properties to display for multiple items'\n        : 'No properties to display for this item';\n    },\n    typeProperties() {\n      if (!this.type) {\n        return [];\n      }\n\n      let definition = this.type.definition;\n      if (!definition.form || definition.form.length === 0) {\n        return [];\n      }\n\n      return definition.form\n        .filter((field) => !field.hideFromInspector)\n        .map((field) => {\n          let path = field.property;\n          if (typeof path === 'string') {\n            path = [path];\n          }\n\n          if (field.control === 'file-input') {\n            path = [...path, 'name'];\n          }\n\n          return {\n            name: field.name,\n            path\n          };\n        })\n        .filter((field) => Array.isArray(field.path))\n        .map((field) => {\n          return {\n            name: field.name,\n            value: field.path.reduce((object, key) => {\n              if (object === undefined) {\n                return object;\n              }\n\n              return object[key];\n            }, this.domainObject)\n          };\n        });\n    },\n    hasLocation() {\n      const domainObject = this.selection?.[0]?.[0]?.context?.item;\n      const isRootObject = domainObject?.location === 'ROOT';\n      const hasSingleSelection = this.selection?.length === 1;\n\n      return hasSingleSelection && !isRootObject;\n    }\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.updateSelection);\n    this.updateSelection(this.openmct.selection.get());\n  },\n  beforeUnmount() {\n    this.openmct.selection.off('change', this.updateSelection);\n  },\n  methods: {\n    getComponent(detail) {\n      const component = detail.component ? detail.component : 'text';\n\n      return `detail-${component}`;\n    },\n    updateSelection(selection) {\n      this.removeListener();\n      this.selection.splice(0, this.selection.length, ...selection);\n      if (this.domainObject) {\n        this.addListener();\n      }\n    },\n    removeListener() {\n      if (this.nameListener) {\n        this.nameListener();\n        this.nameListener = null;\n      }\n    },\n    addListener() {\n      this.nameListener = this.openmct.objects.observe(this.context?.item, 'name', (newValue) => {\n        this.context.item = { ...this.context?.item, name: newValue };\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/properties/PropertiesViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Properties from './PropertiesComponent.vue';\n\nexport default function PropertiesViewProvider(openmct) {\n  return {\n    key: 'propertiesView',\n    name: 'Properties',\n    glyph: 'icon-info',\n    canView: function (selection) {\n      const domainObject = selection?.[0]?.[0]?.context?.item;\n\n      return domainObject && selection.length > 0;\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Properties\n              },\n              provide: {\n                openmct\n              },\n              template: `<Properties />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.LOW : openmct.priority.HIGH;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/properties/location.scss",
    "content": ".c-path,\n.c-location {\n  // Path is two or more items, not clickable\n  // Location used in Inspector and search results, is clickable\n  display: flex;\n\n  &__item {\n    display: flex;\n    font-size: 11px;\n    align-items: center;\n    min-width: 0;\n\n    &:not(:last-child) {\n      &:after {\n        // Right-pointing arrow\n        color: $colorBodyFgSubtle;\n        content: $glyph-icon-arrow-right;\n        font-family: symbolsfont;\n        font-size: 0.7em;\n        margin-left: $interiorMarginSm;\n      }\n    }\n  }\n}\n\n.c-location {\n  flex-wrap: wrap;\n\n  &__item {\n    $m: 1px;\n    cursor: pointer;\n    margin: 0 $m $m 0;\n\n    .c-object-label {\n      border-radius: $smallCr;\n      padding: 2px 3px;\n\n      &__type-icon {\n        width: auto;\n        font-size: 1em;\n        min-width: auto;\n      }\n\n      @include hover() {\n        background: $colorItemTreeHoverBg;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/FontStyleEditor.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-toolbar\">\n    <div ref=\"fontSizeMenu\" class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        class=\"c-icon-button c-button--menu icon-font-size\"\n        aria-label=\"Set Font Size\"\n        @click.prevent.stop=\"showFontSizeMenu\"\n      >\n        <span class=\"c-button__label\">{{ fontSizeLabel }}</span>\n      </button>\n    </div>\n    <div ref=\"fontMenu\" class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        class=\"c-icon-button c-button--menu icon-font\"\n        aria-label=\"Set Font Type\"\n        @click.prevent.stop=\"showFontMenu\"\n      >\n        <span class=\"c-button__label\">{{ fontTypeLabel }}</span>\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { FONT_SIZES, FONTS } from './constants.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    fontStyle: {\n      type: Object,\n      required: true,\n      default: () => {\n        return {};\n      }\n    }\n  },\n  emits: ['set-font-property'],\n  computed: {\n    fontTypeLabel() {\n      const fontType = FONTS.find((f) => f.value === this.fontStyle.font);\n      if (!fontType) {\n        return '??';\n      }\n\n      return fontType.name || fontType.value || FONTS[0].name;\n    },\n    fontSizeLabel() {\n      const fontSize = FONT_SIZES.find((f) => f.value === this.fontStyle.fontSize);\n      if (!fontSize) {\n        return '??';\n      }\n\n      return fontSize.name || fontSize.value || FONT_SIZES[0].name;\n    },\n    fontMenu() {\n      return FONTS.map((font) => {\n        return {\n          cssClass: font.cssClass || '',\n          name: font.name,\n          description: font.name,\n          onItemClicked: () => this.setFont(font.value)\n        };\n      });\n    },\n    fontSizeMenu() {\n      return FONT_SIZES.map((fontSize) => {\n        return {\n          cssClass: fontSize.cssClass || '',\n          name: fontSize.name,\n          description: fontSize.name,\n          onItemClicked: () => this.setFontSize(fontSize.value)\n        };\n      });\n    }\n  },\n  methods: {\n    setFont(font) {\n      this.$emit('set-font-property', { font });\n    },\n    setFontSize(fontSize) {\n      this.$emit('set-font-property', { fontSize });\n    },\n    showFontMenu() {\n      const elementBoundingClientRect = this.$refs.fontMenu.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.bottom;\n\n      this.openmct.menus.showMenu(x, y, this.fontMenu);\n    },\n    showFontSizeMenu() {\n      const elementBoundingClientRect = this.$refs.fontSizeMenu.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.bottom;\n\n      this.openmct.menus.showMenu(x, y, this.fontSizeMenu);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/SavedStyleSelector.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div>\n    <div class=\"c-style c-style--saved has-local-controls c-toolbar\">\n      <div\n        class=\"c-style__controls\"\n        :aria-label=\"description\"\n        :title=\"description\"\n        @click=\"selectStyle()\"\n      >\n        <div class=\"c-style-thumb\" :style=\"thumbStyle\">\n          <span\n            class=\"c-style-thumb__text u-style-receiver js-style-receiver\"\n            :class=\"{ 'hide-nice': !hasProperty(savedStyle.color) }\"\n            :data-font=\"savedStyle.font\"\n          >\n            {{ thumbLabel }}\n          </span>\n        </div>\n        <div\n          class=\"c-icon-button c-icon-button--disabled c-icon-button--swatched icon-line-horz\"\n          title=\"Border color\"\n        >\n          <div\n            class=\"c-swatch\"\n            :style=\"{\n              background: borderColor\n            }\"\n          ></div>\n        </div>\n        <div\n          class=\"c-icon-button c-icon-button--disabled c-icon-button--swatched icon-paint-bucket\"\n          title=\"Background color\"\n        >\n          <div class=\"c-swatch\" :style=\"{ background: savedStyle.backgroundColor }\"></div>\n        </div>\n        <div\n          class=\"c-icon-button c-icon-button--disabled c-icon-button--swatched icon-font\"\n          title=\"Text color\"\n        >\n          <div class=\"c-swatch\" :style=\"{ background: savedStyle.color }\"></div>\n        </div>\n      </div>\n\n      <div v-if=\"canDeleteStyle\" class=\"c-style__button-delete c-local-controls--show-on-hover\">\n        <div\n          class=\"c-icon-button icon-trash\"\n          title=\"Delete this saved style\"\n          @click.stop=\"deleteStyle()\"\n        ></div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'SavedStyleSelector',\n  inject: ['openmct', 'stylesManager'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      required: true\n    },\n    savedStyle: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['delete-style'],\n  data() {\n    return {\n      expanded: false\n    };\n  },\n  computed: {\n    borderColor() {\n      return this.savedStyle.border.substring(this.savedStyle.border.indexOf('#'));\n    },\n    thumbStyle() {\n      return {\n        border: this.savedStyle.border,\n        backgroundColor: this.savedStyle.backgroundColor,\n        color: this.savedStyle.color\n      };\n    },\n    thumbLabel() {\n      return this.savedStyle.fontSize !== 'default' ? `${this.savedStyle.fontSize}px` : 'ABC';\n    },\n    description() {\n      const fill = `Fill: ${this.savedStyle.backgroundColor || 'none'}`;\n      const border = `Border: ${this.savedStyle.border || 'none'}`;\n      const color = `Text Color: ${this.savedStyle.color || 'default'}`;\n      const fontSize = this.savedStyle.fontSize ? `Font Size: ${this.savedStyle.fontSize}` : '';\n      const font = this.savedStyle.font ? `Font Style: ${this.savedStyle.font}` : '';\n\n      // Note: lack of indention in the return string is deliberate, it affects how the text is rendered\n      return `Click to apply this style:\n${fill}\n${border}\n${color}\n${fontSize}\n${font}`;\n    },\n    canDeleteStyle() {\n      return this.isEditing;\n    }\n  },\n  methods: {\n    selectStyle() {\n      if (this.isEditing) {\n        this.stylesManager.select(this.savedStyle);\n      }\n    },\n    deleteStyle() {\n      this.showDeleteStyleDialog()\n        .then(() => {\n          this.$emit('delete-style');\n        })\n        .catch(() => {});\n    },\n    showDeleteStyleDialog(style) {\n      const message = `\n                This will delete this saved style.\n                This action will not effect styling that has already been applied.\n                Do you want to continue?\n            `;\n\n      return new Promise((resolve, reject) => {\n        let dialog = this.openmct.overlays.dialog({\n          title: 'Delete Saved Style',\n          iconClass: 'alert',\n          message: message,\n          buttons: [\n            {\n              label: 'Ok',\n              callback: () => {\n                dialog.dismiss();\n                resolve();\n              }\n            },\n            {\n              label: 'Cancel',\n              callback: () => {\n                dialog.dismiss();\n                reject();\n              }\n            }\n          ]\n        });\n      });\n    },\n    hasProperty(property) {\n      return property !== undefined;\n    },\n    toggleExpanded() {\n      this.expanded = !this.expanded;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/SavedStylesInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"u-contents\"></div>\n</template>\n\n<script>\nimport mount from 'utils/mount';\n\nimport SavedStylesView from './SavedStylesView.vue';\n\nexport default {\n  inject: ['openmct', 'stylesManager'],\n  data() {\n    return {\n      selection: [],\n      destroy: null\n    };\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.updateSelection);\n    this.updateSelection(this.openmct.selection.get());\n  },\n  unmounted() {\n    this.openmct.selection.off('change', this.updateSelection);\n    if (this.destroy) {\n      this.destroy();\n      this.$el.innerHTML = '';\n    }\n  },\n  methods: {\n    updateSelection(selection) {\n      if (selection.length > 0 && selection[0].length > 0) {\n        if (this.destroy) {\n          this.destroy();\n          this.$el.innerHTML = '';\n        }\n\n        let viewContainer = document.createElement('div');\n        this.$el.append(viewContainer);\n        const { destroy } = mount(\n          {\n            el: viewContainer,\n            components: {\n              SavedStylesView\n            },\n            provide: {\n              openmct: this.openmct,\n              selection: selection,\n              stylesManager: this.stylesManager\n            },\n            template: '<saved-styles-view />'\n          },\n          {\n            app: this.openmct.app,\n            element: viewContainer\n          }\n        );\n        this.destroy = destroy;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/SavedStylesView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__saved-styles c-inspect-styles\">\n    <div class=\"c-inspect-styles__content\">\n      <div class=\"c-inspect-styles__saved-styles\">\n        <SavedStyleSelector\n          v-for=\"(savedStyle, index) in savedStyles\"\n          :key=\"index\"\n          class=\"c-inspect-styles__saved-style\"\n          :is-editing=\"isEditing\"\n          :saved-style=\"savedStyle\"\n          @delete-style=\"deleteStyle(index)\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport SavedStyleSelector from './SavedStyleSelector.vue';\n\nexport default {\n  name: 'SavedStylesView',\n  components: {\n    SavedStyleSelector\n  },\n  inject: ['openmct', 'selection', 'stylesManager'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing(),\n      savedStyles: undefined\n    };\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setIsEditing);\n    this.stylesManager.on('stylesUpdated', this.setStyles);\n    this.stylesManager.on('limitReached', this.showLimitReachedDialog);\n    this.stylesManager.on('persistError', this.showPersistErrorDialog);\n\n    this.loadStyles();\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.setIsEditing);\n    this.stylesManager.off('stylesUpdated', this.setStyles);\n    this.stylesManager.off('limitReached', this.showLimitReachedDialog);\n    this.stylesManager.off('persistError', this.showPersistErrorDialog);\n  },\n  methods: {\n    setIsEditing(isEditing) {\n      this.isEditing = isEditing;\n    },\n    loadStyles() {\n      const styles = this.stylesManager.load();\n\n      this.setStyles(styles);\n    },\n    setStyles(styles) {\n      this.savedStyles = styles;\n    },\n    showLimitReachedDialog(limit) {\n      const message = `\n                You have reached the limit on the number of saved styles.\n                Please delete one or more saved styles and try again.\n            `;\n\n      let dialog = this.openmct.overlays.dialog({\n        title: 'Saved Styles Limit',\n        iconClass: 'alert',\n        message: message,\n        buttons: [\n          {\n            label: 'Ok',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    showPersistErrorDialog() {\n      const message = `\n                Problem encountered saving styles.\n                Try again or delete one or more styles before trying again.\n            `;\n      let dialog = this.openmct.overlays.dialog({\n        title: 'Error Saving Style',\n        iconClass: 'error',\n        message: message,\n        buttons: [\n          {\n            label: 'Ok',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    deleteStyle(index) {\n      this.stylesManager.delete(index);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/StylesInspectorView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <Multipane type=\"vertical\">\n    <Pane class=\"c-inspector__styles\">\n      <div class=\"u-contents\">\n        <StylesView />\n      </div>\n    </Pane>\n    <Pane v-if=\"isEditing\" class=\"c-inspector__saved-styles\" handle=\"before\" label=\"Saved Styles\">\n      <SavedStylesInspectorView />\n    </Pane>\n  </Multipane>\n</template>\n\n<script>\nimport StylesView from '@/plugins/condition/components/inspector/StylesView.vue';\n\nimport Multipane from '../../../ui/layout/MultipaneContainer.vue';\nimport Pane from '../../../ui/layout/PaneContainer.vue';\nimport SavedStylesInspectorView from './SavedStylesInspectorView.vue';\n\nexport default {\n  components: {\n    Multipane,\n    Pane,\n    StylesView,\n    SavedStylesInspectorView\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditMode);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditMode);\n  },\n  methods: {\n    setEditMode(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/StylesInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport StylesInspectorView from './StylesInspectorView.vue';\nimport stylesManager from './StylesManager.js';\n\nconst NON_STYLABLE_TYPES = [\n  'clock',\n  'conditionSet',\n  'eventGenerator',\n  'eventGeneratorWithAcknowledge',\n  'example.imagery',\n  'folder',\n  'gantt-chart',\n  'generator',\n  'hyperlink',\n  'notebook',\n  'restricted-notebook',\n  'summary-widget',\n  'time-strip',\n  'timelist',\n  'timer',\n  'webPage'\n];\n\nfunction isLayoutObject(selection, objectType) {\n  //we allow conditionSets to be styled if they're part of a layout\n  return (\n    selection.length > 1 &&\n    (objectType === 'conditionSet' || NON_STYLABLE_TYPES.indexOf(objectType) < 0)\n  );\n}\n\nfunction isCreatableObject(object, typeObject) {\n  return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && typeObject.definition.creatable;\n}\n\nexport default function StylesInspectorViewProvider(openmct) {\n  return {\n    key: 'stylesInspectorView',\n    name: 'Styles',\n    glyph: 'icon-paint-bucket',\n    canView: function (selection) {\n      const objectSelection = selection?.[0];\n      const objectContext = objectSelection?.[0]?.context;\n      const domainObject = objectContext?.item;\n      const hasStyles = domainObject?.configuration?.objectStyles;\n      const isFlexibleLayoutContainer =\n        domainObject?.type === 'flexible-layout' && objectContext.type === 'container';\n      const isLayoutItem = objectContext?.layoutItem;\n\n      if ((isLayoutItem || hasStyles) && !isFlexibleLayoutContainer) {\n        return true;\n      }\n\n      if (!domainObject || isFlexibleLayoutContainer) {\n        return false;\n      }\n\n      const typeObject = openmct.types.get(domainObject.type);\n\n      return (\n        isLayoutObject(objectSelection, domainObject.type) ||\n        isCreatableObject(domainObject, typeObject)\n      );\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                StylesInspectorView\n              },\n              provide: {\n                openmct,\n                stylesManager,\n                selection\n              },\n              template: `<StylesInspectorView />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        showTab: function (isEditing) {\n          return true;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/StylesManager.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nconst LOCAL_STORAGE_KEY = 'mct-saved-styles';\nconst LIMIT = 20;\n\n/**\n * @typedef {Object} Style\n * @property {*} property\n */\nclass StylesManager extends EventEmitter {\n  load() {\n    let styles = window.localStorage.getItem(LOCAL_STORAGE_KEY);\n    styles = styles ? JSON.parse(styles) : [];\n\n    return styles;\n  }\n\n  save(style) {\n    const normalizedStyle = this.normalizeStyle(style);\n    const styles = this.load();\n\n    if (!this.isSaveLimitReached(styles)) {\n      styles.unshift(normalizedStyle);\n\n      if (this.persist(styles)) {\n        this.emit('stylesUpdated', styles);\n      }\n    }\n  }\n\n  delete(index) {\n    const styles = this.load();\n    styles.splice(index, 1);\n\n    if (this.persist(styles)) {\n      this.emit('stylesUpdated', styles);\n    }\n  }\n\n  select(style) {\n    this.emit('styleSelected', style);\n  }\n\n  /**\n   * @private\n   */\n  normalizeStyle(style) {\n    const normalizedStyle = this.getBaseStyleObject();\n\n    Object.keys(normalizedStyle).forEach((property) => {\n      const value = style[property];\n      if (value !== undefined) {\n        normalizedStyle[property] = value;\n      }\n    });\n\n    return normalizedStyle;\n  }\n\n  /**\n   * @private\n   */\n  getBaseStyleObject() {\n    return {\n      backgroundColor: '',\n      border: '',\n      color: '',\n      fontSize: 'default',\n      font: 'default'\n    };\n  }\n\n  /**\n   * @private\n   */\n  isSaveLimitReached(styles) {\n    if (styles.length >= LIMIT) {\n      this.emit('limitReached');\n\n      return true;\n    }\n\n    return false;\n  }\n\n  /**\n   * @private\n   */\n  isExistingStyle(style, styles) {\n    return styles.some((existingStyle) => this.isEqual(style, existingStyle));\n  }\n\n  /**\n   * @private\n   */\n  persist(styles) {\n    try {\n      window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(styles));\n\n      return true;\n    } catch (e) {\n      this.emit('persistError');\n    }\n\n    return false;\n  }\n\n  /**\n   * @private\n   */\n  isEqual(style1, style2) {\n    const keys = Object.keys(Object.assign({}, style1, style2));\n    const different = keys.some(\n      (key) =>\n        (!style1[key] && style2[key]) ||\n        (style1[key] && !style2[key]) ||\n        style1[key] !== style2[key]\n    );\n\n    return !different;\n  }\n}\n\nconst stylesManager = new StylesManager();\n// breaks on adding listener later\n// Object.freeze(stylesManager);\n\nexport default stylesManager;\n"
  },
  {
    "path": "src/plugins/inspectorViews/styles/constants.js",
    "content": "export const FONT_SIZES = [\n  {\n    name: 'Default',\n    value: 'default'\n  },\n  {\n    name: '8px',\n    value: '8'\n  },\n  {\n    name: '9px',\n    value: '9'\n  },\n  {\n    name: '10px',\n    value: '10'\n  },\n  {\n    name: '11px',\n    value: '11'\n  },\n  {\n    name: '12px',\n    value: '12'\n  },\n  {\n    name: '13px',\n    value: '13'\n  },\n  {\n    name: '14px',\n    value: '14'\n  },\n  {\n    name: '16px',\n    value: '16'\n  },\n  {\n    name: '18px',\n    value: '18'\n  },\n  {\n    name: '20px',\n    value: '20'\n  },\n  {\n    name: '24px',\n    value: '24'\n  },\n  {\n    name: '28px',\n    value: '28'\n  },\n  {\n    name: '32px',\n    value: '32'\n  },\n  {\n    name: '36px',\n    value: '36'\n  },\n  {\n    name: '42px',\n    value: '42'\n  },\n  {\n    name: '48px',\n    value: '48'\n  },\n  {\n    name: '72px',\n    value: '72'\n  },\n  {\n    name: '96px',\n    value: '96'\n  },\n  {\n    name: '128px',\n    value: '128'\n  }\n];\n\nexport const FONTS = [\n  {\n    name: 'Default',\n    value: 'default'\n  },\n  {\n    name: 'Bold',\n    value: 'default-bold'\n  },\n  {\n    name: 'Narrow',\n    value: 'narrow'\n  },\n  {\n    name: 'Narrow Bold',\n    value: 'narrow-bold'\n  },\n  {\n    name: 'Monospace',\n    value: 'monospace'\n  },\n  {\n    name: 'Monospace Bold',\n    value: 'monospace-bold'\n  }\n];\n"
  },
  {
    "path": "src/plugins/interceptors/missingObjectInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function MissingObjectInterceptor(openmct) {\n  openmct.objects.addGetInterceptor({\n    appliesTo: (identifier, domainObject) => {\n      return true;\n    },\n    invoke: (identifier, object) => {\n      if (object === undefined) {\n        const keyString = openmct.objects.makeKeyString(identifier);\n        openmct.notifications.error(`Failed to retrieve object ${keyString}`, { minimized: true });\n\n        return {\n          identifier,\n          type: 'unknown',\n          name: 'Missing: ' + keyString\n        };\n      }\n\n      return object;\n    },\n    priority: openmct.priority.HIGH\n  });\n}\n"
  },
  {
    "path": "src/plugins/interceptors/plugin.js",
    "content": "import missingObjectInterceptor from './missingObjectInterceptor.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    missingObjectInterceptor(openmct);\n  };\n}\n"
  },
  {
    "path": "src/plugins/interceptors/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport InterceptorPlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  const TEST_NAMESPACE = 'test';\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(new InterceptorPlugin(openmct));\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the missingObjectInterceptor', () => {\n    let mockProvider;\n\n    beforeEach(() => {\n      mockProvider = jasmine.createSpyObj('mock provider', ['get']);\n      mockProvider.get.and.returnValue(Promise.resolve(undefined));\n      openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);\n    });\n\n    it('returns missing objects', () => {\n      const identifier = {\n        namespace: TEST_NAMESPACE,\n        key: 'hello'\n      };\n\n      return openmct.objects.get(identifier).then((testObject) => {\n        expect(testObject).toEqual({\n          identifier,\n          type: 'unknown',\n          name: 'Missing: test:hello'\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/latestDataClock/LADClock.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LocalClock from '../../../src/plugins/utcTimeSystem/LocalClock.js';\n\nclass LADClock extends LocalClock {\n  /**\n   * A {@link Clock} that mocks a \"latest available data\" type tick source.\n   * This is for testing purposes only, and behaves identically to a local clock.\n   * It DOES NOT tick on receipt of data.\n   * @constructor\n   */\n  constructor(period) {\n    super(period);\n\n    this.key = 'test-lad';\n    this.mode = 'lad';\n    this.cssClass = 'icon-suitcase';\n    this.name = 'Latest available data';\n    this.description = 'Updates when new data is available';\n  }\n}\n\nexport default LADClock;\n"
  },
  {
    "path": "src/plugins/latestDataClock/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LADClock from './LADClock.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.time.addClock(new LADClock());\n  };\n}\n"
  },
  {
    "path": "src/plugins/licenses/LicensesComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-about c-about--licenses\">\n    <h1>Open MCT Third Party Licenses</h1>\n    <p>This software includes components released under the following licenses:</p>\n    <div v-for=\"(pkg, key) in packages\" :key=\"key\" class=\"c-license\">\n      <h2 class=\"c-license__name\">\n        {{ key }}\n      </h2>\n      <div class=\"c-license__details\">\n        <span class=\"c-license__author\"><em>Author</em> {{ pkg.publisher }}</span> |\n        <span class=\"c-license__license\"><em>License(s)</em> {{ pkg.licenses }}</span> |\n        <span class=\"c-license__repo\"\n          ><em>Repository</em>\n          <a :href=\"pkg.repository\" target=\"_blank\">{{ pkg.repository }}</a></span\n        >\n      </div>\n      <div class=\"c-license__text\">\n        <p>{{ pkg.licenseText }}</p>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport packages from './third-party-licenses.json';\n\nexport default {\n  data() {\n    return {\n      packages: packages\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/licenses/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport Licenses from './LicensesComponent.vue';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.router.route(/^\\/licenses$/, () => {\n      const { vNode, destroy } = mount(Licenses, { app: openmct.app });\n\n      openmct.overlays.overlay({\n        element: vNode.el,\n        size: 'fullscreen',\n        dismissible: false,\n        onDestroy: destroy\n      });\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/licenses/third-party-licenses.json",
    "content": "{\n  \"angular-route@1.4.14\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/angular/angular.js\",\n    \"publisher\": \"Angular Core Team\",\n    \"email\": \"angular-core+npm@google.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/angular-route\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/angular-route/LICENSE.md\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2016 Angular\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\\nSOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2016 Angular\"\n  },\n  \"angular@1.4.14\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/angular/angular.js\",\n    \"publisher\": \"Angular Core Team\",\n    \"email\": \"angular-core+npm@google.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/angular\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/angular/LICENSE.md\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2016 Angular\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\\nSOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2016 Angular\"\n  },\n  \"base64-arraybuffer@0.1.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/niklasvh/base64-arraybuffer\",\n    \"publisher\": \"Niklas von Hertzen\",\n    \"email\": \"niklasvh@gmail.com\",\n    \"url\": \"http://hertzen.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/base64-arraybuffer\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/base64-arraybuffer/LICENSE-MIT\",\n    \"licenseText\": \"Copyright (c) 2012 Niklas von Hertzen\\n\\nPermission is hereby granted, free of charge, to any person\\nobtaining a copy of this software and associated documentation\\nfiles (the \\\"Software\\\"), to deal in the Software without\\nrestriction, including without limitation the rights to use,\\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the\\nSoftware is furnished to do so, subject to the following\\nconditions:\\n\\nThe above copyright notice and this permission notice shall be\\nincluded in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND,\\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\\nOTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2012 Niklas von Hertzen\"\n  },\n  \"comma-separated-values@3.6.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/knrz/CSV.js\",\n    \"publisher\": \"=\",\n    \"email\": \"hi@knrz.co\",\n    \"url\": \"http://knrz.co/\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/comma-separated-values\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/comma-separated-values/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2014 Kash Nouroozi\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\\nSOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2014 Kash Nouroozi\"\n  },\n  \"css-line-break@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/niklasvh/css-line-break\",\n    \"publisher\": \"Niklas von Hertzen\",\n    \"email\": \"niklasvh@gmail.com\",\n    \"url\": \"https://hertzen.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/css-line-break\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/css-line-break/LICENSE\",\n    \"licenseText\": \"Copyright (c) 2017 Niklas von Hertzen\\n\\nPermission is hereby granted, free of charge, to any person\\nobtaining a copy of this software and associated documentation\\nfiles (the \\\"Software\\\"), to deal in the Software without\\nrestriction, including without limitation the rights to use,\\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the\\nSoftware is furnished to do so, subject to the following\\nconditions:\\n\\nThe above copyright notice and this permission notice shall be\\nincluded in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND,\\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\\nOTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2017 Niklas von Hertzen\"\n  },\n  \"d3-array@1.2.4\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-array\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-array\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-array/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-axis@1.0.12\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-axis\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-axis\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-axis/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-collection@1.0.7\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-collection\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-collection\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-collection/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016, Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016, Mike Bostock. All rights reserved.\"\n  },\n  \"d3-color@1.0.4\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-color\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-color\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-color/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-format@1.2.2\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-format\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-format\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-format/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2015 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2015 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-interpolate@1.1.6\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-interpolate\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-interpolate\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-interpolate/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-scale@1.0.7\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-scale\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-scale\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-scale/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2015 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2015 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-selection@1.3.2\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-selection\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"https://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-selection\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-selection/LICENSE\",\n    \"licenseText\": \"Copyright (c) 2010-2018, Michael Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without\\nmodification, are permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* The name Michael Bostock may not be used to endorse or promote products\\n  derived from this software without specific prior written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\"\\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,\\nINDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\\nBUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\\nOF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright (c) 2010-2018, Michael Bostock. All rights reserved.\"\n  },\n  \"d3-time-format@2.1.3\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-time-format\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-time-format\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-time-format/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2017 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2017 Mike Bostock. All rights reserved.\"\n  },\n  \"d3-time@1.0.10\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/d3/d3-time\",\n    \"publisher\": \"Mike Bostock\",\n    \"url\": \"http://bost.ocks.org/mike\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/d3-time\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/d3-time/LICENSE\",\n    \"licenseText\": \"Copyright 2010-2016 Mike Bostock\\nAll rights reserved.\\n\\nRedistribution and use in source and binary forms, with or without modification,\\nare permitted provided that the following conditions are met:\\n\\n* Redistributions of source code must retain the above copyright notice, this\\n  list of conditions and the following disclaimer.\\n\\n* Redistributions in binary form must reproduce the above copyright notice,\\n  this list of conditions and the following disclaimer in the documentation\\n  and/or other materials provided with the distribution.\\n\\n* Neither the name of the author nor the names of contributors may be used to\\n  endorse or promote products derived from this software without specific prior\\n  written permission.\\n\\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \\\"AS IS\\\" AND\\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\",\n    \"copyright\": \"Copyright 2010-2016 Mike Bostock. All rights reserved.\"\n  },\n  \"eventemitter3@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/primus/eventemitter3\",\n    \"publisher\": \"Arnout Kazemier\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/eventemitter3\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/eventemitter3/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2014 Arnout Kazemier\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\\nSOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2014 Arnout Kazemier\"\n  },\n  \"file-saver@1.3.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/eligrey/FileSaver.js\",\n    \"publisher\": \"Eli Grey\",\n    \"email\": \"me@eligrey.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/file-saver\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/file-saver/LICENSE.md\",\n    \"licenseText\": \"The MIT License\\n\\nCopyright © 2016 [Eli Grey][1].\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \\\"Software\\\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\\n\\n  [1]: http://eligrey.com\",\n    \"copyright\": \"Copyright © 2016 [Eli Grey][1].\"\n  },\n  \"html2canvas@1.0.0-alpha.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/niklasvh/html2canvas\",\n    \"publisher\": \"Niklas von Hertzen\",\n    \"email\": \"niklasvh@gmail.com\",\n    \"url\": \"https://hertzen.com\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/html2canvas\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/html2canvas/LICENSE\",\n    \"licenseText\": \"Copyright (c) 2012 Niklas von Hertzen\\n\\nPermission is hereby granted, free of charge, to any person\\nobtaining a copy of this software and associated documentation\\nfiles (the \\\"Software\\\"), to deal in the Software without\\nrestriction, including without limitation the rights to use,\\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the\\nSoftware is furnished to do so, subject to the following\\nconditions:\\n\\nThe above copyright notice and this permission notice shall be\\nincluded in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND,\\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\\nOTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2012 Niklas von Hertzen\"\n  },\n  \"location-bar@3.0.1\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/KidkArolis/location-bar\",\n    \"publisher\": \"Karolis Narkevicius\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/location-bar\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/location-bar/README.md\",\n    \"licenseText\": \"\"\n  },\n  \"lodash@3.10.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/lodash\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/lodash/LICENSE\",\n    \"licenseText\": \"Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/>\\nBased on Underscore.js, copyright 2009-2015 Jeremy Ashkenas,\\nDocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>\\n\\nPermission is hereby granted, free of charge, to any person obtaining\\na copy of this software and associated documentation files (the\\n\\\"Software\\\"), to deal in the Software without restriction, including\\nwithout limitation the rights to use, copy, modify, merge, publish,\\ndistribute, sublicense, and/or sell copies of the Software, and to\\npermit persons to whom the Software is furnished to do so, subject to\\nthe following conditions:\\n\\nThe above copyright notice and this permission notice shall be\\nincluded in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND,\\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/>. Based on Underscore.js, copyright 2009-2015 Jeremy Ashkenas,. DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>\"\n  },\n  \"moment-duration-format@2.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jsmreese/moment-duration-format\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/moment-duration-format\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/moment-duration-format/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2018 John Madhavan-Reese\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy of\\nthis software and associated documentation files (the \\\"Software\\\"), to deal in\\nthe Software without restriction, including without limitation the rights to\\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\\nthe Software, and to permit persons to whom the Software is furnished to do so,\\nsubject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2018 John Madhavan-Reese\"\n  },\n  \"moment-timezone@0.5.23\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/moment/moment-timezone\",\n    \"publisher\": \"Tim Wood\",\n    \"email\": \"washwithcare@gmail.com\",\n    \"url\": \"http://timwoodcreates.com/\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/moment-timezone\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/moment-timezone/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\r\\n\\r\\nCopyright (c) JS Foundation and other contributors\\r\\n\\r\\nPermission is hereby granted, free of charge, to any person obtaining a copy of\\r\\nthis software and associated documentation files (the \\\"Software\\\"), to deal in\\r\\nthe Software without restriction, including without limitation the rights to\\r\\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\\r\\nthe Software, and to permit persons to whom the Software is furnished to do so,\\r\\nsubject to the following conditions:\\r\\n\\r\\nThe above copyright notice and this permission notice shall be included in all\\r\\ncopies or substantial portions of the Software.\\r\\n\\r\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\r\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\\r\\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\\r\\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\\r\\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\\r\\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) JS Foundation and other contributors\"\n  },\n  \"moment@2.24.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/moment/moment\",\n    \"publisher\": \"Iskren Ivov Chernev\",\n    \"email\": \"iskren.chernev@gmail.com\",\n    \"url\": \"https://github.com/ichernev\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/moment\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/moment/LICENSE\",\n    \"licenseText\": \"Copyright (c) JS Foundation and other contributors\\n\\nPermission is hereby granted, free of charge, to any person\\nobtaining a copy of this software and associated documentation\\nfiles (the \\\"Software\\\"), to deal in the Software without\\nrestriction, including without limitation the rights to use,\\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the\\nSoftware is furnished to do so, subject to the following\\nconditions:\\n\\nThe above copyright notice and this permission notice shall be\\nincluded in all copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND,\\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\\nOTHER DEALINGS IN THE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) JS Foundation and other contributors\"\n  },\n  \"painterro@0.2.71\": {\n    \"licenses\": \"MIT\",\n    \"publisher\": \"Ivan Borshchov\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/painterro\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/painterro/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2017 Ivan Borshchov\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in\\nall copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\\nTHE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2017 Ivan Borshchov\"\n  },\n  \"printj@1.2.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/SheetJS/printj\",\n    \"publisher\": \"sheetjs\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/printj\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/printj/LICENSE\",\n    \"licenseText\": \"Copyright (C) 2016-present  SheetJS\\n\\n   Licensed under the Apache License, Version 2.0 (the \\\"License\\\");\\n   you may not use this file except in compliance with the License.\\n   You may obtain a copy of the License at\\n\\n       http://www.apache.org/licenses/LICENSE-2.0\\n\\n   Unless required by applicable law or agreed to in writing, software\\n   distributed under the License is distributed on an \\\"AS IS\\\" BASIS,\\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\\n   See the License for the specific language governing permissions and\\n   limitations under the License.\",\n    \"copyright\": \"Copyright (C) 2016-present  SheetJS\"\n  },\n  \"vue@2.5.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vuejs/vue\",\n    \"publisher\": \"Evan You\",\n    \"path\": \"/Users/akhenry/Code/licenses/node_modules/vue\",\n    \"licenseFile\": \"/Users/akhenry/Code/licenses/node_modules/vue/LICENSE\",\n    \"licenseText\": \"The MIT License (MIT)\\n\\nCopyright (c) 2013-present, Yuxi (Evan) You\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \\\"Software\\\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in\\nall copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\\nTHE SOFTWARE.\",\n    \"copyright\": \"Copyright (c) 2013-present, Yuxi (Evan) You\"\n  }\n}\n"
  },
  {
    "path": "src/plugins/linkAction/LinkAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst LINK_ACTION_KEY = 'link';\n\nclass LinkAction {\n  constructor(openmct) {\n    this.name = 'Create Link';\n    this.key = LINK_ACTION_KEY;\n    this.description = 'Create Link to object in another location.';\n    this.cssClass = 'icon-link';\n    this.group = 'action';\n    this.priority = 7;\n\n    this.openmct = openmct;\n    this.transaction = null;\n  }\n\n  appliesTo(objectPath) {\n    return true; // link away!\n  }\n\n  invoke(objectPath) {\n    this.object = objectPath[0];\n    this.parent = objectPath[1];\n    this.showForm(this.object, this.parent);\n  }\n\n  inNavigationPath() {\n    return this.openmct.router.path.some((objectInPath) =>\n      this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)\n    );\n  }\n\n  onSave(changes) {\n    this.startTransaction();\n\n    const inNavigationPath = this.inNavigationPath();\n    if (inNavigationPath && this.openmct.editor.isEditing()) {\n      this.openmct.editor.save();\n    }\n\n    const parentDomainObjectpath = changes.location || [this.parent];\n    const parent = parentDomainObjectpath[0];\n\n    this.linkInNewParent(this.object, parent);\n\n    return this.saveTransaction();\n  }\n\n  linkInNewParent(child, newParent) {\n    let compositionCollection = this.openmct.composition.get(newParent);\n\n    compositionCollection.add(child);\n  }\n\n  showForm(domainObject, parentDomainObject) {\n    const formStructure = {\n      title: `Link \"${domainObject.name}\" to a New Location`,\n      sections: [\n        {\n          rows: [\n            {\n              name: 'Location',\n              cssClass: 'grows',\n              control: 'locator',\n              parent: parentDomainObject,\n              required: true,\n              validate: this.validate(parentDomainObject),\n              key: 'location'\n            }\n          ]\n        }\n      ]\n    };\n    this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this));\n  }\n\n  validate(currentParent) {\n    return (data) => {\n      // default current parent to ROOT, if it's null, then it's a root level item\n      if (!currentParent) {\n        currentParent = {\n          identifier: {\n            key: 'ROOT',\n            namespace: ''\n          }\n        };\n      }\n\n      const parentCandidatePath = data.value;\n      const parentCandidate = parentCandidatePath[0];\n      const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);\n\n      if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {\n        return false;\n      }\n\n      // check if moving to same place\n      if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {\n        return false;\n      }\n\n      // check if moving to a child\n      if (\n        parentCandidatePath.some((candidatePath) => {\n          return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);\n        })\n      ) {\n        return false;\n      }\n\n      const parentCandidateComposition = parentCandidate.composition;\n      if (\n        parentCandidateComposition &&\n        parentCandidateComposition.indexOf(objectKeystring) !== -1\n      ) {\n        return false;\n      }\n\n      return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);\n    };\n  }\n  startTransaction() {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.transaction = this.openmct.objects.startTransaction();\n    }\n  }\n\n  async saveTransaction() {\n    if (!this.transaction) {\n      return;\n    }\n\n    await this.transaction.commit();\n    this.openmct.objects.endTransaction();\n    this.transaction = null;\n  }\n}\n\nexport { LINK_ACTION_KEY };\n\nexport default LinkAction;\n"
  },
  {
    "path": "src/plugins/linkAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport LinkAction from './LinkAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new LinkAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/linkAction/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, getMockObjects, resetApplicationState } from 'utils/testing';\n\nimport { LINK_ACTION_KEY } from './LinkAction.js';\nimport LinkActionPlugin from './plugin.js';\n\ndescribe('The Link Action plugin', () => {\n  let openmct;\n  let linkAction;\n  let childObject;\n  let parentObject;\n  let anotherParentObject;\n  const ORIGINAL_PARENT_ID = 'original-parent-object';\n  const LINK_ACTION_NAME = 'Create Link';\n\n  beforeEach((done) => {\n    const appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n\n    openmct = createOpenMct();\n\n    childObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Child Folder',\n          location: ORIGINAL_PARENT_ID,\n          identifier: {\n            namespace: '',\n            key: 'child-folder-object'\n          }\n        }\n      }\n    }).folder;\n\n    parentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Parent Folder',\n          identifier: {\n            namespace: '',\n            key: 'original-parent-object'\n          },\n          composition: [childObject.identifier]\n        }\n      }\n    }).folder;\n\n    anotherParentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Another Parent Folder'\n        }\n      }\n    }).folder;\n\n    openmct.router.path = [childObject]; // preview action uses this in it's applyTo method\n\n    openmct.install(LinkActionPlugin());\n\n    openmct.on('start', done);\n    openmct.startHeadless(appHolder);\n  });\n\n  afterEach(() => {\n    resetApplicationState(openmct);\n  });\n\n  it('should be defined', () => {\n    expect(LinkActionPlugin).toBeDefined();\n  });\n\n  it('should make the link action available for an appropriate domainObject', () => {\n    const actionCollection = openmct.actions.getActionsCollection([childObject]);\n    const visibleActions = actionCollection.getVisibleActions();\n    linkAction = visibleActions.find((a) => a.key === LINK_ACTION_KEY);\n\n    expect(linkAction.name).toEqual(LINK_ACTION_NAME);\n  });\n\n  describe('when linking an object in a new parent', () => {\n    beforeEach(() => {\n      linkAction = openmct.actions.getAction(LINK_ACTION_KEY);\n      linkAction.linkInNewParent(childObject, anotherParentObject);\n    });\n\n    it(\"the child object's identifier should be in the new parent's composition and location set to original parent\", () => {\n      let newParentChild = anotherParentObject.composition[0];\n      expect(newParentChild).toEqual(childObject.identifier);\n      expect(childObject.location).toEqual(ORIGINAL_PARENT_ID);\n    });\n\n    it(\"the child object's identifier should remain in the original parent's composition\", () => {\n      let oldParentCompositionChild = parentObject.composition[0];\n      expect(oldParentCompositionChild).toEqual(childObject.identifier);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/localStorage/LocalStorageObjectProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { filter__proto__ } from '../../utils/sanitization.js';\n\nexport default class LocalStorageObjectProvider {\n  constructor(spaceKey = 'mct') {\n    this.localStorage = window.localStorage;\n    this.spaceKey = spaceKey;\n    this.initializeSpace(spaceKey);\n  }\n\n  get(identifier) {\n    if (this.getSpaceAsObject()[identifier.key] !== undefined) {\n      const persistedModel = this.getSpaceAsObject()[identifier.key];\n      const domainObject = {\n        identifier,\n        ...persistedModel\n      };\n\n      return Promise.resolve(domainObject);\n    } else {\n      return Promise.resolve(undefined);\n    }\n  }\n\n  getAllObjects() {\n    return this.getSpaceAsObject();\n  }\n\n  create(object) {\n    return this.persistObject(object);\n  }\n\n  update(object) {\n    return this.persistObject(object);\n  }\n\n  /**\n   * @private\n   */\n  persistObject(domainObject) {\n    let space = this.getSpaceAsObject();\n    space[domainObject.identifier.key] = domainObject;\n\n    this.persistSpace(space);\n\n    return Promise.resolve(true);\n  }\n\n  /**\n   * @private\n   */\n  persistSpace(space) {\n    this.localStorage.setItem(this.spaceKey, JSON.stringify(space));\n  }\n\n  isReadOnly() {\n    return false;\n  }\n\n  /**\n   * @private\n   */\n  getSpace() {\n    return this.localStorage.getItem(this.spaceKey);\n  }\n\n  /**\n   * @private\n   */\n  getSpaceAsObject() {\n    return JSON.parse(this.getSpace(), filter__proto__);\n  }\n\n  /**\n   * @private\n   */\n  initializeSpace() {\n    if (this.isEmpty()) {\n      this.localStorage.setItem(this.spaceKey, JSON.stringify({}));\n    }\n  }\n\n  /**\n   * @private\n   */\n  isEmpty() {\n    return this.getSpace() === null;\n  }\n}\n"
  },
  {
    "path": "src/plugins/localStorage/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LocalStorageObjectProvider from './LocalStorageObjectProvider.js';\n\nexport default function (namespace = '', storageSpace = 'mct') {\n  return function (openmct) {\n    openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace));\n  };\n}\n"
  },
  {
    "path": "src/plugins/localStorage/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('The local storage plugin', () => {\n  let space;\n  let openmct;\n\n  beforeEach(() => {\n    space = `test-${Date.now()}`;\n    openmct = createOpenMct();\n\n    openmct.install(openmct.plugins.LocalStorage('', space));\n  });\n\n  it('initializes localstorage if not already initialized', () => {\n    const ls = getLocalStorage();\n    expect(ls[space]).toBeDefined();\n  });\n\n  it('successfully persists an object to localstorage', async () => {\n    const domainObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-key'\n      },\n      name: 'A test object'\n    };\n    let spaceAsObject = getSpaceAsObject();\n    expect(spaceAsObject['test-key']).not.toBeDefined();\n\n    await openmct.objects.save(domainObject);\n\n    spaceAsObject = getSpaceAsObject();\n    expect(spaceAsObject['test-key']).toBeDefined();\n  });\n\n  it('successfully retrieves an object from localstorage', async () => {\n    const domainObject = {\n      identifier: {\n        namespace: '',\n        key: 'test-key'\n      },\n      name: 'A test object',\n      anotherProperty: Date.now()\n    };\n    await openmct.objects.save(domainObject);\n\n    let testObject = await openmct.objects.get(domainObject.identifier);\n\n    expect(testObject.name).toEqual(domainObject.name);\n    expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty);\n  });\n\n  it('prevents prototype pollution from manipulated localstorage', async () => {\n    spyOn(console, 'warn');\n\n    const identifier = {\n      namespace: '',\n      key: 'test-key'\n    };\n\n    const pollutedSpaceString = `{\"test-key\":{\"__proto__\":{\"toString\":\"foobar\"},\"type\":\"folder\",\"name\":\"A test object\",\"identifier\":{\"namespace\":\"\",\"key\":\"test-key\"}}}`;\n    getLocalStorage()[space] = pollutedSpaceString;\n\n    let testObject = await openmct.objects.get(identifier);\n\n    const hasPollutedProto =\n      Object.prototype.hasOwnProperty.call(testObject, '__proto__') ||\n      Object.getPrototypeOf(testObject) !== Object.getPrototypeOf({});\n\n    // warning from openmct.objects.get\n    expect(console.warn).not.toHaveBeenCalled();\n    expect(hasPollutedProto).toBeFalse();\n  });\n\n  afterEach(() => {\n    resetApplicationState(openmct);\n    resetLocalStorage();\n  });\n\n  function resetLocalStorage() {\n    delete window.localStorage[space];\n  }\n\n  function getLocalStorage() {\n    return window.localStorage;\n  }\n\n  function getSpaceAsObject() {\n    return JSON.parse(getLocalStorage()[space]);\n  }\n});\n"
  },
  {
    "path": "src/plugins/localTimeSystem/LocalTimeFormat.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport moment from 'moment';\n\nconst DATE_FORMAT = 'YYYY-MM-DD h:mm:ss.SSS a';\n\nconst DATE_FORMATS = [DATE_FORMAT, 'YYYY-MM-DD h:mm:ss a', 'YYYY-MM-DD h:mm a', 'YYYY-MM-DD'];\n\n/**\n * @typedef Scale\n * @property {number} min the minimum scale value, in ms\n * @property {number} max the maximum scale value, in ms\n */\n\n/**\n * Formatter for UTC timestamps. Interprets numeric values as\n * milliseconds since the start of 1970.\n *\n * @implements {Format}\n * @constructor\n */\nexport default function LocalTimeFormat() {\n  this.key = 'local-format';\n}\n\n/**\n *\n * @param value\n * @returns {string} the formatted date\n */\nLocalTimeFormat.prototype.format = function (value, scale) {\n  return moment(value).format(DATE_FORMAT);\n};\n\nLocalTimeFormat.prototype.parse = function (text) {\n  if (typeof text === 'number') {\n    return text;\n  }\n\n  return moment(text, DATE_FORMATS).valueOf();\n};\n\nLocalTimeFormat.prototype.validate = function (text) {\n  return moment(text, DATE_FORMATS).isValid();\n};\n"
  },
  {
    "path": "src/plugins/localTimeSystem/LocalTimeSystem.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This time system supports UTC dates and provides a ticking clock source.\n * @implements TimeSystem\n * @constructor\n */\nexport default class LocalTimeSystem {\n  constructor() {\n    /**\n     * Some metadata, which will be used to identify the time system in\n     * the UI\n     * @type {{key: string, name: string, glyph: string}}\n     */\n    this.key = 'local';\n    this.name = 'Local';\n    this.cssClass = 'icon-clock';\n\n    this.timeFormat = 'local-format';\n    this.durationFormat = 'duration';\n\n    this.isUTCBased = true;\n  }\n}\n"
  },
  {
    "path": "src/plugins/localTimeSystem/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport LocalTimeFormat from './LocalTimeFormat.js';\nimport LocalTimeSystem from './LocalTimeSystem.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.time.addTimeSystem(new LocalTimeSystem());\n    openmct.telemetry.addFormat(new LocalTimeFormat());\n  };\n}\n"
  },
  {
    "path": "src/plugins/localTimeSystem/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('The local time', () => {\n  const LOCAL_FORMAT_KEY = 'local-format';\n  const LOCAL_SYSTEM_KEY = 'local';\n  const JUNK = 'junk';\n  const TIMESTAMP = -14256000000;\n  const DATESTRING = '1969-07-20 12:00:00.000 am';\n  let openmct;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.install(openmct.plugins.LocalTimeSystem());\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('system', function () {\n    let localTimeSystem;\n\n    beforeEach(() => {\n      localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, {\n        start: 0,\n        end: 1\n      });\n    });\n\n    it('is installed', () => {\n      let timeSystems = openmct.time.getAllTimeSystems();\n      let local = timeSystems.find((ts) => ts.key === LOCAL_SYSTEM_KEY);\n\n      expect(local).not.toEqual(-1);\n    });\n\n    it('can be set to be the main time system', () => {\n      expect(openmct.time.getTimeSystem().key).toBe(LOCAL_SYSTEM_KEY);\n    });\n\n    it('uses the local-format time format', () => {\n      expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY);\n    });\n\n    it('is UTC based', () => {\n      expect(localTimeSystem.isUTCBased).toBe(true);\n    });\n\n    it('defines expected metadata', () => {\n      expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY);\n      expect(localTimeSystem.name).toBeDefined();\n      expect(localTimeSystem.cssClass).toBeDefined();\n      expect(localTimeSystem.durationFormat).toBeDefined();\n    });\n  });\n\n  describe('formatter can be obtained from the telemetry API and', () => {\n    let localTimeFormatter;\n    let dateString;\n    let timeStamp;\n\n    beforeEach(() => {\n      localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY);\n      dateString = localTimeFormatter.format(TIMESTAMP);\n      timeStamp = localTimeFormatter.parse(DATESTRING);\n    });\n\n    it('will format a timestamp in local time format', () => {\n      expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString);\n    });\n\n    it('will parse an local time Date String into milliseconds', () => {\n      expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp);\n    });\n\n    it('will validate correctly', () => {\n      expect(localTimeFormatter.validate(DATESTRING)).toBe(true);\n      expect(localTimeFormatter.validate(JUNK)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/move/MoveAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst MOVE_ACTION_KEY = 'move';\n\nclass MoveAction {\n  constructor(openmct) {\n    this.name = 'Move';\n    this.key = MOVE_ACTION_KEY;\n    this.description = 'Move this object from its containing object to another object.';\n    this.cssClass = 'icon-move';\n    this.group = 'action';\n    this.priority = 7;\n\n    this.openmct = openmct;\n    this.transaction = null;\n  }\n\n  invoke(objectPath) {\n    this.object = objectPath[0];\n    this.oldParent = objectPath[1];\n\n    this.showForm(this.object, this.oldParent);\n  }\n\n  inNavigationPath() {\n    return this.openmct.router.path.some((objectInPath) =>\n      this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)\n    );\n  }\n\n  navigateTo(objectPath) {\n    const urlPath = objectPath\n      .reverse()\n      .map((object) => this.openmct.objects.makeKeyString(object.identifier))\n      .join('/');\n\n    this.openmct.router.navigate('#/browse/' + urlPath);\n  }\n\n  addToNewParent(child, newParent) {\n    const newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier);\n    const compositionCollection = this.openmct.composition.get(newParent);\n\n    this.openmct.objects.mutate(child, 'location', newParentKeyString);\n    compositionCollection.add(child);\n  }\n\n  async onSave(changes) {\n    this.startTransaction();\n\n    const inNavigationPath = this.inNavigationPath(this.object);\n    const parentDomainObjectpath = changes.location || [this.parent];\n    const parent = parentDomainObjectpath[0];\n\n    if (this.openmct.objects.areIdsEqual(parent.identifier, this.oldParent.identifier)) {\n      this.openmct.notifications.error(`Error: new location cant not be same as old`);\n\n      return;\n    }\n\n    if (changes.name && changes.name !== this.object.name) {\n      this.object.name = changes.name;\n    }\n\n    this.addToNewParent(this.object, parent);\n    this.removeFromOldParent(this.object);\n\n    await this.saveTransaction();\n\n    if (!inNavigationPath) {\n      return;\n    }\n\n    let newObjectPath;\n\n    if (parentDomainObjectpath) {\n      newObjectPath = parentDomainObjectpath && [this.object].concat(parentDomainObjectpath);\n    } else {\n      const root = await this.openmct.objects.getRoot();\n      const rootCompositionCollection = this.openmct.composition.get(root);\n      const rootComposition = await rootCompositionCollection.load();\n      const rootChildCount = rootComposition.length;\n      newObjectPath = await this.openmct.objects.getOriginalPath(this.object.identifier);\n\n      // if not multiple root children, remove root from path\n      if (rootChildCount < 2) {\n        newObjectPath.pop(); // remove ROOT\n      }\n    }\n\n    this.navigateTo(newObjectPath);\n  }\n\n  removeFromOldParent(child) {\n    const compositionCollection = this.openmct.composition.get(this.oldParent);\n    compositionCollection.remove(child);\n  }\n\n  showForm(domainObject, parentDomainObject) {\n    const formStructure = {\n      title: 'Move Item',\n      sections: [\n        {\n          rows: [\n            {\n              key: 'name',\n              control: 'textfield',\n              name: 'Title',\n              pattern: '\\\\S+',\n              required: true,\n              cssClass: 'l-input-lg',\n              value: domainObject.name\n            },\n            {\n              name: 'Location',\n              cssClass: 'grows',\n              control: 'locator',\n              parent: parentDomainObject,\n              required: true,\n              validate: this.validate(parentDomainObject),\n              key: 'location'\n            }\n          ]\n        }\n      ]\n    };\n\n    this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this));\n  }\n\n  validate(currentParent) {\n    return (data) => {\n      const parentCandidatePath = data.value;\n      const parentCandidate = parentCandidatePath[0];\n\n      // check if moving to same place\n      if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {\n        return false;\n      }\n\n      // check if moving to a child\n      if (\n        parentCandidatePath.some((candidatePath) => {\n          return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);\n        })\n      ) {\n        return false;\n      }\n\n      if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {\n        return false;\n      }\n\n      const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);\n      const parentCandidateComposition = parentCandidate.composition;\n\n      if (\n        parentCandidateComposition &&\n        parentCandidateComposition.indexOf(objectKeystring) !== -1\n      ) {\n        return false;\n      }\n\n      return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);\n    };\n  }\n\n  appliesTo(objectPath) {\n    const parent = objectPath[1];\n    const parentType = parent && this.openmct.types.get(parent.type);\n    const child = objectPath[0];\n    const childType = child && this.openmct.types.get(child.type);\n    const isPersistable = this.openmct.objects.isPersistable(child.identifier);\n\n    if (parent?.locked || !isPersistable) {\n      return false;\n    }\n\n    return (\n      parentType?.definition.creatable &&\n      childType?.definition.creatable &&\n      Array.isArray(parent.composition)\n    );\n  }\n\n  startTransaction() {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.transaction = this.openmct.objects.startTransaction();\n    }\n  }\n\n  async saveTransaction() {\n    if (!this.transaction) {\n      return;\n    }\n\n    await this.transaction.commit();\n    this.openmct.objects.endTransaction();\n    this.transaction = null;\n  }\n}\n\nexport { MOVE_ACTION_KEY };\n\nexport default MoveAction;\n"
  },
  {
    "path": "src/plugins/move/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport MoveAction from './MoveAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new MoveAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/move/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, getMockObjects, resetApplicationState } from 'utils/testing';\n\ndescribe('The Move Action plugin', () => {\n  let openmct;\n  let moveAction;\n  let childObject;\n  let parentObject;\n  let anotherParentObject;\n\n  // this setups up the app\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    childObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Child Folder',\n          identifier: {\n            namespace: '',\n            key: 'child-folder-object'\n          },\n          location: 'parent-folder-object'\n        }\n      }\n    }).folder;\n\n    parentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Parent Folder',\n          composition: [childObject.identifier],\n          identifier: {\n            namespace: '',\n            key: 'parent-folder-object'\n          }\n        }\n      }\n    }).folder;\n\n    anotherParentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Another Parent Folder',\n          identifier: {\n            namespace: '',\n            key: 'another-parent-folder-object'\n          }\n        }\n      }\n    }).folder;\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    moveAction = openmct.actions._allActions.move;\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should be defined', () => {\n    expect(moveAction).toBeDefined();\n  });\n\n  describe('when determining the object is applicable', () => {\n    beforeEach(() => {\n      spyOn(moveAction, 'appliesTo').and.callThrough();\n    });\n\n    it('should be true when the parent is creatable and has composition', () => {\n      let applies = moveAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(true);\n    });\n\n    it('should be true when the child is locked and not an alias', () => {\n      childObject.locked = true;\n      let applies = moveAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(true);\n    });\n\n    it('should still be true when the child is locked and is an alias', () => {\n      childObject.locked = true;\n      childObject.location = 'another-parent-folder-object';\n      let applies = moveAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(true);\n    });\n  });\n\n  describe('when moving an object to a new parent and removing from the old parent', () => {\n    let unObserve;\n    beforeEach((done) => {\n      openmct.router.path = [];\n\n      spyOn(openmct.objects, 'save');\n      openmct.objects.save.and.callThrough();\n      spyOn(openmct.forms, 'showForm');\n      openmct.forms.showForm.and.callFake((formStructure) => {\n        return Promise.resolve({\n          name: 'test',\n          location: [anotherParentObject]\n        });\n      });\n\n      unObserve = openmct.objects.observe(parentObject, '*', (newObject) => {\n        done();\n      });\n\n      moveAction.inNavigationPath = () => false;\n\n      moveAction.invoke([childObject, parentObject]);\n    });\n\n    afterEach(() => {\n      unObserve();\n    });\n\n    it(\"the child object's identifier should be in the new parent's composition\", () => {\n      let newParentChild = anotherParentObject.composition[0];\n      expect(newParentChild).toEqual(childObject.identifier);\n    });\n\n    it(\"the child object's identifier should be removed from the old parent's composition\", () => {\n      let oldParentComposition = parentObject.composition;\n      expect(oldParentComposition.length).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/myItems/README.md",
    "content": "# My Items plugin\nDefines top-level folder named \"My Items\" to store user-created items. Enabled by default, this can be disabled in a read-only deployment with no user-editable objects.\n\n## Installation\n```js\nopenmct.install(openmct.plugins.MyItems());\n```\n\n## Options\nWhen installing, the plugin can take several options:\n\n- `name`: The label of the root object. Defaults to \"My Items\"\n  - Example: `'Apple Items'`\n\n- `namespace`: The namespace to create the root object in. Defaults to the empty string `''`\n  - Example: `'apple-namespace'`\n\n- `priority`: The optional priority to install this plugin. Defaults to `openmct.priority.LOW`\n  - Example: `'openmct.priority.LOW'`\n\nE.g., to install with a custom name and namespace, you could use:\n\n\n```js\nopenmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));\n```"
  },
  {
    "path": "src/plugins/myItems/createMyItemsIdentifier.js",
    "content": "export const MY_ITEMS_KEY = 'mine';\n\nexport function createMyItemsIdentifier(namespace = '') {\n  return {\n    key: MY_ITEMS_KEY,\n    namespace\n  };\n}\n"
  },
  {
    "path": "src/plugins/myItems/myItemsInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction myItemsInterceptor({ openmct, identifierObject, name }) {\n  const myItemsModel = {\n    identifier: identifierObject,\n    name,\n    type: 'folder',\n    composition: [],\n    location: 'ROOT'\n  };\n\n  return {\n    appliesTo: (identifier) => {\n      return (\n        identifier.key === myItemsModel.identifier.key &&\n        identifier.namespace === myItemsModel.identifier.namespace\n      );\n    },\n    invoke: (identifier, object) => {\n      if (!object || openmct.objects.isMissing(object)) {\n        openmct.objects.save(myItemsModel);\n\n        return myItemsModel;\n      }\n\n      return object;\n    },\n    priority: openmct.priority.HIGHEST\n  };\n}\n\nexport default myItemsInterceptor;\n"
  },
  {
    "path": "src/plugins/myItems/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createMyItemsIdentifier } from './createMyItemsIdentifier.js';\nimport myItemsInterceptor from './myItemsInterceptor.js';\n\nconst MY_ITEMS_DEFAULT_NAME = 'My Items';\n\nexport default function MyItemsPlugin(\n  name = MY_ITEMS_DEFAULT_NAME,\n  namespace = '',\n  priority = undefined\n) {\n  return function install(openmct) {\n    const identifierObject = createMyItemsIdentifier(namespace);\n\n    if (priority === undefined) {\n      priority = openmct.priority.LOW;\n    }\n\n    openmct.objects.addGetInterceptor(myItemsInterceptor({ openmct, identifierObject, name }));\n    openmct.objects.addRoot(identifierObject, priority);\n  };\n}\n"
  },
  {
    "path": "src/plugins/myItems/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport { createMyItemsIdentifier, MY_ITEMS_KEY } from './createMyItemsIdentifier.js';\n\nconst MISSING_NAME = `Missing: ${MY_ITEMS_KEY}`;\nconst DEFAULT_NAME = 'My Items';\nconst FANCY_NAME = 'Fancy Items';\nconst myItemsIdentifier = createMyItemsIdentifier();\n\ndescribe('the plugin', () => {\n  let openmct;\n  let missingObj = {\n    identifier: myItemsIdentifier,\n    type: 'unknown',\n    name: MISSING_NAME\n  };\n\n  describe('with no arguments passed in', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n      openmct.install(openmct.plugins.MyItems());\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('when installed, adds \"My Items\" to the root', async () => {\n      const root = await openmct.objects.get('ROOT');\n      const rootCompositionCollection = openmct.composition.get(root);\n      const rootComposition = await rootCompositionCollection.load();\n      let myItems = rootComposition.filter((domainObject) => {\n        return openmct.objects.areIdsEqual(domainObject.identifier, myItemsIdentifier);\n      })[0];\n\n      expect(myItems.name).toBe(DEFAULT_NAME);\n      expect(myItems).toBeDefined();\n    });\n\n    describe('adds an interceptor that returns a \"My Items\" model for', () => {\n      let myItemsObject;\n      let mockNotFoundProvider;\n      let activeProvider;\n\n      beforeEach(async () => {\n        mockNotFoundProvider = {\n          get: () => Promise.reject(new Error('Not found')),\n          create: () => Promise.resolve(missingObj),\n          update: () => Promise.resolve(missingObj)\n        };\n\n        activeProvider = mockNotFoundProvider;\n        spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);\n        myItemsObject = await openmct.objects.get(myItemsIdentifier);\n      });\n\n      it('missing objects', () => {\n        let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);\n\n        expect(myItemsObject).toBeDefined();\n        expect(idsMatch).toBeTrue();\n      });\n    });\n  });\n\n  describe('with a name argument passed in', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n      openmct.install(openmct.plugins.MyItems(FANCY_NAME));\n\n      spyOn(openmct.objects, 'isMissing').and.returnValue(true);\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('when installed, uses the passed in name', async () => {\n      let myItems = await openmct.objects.get(myItemsIdentifier);\n\n      expect(myItems.name).toBe(FANCY_NAME);\n      expect(myItems).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/newFolderAction/newFolderAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst NEW_FOLDER_ACTION_KEY = 'newFolder';\n\nclass NewFolderAction {\n  constructor(openmct) {\n    this.type = 'folder';\n    this.name = 'Add New Folder';\n    this.key = NEW_FOLDER_ACTION_KEY;\n    this.description = 'Create a new folder';\n    this.cssClass = 'icon-folder-new';\n    this.group = 'action';\n    this.priority = 9;\n\n    this._openmct = openmct;\n  }\n\n  invoke(objectPath) {\n    const parentDomainObject = objectPath[0];\n    const createAction = this._openmct.actions.getAction('create');\n    createAction.invoke(this.type, parentDomainObject);\n  }\n\n  appliesTo(objectPath) {\n    let domainObject = objectPath[0];\n    let isPersistable = this._openmct.objects.isPersistable(domainObject.identifier);\n\n    return domainObject.type === this.type && isPersistable;\n  }\n}\n\nexport { NEW_FOLDER_ACTION_KEY };\n\nexport default NewFolderAction;\n"
  },
  {
    "path": "src/plugins/newFolderAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport NewFolderAction from './newFolderAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new NewFolderAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/newFolderAction/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('the plugin', () => {\n  let openmct;\n  let newFolderAction;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    newFolderAction = openmct.actions._allActions.newFolder;\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('installs the new folder action', () => {\n    expect(newFolderAction).toBeDefined();\n  });\n\n  describe('when invoked', () => {\n    let parentObject;\n    let parentObjectPath;\n    let changedParentObject;\n    let unobserve;\n    beforeEach((done) => {\n      parentObject = {\n        name: 'mock folder',\n        type: 'folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        },\n        composition: []\n      };\n      parentObjectPath = [parentObject];\n\n      spyOn(openmct.objects, 'save');\n      openmct.objects.save.and.callThrough();\n\n      spyOn(openmct.forms, 'showForm');\n      openmct.forms.showForm.and.callFake((formStructure) => {\n        return Promise.resolve({\n          name: 'test',\n          notes: 'test notes',\n          location: parentObjectPath\n        });\n      });\n\n      unobserve = openmct.objects.observe(parentObject, '*', (newObject) => {\n        changedParentObject = newObject;\n\n        done();\n      });\n\n      newFolderAction.invoke(parentObjectPath);\n    });\n    afterEach(() => {\n      unobserve();\n    });\n\n    it('creates a new folder object', () => {\n      expect(openmct.objects.save).toHaveBeenCalled();\n    });\n\n    it('adds new folder object to parent composition', () => {\n      const composition = changedParentObject.composition;\n\n      expect(composition.length).toBe(1);\n    });\n\n    it('checks if the domainObject is persistable', () => {\n      const mockObjectPath = [\n        {\n          name: 'mock folder',\n          type: 'folder',\n          identifier: {\n            key: 'mock-folder',\n            namespace: ''\n          }\n        }\n      ];\n\n      spyOn(openmct.objects, 'isPersistable').and.returnValue(true);\n\n      newFolderAction.appliesTo(mockObjectPath);\n\n      expect(openmct.objects.isPersistable).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/notebook/NotebookType.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration.js';\n\nexport default class NotebookType {\n  constructor(name, description, icon) {\n    this.name = name;\n    this.description = description;\n    this.cssClass = icon;\n    this.creatable = true;\n    this.form = [\n      {\n        key: 'defaultSort',\n        name: 'Entry Sorting',\n        control: 'select',\n        options: [\n          {\n            name: 'Newest First',\n            value: 'newest'\n          },\n          {\n            name: 'Oldest First',\n            value: 'oldest'\n          }\n        ],\n        cssClass: 'l-inline',\n        property: ['configuration', 'defaultSort']\n      },\n      {\n        key: 'sectionTitle',\n        name: 'Section Title',\n        control: 'textfield',\n        cssClass: 'l-inline',\n        required: true,\n        property: ['configuration', 'sectionTitle']\n      },\n      {\n        key: 'pageTitle',\n        name: 'Page Title',\n        control: 'textfield',\n        cssClass: 'l-inline',\n        required: true,\n        property: ['configuration', 'pageTitle']\n      }\n    ];\n  }\n\n  initialize(domainObject) {\n    domainObject.configuration = {\n      defaultSort: 'oldest',\n      entries: {},\n      imageMigrationVer: IMAGE_MIGRATION_VER,\n      pageTitle: 'Page',\n      sections: [],\n      sectionTitle: 'Section',\n      type: 'General'\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/NotebookViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Agent from '@/utils/agent/Agent';\n\nimport Notebook from './components/NotebookComponent.vue';\n\nexport default class NotebookViewProvider {\n  constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) {\n    this.openmct = openmct;\n    this.key = key;\n    this.name = `${name} View`;\n    this.type = type;\n    this.cssClass = cssClass;\n    this.snapshotContainer = snapshotContainer;\n    this.entryUrlWhitelist = entryUrlWhitelist;\n  }\n\n  canView(domainObject) {\n    return domainObject.type === this.type;\n  }\n\n  view(domainObject) {\n    let openmct = this.openmct;\n    let snapshotContainer = this.snapshotContainer;\n    let agent = new Agent(window);\n    let entryUrlWhitelist = this.entryUrlWhitelist;\n    let _destroy = null;\n\n    return {\n      show(container) {\n        const { destroy } = mount(\n          {\n            el: container,\n            components: {\n              Notebook\n            },\n            provide: {\n              openmct,\n              snapshotContainer,\n              agent,\n              entryUrlWhitelist\n            },\n            data() {\n              return {\n                domainObject\n              };\n            },\n            template: '<Notebook :domain-object=\"domainObject\"></Notebook>'\n          },\n          {\n            app: openmct.app,\n            element: container\n          }\n        );\n        _destroy = destroy;\n      },\n      destroy() {\n        if (_destroy) {\n          _destroy();\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/actions/CopyToNotebookAction.js",
    "content": "import { addNotebookEntry } from '../utils/notebook-entries.js';\nimport { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage.js';\n\nconst COPY_TO_NOTEBOOK_ACTION_KEY = 'copyToNotebook';\nclass CopyToNotebookAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-duplicate';\n    this.description = 'Copy value to notebook as an entry';\n    this.group = 'action';\n    this.key = COPY_TO_NOTEBOOK_ACTION_KEY;\n    this.name = 'Copy to Notebook';\n    this.priority = 1;\n  }\n\n  copyToNotebook(entryText) {\n    const notebookStorage = getDefaultNotebook();\n    this.openmct.objects.get(notebookStorage.identifier).then((domainObject) => {\n      addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);\n\n      const { section, page } = getNotebookSectionAndPage(\n        domainObject,\n        notebookStorage.defaultSectionId,\n        notebookStorage.defaultPageId\n      );\n      if (!section || !page) {\n        return;\n      }\n\n      const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;\n      const msg = `Saved to Notebook ${defaultPath}`;\n      this.openmct.notifications.info(msg);\n    });\n  }\n\n  invoke(objectPath, view) {\n    const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy;\n\n    this.copyToNotebook(formattedValueForCopy());\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const viewContext = view.getViewContext && view.getViewContext();\n    const row = viewContext && viewContext.row;\n    if (!row) {\n      return;\n    }\n\n    return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function';\n  }\n}\n\nexport { COPY_TO_NOTEBOOK_ACTION_KEY };\n\nexport default CopyToNotebookAction;\n"
  },
  {
    "path": "src/plugins/notebook/actions/ExportNotebookAsTextAction.js",
    "content": "import { saveAs } from 'file-saver';\nimport Moment from 'moment';\n\nimport { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants.js';\nconst UNKNOWN_USER = 'Unknown';\nconst UNKNOWN_TIME = 'Unknown';\nconst ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE];\nconst EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY = 'exportNotebookAsText';\n\nclass ExportNotebookAsTextAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-export';\n    this.description = 'Exports notebook contents as a text file';\n    this.group = 'export';\n    this.key = EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY;\n    this.name = 'Export Notebook as Text';\n  }\n\n  invoke(objectPath) {\n    this.showForm(objectPath);\n  }\n\n  getTagName(tagId, availableTags) {\n    const foundTag = availableTags.find((tag) => tag.id === tagId);\n    if (foundTag) {\n      return foundTag.label;\n    } else {\n      return tagId;\n    }\n  }\n\n  getTagsForEntry(entry, domainObjectKeyString, annotations) {\n    const foundTags = [];\n    annotations.forEach((annotation) => {\n      const target = annotation.targets.find(\n        (annotationTarget) => annotationTarget.keyString === domainObjectKeyString\n      );\n      if (target?.entryId === entry.id) {\n        annotation.tags.forEach((tag) => {\n          if (!foundTags.includes(tag)) {\n            foundTags.push(tag);\n          }\n        });\n      }\n    });\n\n    return foundTags;\n  }\n\n  formatTimeStamp(timestamp) {\n    if (timestamp) {\n      return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`;\n    } else {\n      return UNKNOWN_TIME;\n    }\n  }\n\n  appliesTo(objectPath) {\n    const domainObject = objectPath[0];\n\n    return ALLOWED_TYPES.includes(domainObject.type);\n  }\n\n  async onSave(changes, objectPath) {\n    const availableTags = this.openmct.annotation.getAvailableTags();\n    const identifier = objectPath[0].identifier;\n    const domainObject = await this.openmct.objects.get(identifier);\n    let foundAnnotations = [];\n    // only load annotations if there are tags\n    if (availableTags.length) {\n      foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier);\n    }\n\n    let notebookAsText = `# ${domainObject.name}\\n\\n`;\n\n    if (changes.exportMetaData) {\n      const createdTimestamp = domainObject.created;\n      const createdBy = this.getUserName(domainObject.createdBy);\n      const modifiedBy = this.getUserName(domainObject.modifiedBy);\n      const modifiedTimestamp = domainObject.modified ?? domainObject.created;\n      notebookAsText += `Created on ${this.formatTimeStamp(\n        createdTimestamp\n      )} by user ${createdBy}\\n\\n`;\n      notebookAsText += `Updated on ${this.formatTimeStamp(\n        modifiedTimestamp\n      )} by user ${modifiedBy}\\n\\n`;\n    }\n\n    const notebookSections = domainObject.configuration.sections;\n    const notebookEntries = domainObject.configuration.entries;\n\n    notebookSections.forEach((section) => {\n      notebookAsText += `## ${section.name}\\n\\n`;\n\n      const notebookPages = section.pages;\n\n      notebookPages.forEach((page) => {\n        notebookAsText += `### ${page.name}\\n\\n`;\n\n        const notebookPageEntries = notebookEntries[section.id]?.[page.id];\n        if (!notebookPageEntries) {\n          // blank page\n          return;\n        }\n\n        notebookPageEntries.forEach((entry) => {\n          if (changes.exportMetaData) {\n            const createdTimestamp = entry.createdOn;\n            const createdBy = this.getUserName(entry.createdBy);\n            const createdByRole = entry.createdByRole;\n            const modifiedBy = this.getUserName(entry.modifiedBy);\n            const modifiedByRole = entry.modifiedByRole;\n            const modifiedTimestamp = entry.modified ?? entry.created;\n            notebookAsText += `Created on ${this.formatTimeStamp(\n              createdTimestamp\n            )} by user ${createdBy}${createdByRole ? `: ${createdByRole}` : ''}\\n\\n`;\n            notebookAsText += `Updated on ${this.formatTimeStamp(\n              modifiedTimestamp\n            )} by user ${modifiedBy}${modifiedByRole ? `: ${modifiedByRole}` : ''}\\n\\n`;\n          }\n\n          if (changes.exportTags) {\n            const domainObjectKeyString = this.openmct.objects.makeKeyString(\n              domainObject.identifier\n            );\n            const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations);\n            const tagNames = tags.map((tag) => this.getTagName(tag, availableTags));\n            if (tagNames) {\n              notebookAsText += `Tags: ${tagNames.join(', ')}\\n\\n`;\n            }\n          }\n\n          notebookAsText += `${entry.text}\\n\\n`;\n        });\n      });\n    });\n\n    const blob = new Blob([notebookAsText], { type: 'text/markdown' });\n    const fileName = domainObject.name + '.md';\n    saveAs(blob, fileName);\n  }\n\n  getUserName(userId) {\n    if (userId && userId.length) {\n      return userId;\n    }\n\n    return UNKNOWN_USER;\n  }\n\n  async showForm(objectPath) {\n    const formStructure = {\n      title: 'Export Notebook Text',\n      sections: [\n        {\n          rows: [\n            {\n              key: 'exportMetaData',\n              control: 'toggleSwitch',\n              name: 'Include Metadata (created/modified, etc.)',\n              required: true,\n              value: false\n            },\n            {\n              name: 'Include Tags',\n              control: 'toggleSwitch',\n              required: true,\n              key: 'exportTags',\n              value: false\n            }\n          ]\n        }\n      ]\n    };\n\n    const changes = await this.openmct.forms.showForm(formStructure);\n\n    return this.onSave(changes, objectPath);\n  }\n}\n\nexport { EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY };\n\nexport default ExportNotebookAsTextAction;\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-notebook\" :class=\"[{ 'c-notebook--restricted': isRestricted }]\">\n    <div class=\"c-notebook__head\">\n      <Search\n        class=\"c-notebook__search\"\n        :value=\"search\"\n        @input=\"search = $event\"\n        @clear=\"resetSearch()\"\n      />\n    </div>\n    <SearchResults\n      v-if=\"search.length\"\n      ref=\"searchResults\"\n      :domain-object=\"domainObject\"\n      :results=\"searchResults\"\n      @cancel-edit=\"cancelTransaction\"\n      @editing-entry=\"startTransaction\"\n      @change-section-page=\"changeSelectedSection\"\n      @update-entries=\"updateEntries\"\n    />\n    <div v-if=\"!search.length\" class=\"c-notebook__body\">\n      <Sidebar\n        ref=\"sidebar\"\n        class=\"c-notebook__nav c-sidebar c-drawer c-drawer--align-left\"\n        :class=\"sidebarClasses\"\n        :default-page-id=\"defaultPageId\"\n        :selected-page-id=\"getSelectedPageId()\"\n        :default-section-id=\"defaultSectionId\"\n        :selected-section-id=\"getSelectedSectionId()\"\n        :domain-object=\"domainObject\"\n        :page-title=\"domainObject.configuration.pageTitle\"\n        :section-title=\"domainObject.configuration.sectionTitle\"\n        :sections=\"sections\"\n        :sidebar-covers-entries=\"sidebarCoversEntries\"\n        @default-page-deleted=\"cleanupDefaultNotebook\"\n        @default-section-deleted=\"cleanupDefaultNotebook\"\n        @pages-changed=\"pagesChanged\"\n        @select-page=\"selectPage\"\n        @sections-changed=\"sectionsChanged\"\n        @select-section=\"selectSection\"\n        @toggle-nav=\"toggleNav\"\n      />\n      <div\n        class=\"c-notebook__page-view\"\n        :class=\"{\n          'c-notebook--page-locked': selectedPage?.isLocked,\n          'c-notebook--page-unlocked': !selectedPage?.isLocked\n        }\"\n      >\n        <div class=\"c-notebook__page-view__header\">\n          <button\n            class=\"c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger\"\n            @click=\"toggleNav\"\n          ></button>\n          <div class=\"c-notebook__page-view__path c-path\">\n            <span class=\"c-notebook__path__section c-path__item\">\n              {{ selectedSection ? selectedSection.name : '' }}\n            </span>\n            <span class=\"c-notebook__path__page c-path__item\">\n              {{ selectedPage ? selectedPage.name : '' }}\n            </span>\n          </div>\n          <div class=\"c-notebook__page-view__controls\">\n            <select v-model=\"showTime\" class=\"c-notebook__controls__time\">\n              <option value=\"0\" :selected=\"showTime === 0\">Show all</option>\n              <option value=\"1\" :selected=\"showTime === 1\">Last hour</option>\n              <option value=\"8\" :selected=\"showTime === 8\">Last 8 hours</option>\n              <option value=\"24\" :selected=\"showTime === 24\">Last 24 hours</option>\n            </select>\n            <select v-model=\"defaultSort\" class=\"c-notebook__controls__time\">\n              <option value=\"newest\" :selected=\"defaultSort === 'newest'\">Newest first</option>\n              <option value=\"oldest\" :selected=\"defaultSort === 'oldest'\">Oldest first</option>\n            </select>\n          </div>\n        </div>\n        <div\n          v-if=\"selectedPage && !selectedPage.isLocked\"\n          :aria-disabled=\"activeTransaction\"\n          class=\"c-notebook__drag-area icon-plus\"\n          aria-dropeffect=\"link\"\n          aria-labelledby=\"newEntryLabel\"\n          @click=\"newEntry(null, $event)\"\n          @dragover=\"dragOver\"\n          @drop.capture=\"dropCapture\"\n          @drop=\"dropOnEntry($event)\"\n        >\n          <span id=\"newEntryLabel\" class=\"c-notebook__drag-area__label\">\n            To start a new entry, click here or drag and drop any object\n          </span>\n        </div>\n        <ProgressBar\n          v-if=\"savingTransaction\"\n          class=\"c-telemetry-table__progress-bar\"\n          :model=\"{ progressPerc: null }\"\n        />\n        <div v-if=\"selectedPage && selectedPage.isLocked\" class=\"c-notebook__page-locked-message\">\n          <div class=\"icon-lock\"></div>\n          <div class=\"c-notebook__page-locked-message-text\">\n            This page has been committed and cannot be modified or removed\n          </div>\n        </div>\n        <div\n          v-if=\"selectedSection && selectedPage\"\n          ref=\"notebookEntries\"\n          class=\"c-notebook__entries\"\n          aria-label=\"Notebook Entries\"\n        >\n          <NotebookEntry\n            v-for=\"entry in filteredAndSortedEntries\"\n            :key=\"entry.id\"\n            :entry=\"entry\"\n            :domain-object=\"domainObject\"\n            :notebook-annotations=\"notebookAnnotations[entry.id]\"\n            :selected-page=\"selectedPage\"\n            :selected-section=\"selectedSection\"\n            :read-only=\"false\"\n            :is-locked=\"selectedPage.isLocked\"\n            :selected-entry-id=\"selectedEntryId\"\n            @cancel-edit=\"cancelTransaction\"\n            @editing-entry=\"startTransaction\"\n            @delete-entry=\"deleteEntry\"\n            @update-entry=\"updateEntry\"\n            @update-annotations=\"loadAnnotations\"\n            @entry-selection=\"entrySelection(entry)\"\n          />\n        </div>\n        <div\n          v-if=\"isRestricted && filteredAndSortedEntries?.length > 0 && !selectedPage.isLocked\"\n          class=\"c-notebook__commit-entries-control\"\n        >\n          <button\n            class=\"c-button commit-button icon-lock\"\n            title=\"Commit entries and lock this page from further changes\"\n            aria-labelledby=\"commitEntriesLabel\"\n            @click=\"lockPage()\"\n          >\n            <span id=\"commitEntriesLabel\" class=\"c-button__label\">Commit Entries</span>\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { debounce } from 'lodash';\n\nimport Search from '@/ui/components/SearchComponent.vue';\n\nimport ProgressBar from '../../../ui/components/ProgressBar.vue';\nimport objectLink from '../../../ui/mixins/object-link.js';\nimport { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants.js';\nimport {\n  addNotebookEntry,\n  createNewEmbed,\n  createNewImageEmbed,\n  getEntryPosById,\n  getNotebookEntries,\n  mutateObject,\n  selectEntry\n} from '../utils/notebook-entries.js';\nimport {\n  saveNotebookImageDomainObject,\n  updateNamespaceOfDomainObject\n} from '../utils/notebook-image.js';\nimport {\n  clearDefaultNotebook,\n  getDefaultNotebook,\n  setDefaultNotebook,\n  setDefaultNotebookPageId,\n  setDefaultNotebookSectionId\n} from '../utils/notebook-storage.js';\nimport NotebookEntry from './NotebookEntry.vue';\nimport SearchResults from './SearchResults.vue';\nimport Sidebar from './SidebarComponent.vue';\nfunction objectCopy(obj) {\n  return JSON.parse(JSON.stringify(obj));\n}\n\nexport default {\n  components: {\n    NotebookEntry,\n    Search,\n    SearchResults,\n    Sidebar,\n    ProgressBar\n  },\n  inject: ['agent', 'openmct', 'snapshotContainer'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      defaultPageId: this.getDefaultPageId(),\n      defaultSectionId: this.getDefaultSectionId(),\n      selectedSectionId: this.getSelectedSectionId(),\n      selectedPageId: this.getSelectedPageId(),\n      defaultSort: this.domainObject.configuration.defaultSort,\n      focusEntryId: null,\n      isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,\n      search: '',\n      searchResults: [],\n      lastLocalAnnotationCreation: 0,\n      showTime: this.domainObject.configuration.showTime || 0,\n      showNav: false,\n      sidebarCoversEntries: false,\n      filteredAndSortedEntries: [],\n      notebookAnnotations: {},\n      selectedEntryId: undefined,\n      activeTransaction: false,\n      savingTransaction: false,\n      sections: this.domainObject.configuration.sections || []\n    };\n  },\n  computed: {\n    pages() {\n      return this.getPages() || [];\n    },\n    selectedPage() {\n      const pages = this.getPages();\n      if (!pages.length) {\n        return undefined;\n      }\n\n      const selectedPage = pages.find((page) => page.id === this.selectedPageId);\n      if (selectedPage) {\n        return selectedPage;\n      }\n\n      const defaultPage = pages.find((page) => page.id === this.defaultPageId);\n      if (defaultPage) {\n        return defaultPage;\n      }\n\n      return this.pages[0];\n    },\n    selectedSection() {\n      if (!this.sections.length) {\n        return undefined;\n      }\n\n      const selectedSection = this.sections.find(\n        (section) => section.id === this.selectedSectionId\n      );\n      if (selectedSection) {\n        return selectedSection;\n      }\n\n      const defaultSection = this.sections.find((section) => section.id === this.defaultSectionId);\n      if (defaultSection) {\n        return defaultSection;\n      }\n\n      return this.sections[0];\n    },\n    sidebarClasses() {\n      let sidebarClasses = [];\n      if (this.showNav) {\n        sidebarClasses.push('is-expanded');\n      }\n\n      if (this.sidebarCoversEntries) {\n        sidebarClasses.push('c-drawer--overlays');\n      } else {\n        sidebarClasses.push('c-drawer--push');\n      }\n\n      return sidebarClasses;\n    }\n  },\n  watch: {\n    search() {\n      this.getSearchResults();\n    },\n    defaultSort() {\n      mutateObject(this.openmct, this.domainObject, 'configuration.defaultSort', this.defaultSort);\n      this.filterAndSortEntries();\n    },\n    showTime() {\n      mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);\n    },\n    notebookAnnotations: {\n      handler() {\n        this.filterAndSortEntries();\n      },\n      deep: true\n    }\n  },\n  beforeMount() {\n    this.getSearchResults = debounce(this.getSearchResults, 500);\n    this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);\n  },\n  async created() {\n    this.transaction = null;\n    this.abortController = new AbortController();\n    try {\n      await this.loadAnnotations();\n    } catch (err) {\n      if (err.name !== 'AbortError') {\n        throw err;\n      }\n    }\n  },\n  mounted() {\n    this.formatSidebar();\n    this.setSectionAndPageFromUrl();\n\n    this.openmct.selection.on('change', this.updateSelection);\n\n    window.addEventListener('orientationchange', this.formatSidebar);\n    window.addEventListener('hashchange', this.setSectionAndPageFromUrl);\n    this.filterAndSortEntries();\n    this.unobserveEntries = this.openmct.objects.observe(\n      this.domainObject,\n      '*',\n      this.filterAndSortEntries\n    );\n  },\n  beforeUnmount() {\n    this.abortController.abort();\n    if (this.unlisten) {\n      this.unlisten();\n    }\n\n    if (this.unobserveEntries) {\n      this.unobserveEntries();\n    }\n\n    Object.keys(this.notebookAnnotations).forEach((entryID) => {\n      const notebookAnnotationsForEntry = this.notebookAnnotations[entryID];\n      notebookAnnotationsForEntry.forEach((notebookAnnotation) => {\n        this.openmct.objects.destroyMutable(notebookAnnotation);\n      });\n    });\n\n    window.removeEventListener('orientationchange', this.formatSidebar);\n    window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);\n    this.openmct.selection.off('change', this.updateSelection);\n  },\n  updated: function () {\n    this.$nextTick(() => {\n      this.focusOnEntryId();\n    });\n  },\n  methods: {\n    changeSectionPage(newParams, oldParams, changedParams) {\n      if (isNotebookViewType(newParams.view)) {\n        return;\n      }\n\n      let pageId = newParams.pageId;\n      let sectionId = newParams.sectionId;\n\n      if (!pageId && !sectionId) {\n        return;\n      }\n\n      this.sections.forEach((section) => {\n        section.isSelected = Boolean(section.id === sectionId);\n\n        if (section.isSelected) {\n          section.pages.forEach((page) => {\n            page.isSelected = Boolean(page.id === pageId);\n          });\n        }\n      });\n    },\n    updateSelection(selection) {\n      const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      if (selection?.[0]?.[0]?.context?.targetDetails?.[keyString]?.entryId === undefined) {\n        this.selectedEntryId = undefined;\n      }\n    },\n    async loadAnnotations() {\n      if (!this.openmct.annotation.getAvailableTags().length) {\n        // don't bother loading annotations if there are no tags\n        return;\n      }\n\n      this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;\n\n      const foundAnnotations = await this.openmct.annotation.getAnnotations(\n        this.domainObject.identifier,\n        this.abortController.signal\n      );\n\n      foundAnnotations.forEach((foundAnnotation) => {\n        const target = foundAnnotation.targets?.[0];\n        const entryId = target.entryId;\n        if (!this.notebookAnnotations[entryId]) {\n          this.notebookAnnotations[entryId] = [];\n        }\n\n        const annotationExtant = this.notebookAnnotations[entryId].some((existingAnnotation) => {\n          return this.openmct.objects.areIdsEqual(\n            existingAnnotation.identifier,\n            foundAnnotation.identifier\n          );\n        });\n        if (!annotationExtant) {\n          const annotationArray = this.notebookAnnotations[entryId];\n          const mutableAnnotation = this.openmct.objects.toMutable(foundAnnotation);\n          annotationArray.push(mutableAnnotation);\n        }\n      });\n    },\n    filterAndSortEntries() {\n      const filterTime = this.openmct.time.now();\n      const pageEntries =\n        getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];\n      const hours = parseInt(this.showTime, 10);\n      const filteredPageEntriesByTime = hours\n        ? pageEntries.filter((entry) => filterTime - entry.createdOn <= hours * 60 * 60 * 1000)\n        : pageEntries;\n\n      this.filteredAndSortedEntries =\n        this.defaultSort === 'oldest'\n          ? filteredPageEntriesByTime\n          : [...filteredPageEntriesByTime].reverse();\n\n      if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {\n        this.loadAnnotations().catch((err) => {\n          if (err.name !== 'AbortError') {\n            throw err;\n          }\n        });\n      }\n    },\n    changeSelectedSection({ sectionId, pageId }) {\n      const sections = this.sections.map((s) => {\n        s.isSelected = false;\n\n        if (s.id === sectionId) {\n          s.isSelected = true;\n        }\n\n        s.pages.forEach((p, i) => {\n          p.isSelected = false;\n\n          if (pageId && pageId === p.id) {\n            p.isSelected = true;\n          }\n\n          if (!pageId && i === 0) {\n            p.isSelected = true;\n          }\n        });\n\n        return s;\n      });\n\n      this.sectionsChanged({ sections });\n      this.resetSearch();\n    },\n    cleanupDefaultNotebook() {\n      this.defaultPageId = undefined;\n      this.defaultSectionId = undefined;\n      this.removeDefaultClass(this.domainObject.identifier);\n      clearDefaultNotebook();\n    },\n    lockPage() {\n      let prompt = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message:\n          'This action will lock this page and disallow any new entries, or editing of existing entries. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Lock Page',\n            callback: () => {\n              this.selectedPage.isLocked = true;\n\n              // cant be default if it's locked\n              if (this.selectedPage.id === this.defaultPageId) {\n                this.cleanupDefaultNotebook();\n              }\n\n              if (!this.selectedSection.isLocked) {\n                this.selectedSection.isLocked = true;\n              }\n\n              mutateObject(\n                this.openmct,\n                this.domainObject,\n                'configuration.sections',\n                this.sections\n              );\n\n              if (!this.domainObject.locked) {\n                mutateObject(this.openmct, this.domainObject, 'locked', true);\n              }\n\n              prompt.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              prompt.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    setSectionAndPageFromUrl() {\n      let sectionId =\n        this.getSectionIdFromUrl() || this.getDefaultSectionId() || this.getSelectedSectionId();\n      let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();\n\n      this.selectSection(sectionId);\n      this.selectPage(pageId);\n    },\n    createNotebookStorageObject() {\n      const page = this.selectedPage;\n      const section = this.selectedSection;\n\n      return {\n        name: this.domainObject.name,\n        identifier: this.domainObject.identifier,\n        link: this.getLinktoNotebook(),\n        defaultSectionId: section.id,\n        defaultPageId: page.id\n      };\n    },\n    deleteEntry(entryId) {\n      const entryPos = getEntryPosById(\n        entryId,\n        this.domainObject,\n        this.selectedSection,\n        this.selectedPage\n      );\n      if (entryPos === -1) {\n        this.openmct.notifications.alert('Warning: unable to delete entry');\n        console.error(\n          `unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`\n        );\n\n        this.cancelTransaction();\n\n        return;\n      }\n\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will permanently delete this entry. Do you wish to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              const entries = getNotebookEntries(\n                this.domainObject,\n                this.selectedSection,\n                this.selectedPage\n              );\n              if (entries) {\n                entries.splice(entryPos, 1);\n                this.updateEntries(entries);\n                this.filterAndSortEntries();\n                this.removeAnnotations(entryId);\n              } else {\n                this.cancelTransaction();\n              }\n\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    removeAnnotations(entryId) {\n      if (this.notebookAnnotations[entryId]) {\n        this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]);\n      }\n    },\n    checkEntryPos(entry) {\n      const entryPos = getEntryPosById(\n        entry.id,\n        this.domainObject,\n        this.selectedSection,\n        this.selectedPage\n      );\n      if (entryPos === -1) {\n        this.openmct.notifications.alert('Warning: unable to tag entry');\n        console.error(\n          `unable to tag entry ${entry} from section ${this.selectedSection}, page ${this.selectedPage}`\n        );\n\n        return false;\n      }\n\n      return true;\n    },\n    dragOver(event) {\n      event.preventDefault();\n      event.dataTransfer.dropEffect = 'copy';\n    },\n    dropCapture(event) {\n      const isEditing = this.openmct.editor.isEditing();\n      if (isEditing) {\n        this.openmct.editor.cancel();\n      }\n    },\n    async dropOnEntry(dropEvent) {\n      dropEvent.preventDefault();\n      dropEvent.stopImmediatePropagation();\n\n      const dataTransferFiles = Array.from(dropEvent.dataTransfer.files);\n      const localImageDropped = dataTransferFiles.some((file) => file.type.includes('image'));\n      const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id');\n      const domainObjectData = dropEvent.dataTransfer.getData('openmct/domain-object-path');\n      const imageUrl = dropEvent.dataTransfer.getData('URL');\n      if (localImageDropped) {\n        // local image(s) dropped from disk (file)\n        const embeds = [];\n        await Promise.all(\n          dataTransferFiles.map(async (file) => {\n            if (file.type.includes('image')) {\n              const imageData = file;\n              const imageEmbed = await createNewImageEmbed(\n                imageData,\n                this.openmct,\n                imageData?.name\n              );\n              embeds.push(imageEmbed);\n            }\n          })\n        );\n        this.newEntry(embeds);\n      } else if (imageUrl) {\n        // remote image dropped (URL)\n        try {\n          const response = await fetch(imageUrl);\n          const imageData = await response.blob();\n          const imageEmbed = await createNewImageEmbed(imageData, this.openmct);\n          this.newEntry([imageEmbed]);\n        } catch (error) {\n          this.openmct.notifications.alert(`Unable to add image: ${error.message} `);\n          console.error(`Problem embedding remote image`, error);\n        }\n      } else if (snapshotId.length) {\n        // snapshot object\n        const snapshot = this.snapshotContainer.getSnapshot(snapshotId);\n        this.newEntry([snapshot.embedObject]);\n        this.snapshotContainer.removeSnapshot(snapshotId);\n\n        const namespace = this.domainObject.identifier.namespace;\n        const notebookImageDomainObject = updateNamespaceOfDomainObject(\n          snapshot.notebookImageDomainObject,\n          namespace\n        );\n        saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);\n      } else if (domainObjectData) {\n        // plain domain object\n        const objectPath = JSON.parse(domainObjectData);\n        const bounds = this.openmct.time.getBounds();\n        const snapshotMeta = {\n          bounds,\n          link: null,\n          objectPath,\n          openmct: this.openmct\n        };\n        const embed = await createNewEmbed(snapshotMeta);\n        this.newEntry([embed]);\n      } else {\n        this.openmct.notifications.error(\n          `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.`\n        );\n        console.warn(\n          `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.`\n        );\n        return;\n      }\n    },\n    focusOnEntryId() {\n      if (!this.focusEntryId) {\n        return;\n      }\n\n      const element = this.$refs.notebookEntries.querySelector(`#${this.focusEntryId}`);\n\n      if (!element) {\n        return;\n      }\n\n      element.focus();\n      this.focusEntryId = null;\n    },\n    formatSidebar() {\n      /*\n                Determine if the sidebar should slide over content, or compress it\n                Slide over checks:\n                - phone (all orientations)\n                - tablet portrait\n                - in a layout frame (within .c-so-view)\n            */\n      const isPhone = this.agent.isPhone();\n      const isTablet = this.agent.isTablet();\n      const isPortrait = this.agent.isPortrait();\n      const isInLayout = Boolean(this.$el.closest('.c-so-view'));\n      const sidebarCoversEntries = isPhone || (isTablet && isPortrait) || isInLayout;\n      this.sidebarCoversEntries = sidebarCoversEntries;\n    },\n    getDefaultPageId() {\n      return this.isDefaultNotebook() ? getDefaultNotebook().defaultPageId : undefined;\n    },\n    isDefaultNotebook() {\n      const defaultNotebook = getDefaultNotebook();\n      const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;\n\n      return (\n        defaultNotebookIdentifier !== null &&\n        this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier)\n      );\n    },\n    getDefaultSectionId() {\n      return this.isDefaultNotebook() ? getDefaultNotebook().defaultSectionId : undefined;\n    },\n    getLinktoNotebook() {\n      const objectPath = this.openmct.router.path;\n      const link = objectLink.computed.objectLink.call({\n        objectPath,\n        openmct: this.openmct\n      });\n\n      const selectedSection = this.selectedSection;\n      const selectedPage = this.selectedPage;\n      const sectionId = selectedSection ? selectedSection.id : '';\n      const pageId = selectedPage ? selectedPage.id : '';\n\n      return `${link}?sectionId=${sectionId}&pageId=${pageId}`;\n    },\n    getPage(section, id) {\n      return section.pages.find((p) => p.id === id);\n    },\n    getSection(id) {\n      return this.sections.find((s) => s.id === id);\n    },\n    getSearchResults() {\n      if (!this.search.length) {\n        return [];\n      }\n\n      const output = [];\n      const sections = this.domainObject.configuration.sections;\n      const entries = this.domainObject.configuration.entries;\n      const searchTextLower = this.search.toLowerCase();\n      const originalSearchText = this.search;\n      let sectionTrackPageHit;\n      let pageTrackEntryHit;\n      let sectionTrackEntryHit;\n\n      sections.forEach((section) => {\n        const pages = section.pages;\n        let resultMetadata = {\n          originalSearchText,\n          sectionHit: section.name && section.name.toLowerCase().includes(searchTextLower)\n        };\n        sectionTrackPageHit = false;\n        sectionTrackEntryHit = false;\n\n        pages.forEach((page) => {\n          resultMetadata.pageHit = page.name && page.name.toLowerCase().includes(searchTextLower);\n          pageTrackEntryHit = false;\n\n          if (resultMetadata.pageHit) {\n            sectionTrackPageHit = true;\n          }\n\n          // check for no entries first\n          if (entries[section.id] && entries[section.id][page.id]) {\n            const pageEntries = entries[section.id][page.id];\n\n            pageEntries.forEach((entry) => {\n              const entryHit = entry.text && entry.text.toLowerCase().includes(searchTextLower);\n\n              // any entry hit goes in, it's the most unique of the hits\n              if (entryHit) {\n                resultMetadata.entryHit = entryHit;\n                pageTrackEntryHit = true;\n                sectionTrackEntryHit = true;\n\n                output.push(\n                  objectCopy({\n                    metadata: resultMetadata,\n                    section,\n                    page,\n                    entry\n                  })\n                );\n              }\n            });\n          }\n\n          // all entries checked, now in pages,\n          // if page hit, but not in results, need to add\n          if (resultMetadata.pageHit && !pageTrackEntryHit) {\n            resultMetadata.entryHit = false;\n\n            output.push(\n              objectCopy({\n                metadata: resultMetadata,\n                section,\n                page\n              })\n            );\n          }\n        });\n\n        // all pages checked, now in sections,\n        // if section hit, but not in results, need to add and default page\n        if (resultMetadata.sectionHit && !sectionTrackPageHit && !sectionTrackEntryHit) {\n          resultMetadata.entryHit = false;\n          resultMetadata.pageHit = false;\n\n          output.push(\n            objectCopy({\n              metadata: resultMetadata,\n              section,\n              page: pages[0]\n            })\n          );\n        }\n      });\n\n      this.searchResults = output;\n    },\n    getPages() {\n      if (!this.selectedSection || !this.selectedSection.pages.length) {\n        return [];\n      }\n\n      return this.selectedSection.pages;\n    },\n    getSelectedPageId() {\n      return this.selectedPage?.id;\n    },\n    getSelectedSectionId() {\n      return this.selectedSection?.id;\n    },\n    async newEntry(embeds, event) {\n      this.startTransaction();\n      this.resetSearch();\n      const notebookStorage = this.createNotebookStorageObject();\n      this.updateDefaultNotebook(notebookStorage);\n      const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embeds);\n\n      const element = this.$refs.notebookEntries.querySelector(`#${id}`);\n      const entryAnnotations = this.notebookAnnotations[id] ?? {};\n      selectEntry({\n        element,\n        entryId: id,\n        domainObject: this.domainObject,\n        openmct: this.openmct,\n        notebookAnnotations: entryAnnotations\n      });\n      if (event) {\n        event.stopPropagation();\n      }\n\n      this.filterAndSortEntries();\n      this.focusEntryId = id;\n      this.selectedEntryId = id;\n\n      // put entry into edit mode\n      this.$nextTick(() => {\n        element.dispatchEvent(new Event('click'));\n      });\n    },\n    orientationChange() {\n      this.formatSidebar();\n    },\n    pagesChanged({ pages = [], id = null }) {\n      const selectedSection = this.selectedSection;\n      if (!selectedSection) {\n        return;\n      }\n\n      selectedSection.pages = pages;\n      const sections = this.sections.map((section) => {\n        if (section.id === selectedSection.id) {\n          section = selectedSection;\n        }\n\n        return section;\n      });\n\n      this.sectionsChanged({ sections });\n    },\n    removeDefaultClass(identifier) {\n      this.openmct.status.delete(identifier);\n    },\n    resetSearch() {\n      this.search = '';\n      this.searchResults = [];\n    },\n    toggleNav() {\n      this.showNav = !this.showNav;\n    },\n    updateDefaultNotebook(updatedNotebookStorageObject) {\n      if (!this.isDefaultNotebook()) {\n        const persistedNotebookStorageObject = getDefaultNotebook();\n        if (\n          persistedNotebookStorageObject &&\n          persistedNotebookStorageObject.identifier !== undefined\n        ) {\n          this.removeDefaultClass(persistedNotebookStorageObject.identifier);\n        }\n\n        setDefaultNotebook(this.openmct, updatedNotebookStorageObject, this.domainObject);\n      }\n\n      if (this.defaultSectionId !== updatedNotebookStorageObject.defaultSectionId) {\n        setDefaultNotebookSectionId(updatedNotebookStorageObject.defaultSectionId);\n        this.defaultSectionId = updatedNotebookStorageObject.defaultSectionId;\n      }\n\n      if (this.defaultPageId !== updatedNotebookStorageObject.defaultPageId) {\n        setDefaultNotebookPageId(updatedNotebookStorageObject.defaultPageId);\n        this.defaultPageId = updatedNotebookStorageObject.defaultPageId;\n      }\n    },\n    updateDefaultNotebookSection(sections, id) {\n      if (!id) {\n        return;\n      }\n\n      const notebookStorage = getDefaultNotebook();\n      if (!notebookStorage || notebookStorage.identifier.key !== this.domainObject.identifier.key) {\n        return;\n      }\n\n      const defaultNotebookSectionId = notebookStorage.defaultSectionId;\n      if (defaultNotebookSectionId === id) {\n        const section = sections.find((s) => s.id === id);\n        if (!section) {\n          this.removeDefaultClass(this.domainObject.identifier);\n          clearDefaultNotebook();\n\n          return;\n        }\n      }\n\n      if (id !== defaultNotebookSectionId) {\n        return;\n      }\n\n      setDefaultNotebookSectionId(defaultNotebookSectionId);\n    },\n    updateEntry(entry) {\n      const entries = getNotebookEntries(\n        this.domainObject,\n        this.selectedSection,\n        this.selectedPage\n      );\n      const entryPos = getEntryPosById(\n        entry.id,\n        this.domainObject,\n        this.selectedSection,\n        this.selectedPage\n      );\n      entries[entryPos] = entry;\n\n      this.updateEntries(entries);\n    },\n    updateEntries(entries) {\n      const configuration = this.domainObject.configuration;\n      const notebookEntries = configuration.entries || {};\n      notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;\n\n      mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);\n\n      this.saveTransaction();\n    },\n    getPageIdFromUrl() {\n      return this.openmct.router.getParams().pageId;\n    },\n    getSectionIdFromUrl() {\n      return this.openmct.router.getParams().sectionId;\n    },\n    syncUrlWithPageAndSection() {\n      this.openmct.router.updateParams({\n        pageId: this.selectedPageId,\n        sectionId: this.selectedSectionId\n      });\n    },\n    sectionsChanged({ sections, id = undefined }) {\n      this.sections = [...sections];\n      this.startTransaction();\n      mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);\n      this.saveTransaction();\n      this.updateDefaultNotebookSection(sections, id);\n    },\n    selectPage(pageId) {\n      if (!pageId) {\n        return;\n      }\n\n      this.selectedPageId = pageId;\n      this.syncUrlWithPageAndSection();\n      this.filterAndSortEntries();\n    },\n    selectSection(sectionId) {\n      if (!sectionId) {\n        return;\n      }\n\n      this.selectedSectionId = sectionId;\n\n      const pageId = this.selectedSection.pages[0].id;\n      this.selectPage(pageId);\n\n      this.syncUrlWithPageAndSection();\n      this.filterAndSortEntries();\n    },\n    startTransaction() {\n      if (!this.openmct.objects.isTransactionActive()) {\n        this.activeTransaction = true;\n        this.transaction = this.openmct.objects.startTransaction();\n      }\n    },\n    async saveTransaction() {\n      if (this.transaction !== null) {\n        this.savingTransaction = true;\n        try {\n          await this.transaction.commit();\n        } finally {\n          this.endTransaction();\n        }\n      }\n    },\n    async cancelTransaction() {\n      if (this.transaction !== null) {\n        try {\n          await this.transaction.cancel();\n        } finally {\n          this.endTransaction();\n        }\n      }\n    },\n    entrySelection(entry) {\n      this.selectedEntryId = entry.id;\n    },\n    endTransaction() {\n      this.openmct.objects.endTransaction();\n      this.transaction = null;\n      this.savingTransaction = false;\n      this.activeTransaction = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookEmbed.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"notebookEmbed\"\n    class=\"c-snapshot c-ne__embed\"\n    :aria-label=\"`${embed.name} Notebook Embed`\"\n    @mouseover.ctrl=\"showToolTip\"\n    @mouseleave=\"hideToolTip\"\n  >\n    <div v-if=\"embed.snapshot\" class=\"c-ne__embed__snap-thumb\" @click=\"openSnapshot()\">\n      <img :src=\"thumbnailImage\" :alt=\"`${embed.name} thumbnail`\" />\n    </div>\n    <div class=\"c-ne__embed__info\">\n      <div class=\"c-ne__embed__name\">\n        <a class=\"c-ne__embed__link\" :class=\"embed.cssClass\" @click=\"navigateToItemInTime\">\n          {{ embed.name }}\n        </a>\n        <button\n          class=\"c-ne__embed__actions c-icon-button icon-3-dots\"\n          title=\"More actions\"\n          aria-label=\"More actions\"\n          @click.prevent.stop=\"showMenuItems($event)\"\n        ></button>\n      </div>\n      <div class=\"c-ne__embed__time\">\n        {{ createdOn }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Moment from 'moment';\nimport mount from 'utils/mount';\n\nimport { objectPathToUrl } from '@/tools/url';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport ImageExporter from '../../../exporters/ImageExporter.js';\nimport { updateNotebookImageDomainObject } from '../utils/notebook-image.js';\nimport PainterroInstance from '../utils/painterroInstance.js';\nimport RemoveDialog from '../utils/removeDialog.js';\nimport SnapshotTemplate from './snapshot-template.html';\n\nexport default {\n  mixins: [tooltipHelpers],\n  inject: ['openmct', 'snapshotContainer'],\n  props: {\n    embed: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    isLocked: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    isSnapshotContainer: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    removeActionString: {\n      type: String,\n      default() {\n        return 'Remove This Embed';\n      }\n    }\n  },\n  emits: ['update-embed', 'remove-embed'],\n  data() {\n    return {\n      menuActions: []\n    };\n  },\n  computed: {\n    createdOn() {\n      return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');\n    },\n    thumbnailImage() {\n      return this.embed.snapshot.thumbnailImage\n        ? this.embed.snapshot.thumbnailImage.src\n        : this.embed.snapshot.src;\n    }\n  },\n  watch: {\n    isLocked(value) {\n      if (value === true) {\n        const index = this.menuActions.findIndex((item) => item.id === 'removeEmbed');\n        this.menuActions.splice(index, 1);\n      }\n    }\n  },\n  async mounted() {\n    this.objectPath = [];\n    await this.setEmbedObjectPath();\n    this.addMenuActions();\n    this.imageExporter = new ImageExporter(this.openmct);\n  },\n  methods: {\n    showMenuItems(event) {\n      const x = event.x;\n      const y = event.y;\n\n      const menuOptions = {\n        menuClass: 'c-ne__embed__actions-menu',\n        placement: this.openmct.menus.menuPlacement.TOP_RIGHT\n      };\n\n      this.openmct.menus.showSuperMenu(x, y, this.menuActions, menuOptions);\n    },\n    addMenuActions() {\n      if (this.embed.snapshot) {\n        const viewSnapshot = {\n          id: 'viewSnapshot',\n          cssClass: 'icon-camera',\n          name: 'View Snapshot',\n          description: 'View the snapshot image taken in the form of a jpeg.',\n          onItemClicked: () => this.openSnapshot()\n        };\n\n        this.menuActions.splice(0, this.menuActions.length, viewSnapshot);\n      }\n\n      if (this.embed.domainObject) {\n        const navigateToItem = {\n          id: 'navigateToItem',\n          cssClass: this.embed.cssClass,\n          name: 'Navigate to Item',\n          description: 'Navigate to the item with the current time settings.',\n          onItemClicked: () => this.navigateToItem()\n        };\n\n        const navigateToItemInTime = {\n          id: 'navigateToItemInTime',\n          cssClass: this.embed.cssClass,\n          name: 'Navigate to Item in Time',\n          description: 'Navigate to the item in its time frame when captured.',\n          onItemClicked: () => this.navigateToItemInTime()\n        };\n\n        const quickView = {\n          id: 'quickView',\n          cssClass: 'icon-eye-open',\n          name: 'Quick View',\n          description: 'Full screen overlay view of the item.',\n          onItemClicked: () => this.previewEmbed()\n        };\n\n        this.menuActions.push(...[quickView, navigateToItem, navigateToItemInTime]);\n      }\n\n      if (!this.isLocked) {\n        const removeEmbed = {\n          id: 'removeEmbed',\n          cssClass: 'icon-trash',\n          name: this.removeActionString,\n          description: 'Permanently delete this embed from this Notebook entry.',\n          onItemClicked: this.getRemoveDialog.bind(this)\n        };\n\n        this.menuActions.push(removeEmbed);\n      }\n    },\n    async setEmbedObjectPath() {\n      if (!this.embed.domainObject) {\n        return;\n      }\n      this.objectPath = await this.openmct.objects.getOriginalPath(\n        this.embed.domainObject.identifier\n      );\n\n      if (\n        this.objectPath.length > 0 &&\n        this.objectPath[this.objectPath.length - 1].type === 'root'\n      ) {\n        this.objectPath.pop();\n      }\n    },\n    annotateSnapshot() {\n      const { vNode, destroy } = mount(\n        {\n          template: '<div id=\"snap-annotation\"></div>'\n        },\n        {\n          app: this.openmct.app\n        }\n      );\n\n      const painterroInstance = new PainterroInstance(vNode.el, this.openmct);\n      const annotateOverlay = this.openmct.overlays.overlay({\n        element: vNode.el,\n        size: 'large',\n        dismissible: false,\n        buttons: [\n          {\n            label: 'Cancel',\n            callback: () => {\n              painterroInstance.dismiss();\n              annotateOverlay.dismiss();\n            }\n          },\n          {\n            label: 'Save',\n            emphasis: true,\n            callback: () => {\n              painterroInstance.save((snapshotObject) => {\n                annotateOverlay.dismiss();\n                this.snapshotOverlay.dismiss();\n                this.updateSnapshot(snapshotObject);\n                this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);\n              });\n            }\n          }\n        ],\n        onDestroy: destroy\n      });\n\n      painterroInstance.initialize();\n\n      const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;\n      if (!fullSizeImageObjectIdentifier) {\n        // legacy image data stored in embed\n        painterroInstance.show(this.embed.snapshot.src);\n\n        return;\n      }\n\n      if (this.isSnapshotContainer) {\n        const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);\n        const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;\n        painterroInstance.show(fullSizeImageURL);\n\n        return;\n      }\n\n      this.openmct.objects.get(fullSizeImageObjectIdentifier).then((object) => {\n        painterroInstance.show(object.configuration.fullSizeImageURL);\n      });\n    },\n    navigateToItem() {\n      const url = objectPathToUrl(this.openmct, this.objectPath);\n      this.openmct.router.navigate(url);\n    },\n    navigateToItemInTime() {\n      if (!this.embed.historicLink) {\n        // no historic link available\n\n        return;\n      }\n      const hash = this.embed.historicLink;\n\n      const bounds = this.openmct.time.getBounds();\n      const isTimeBoundChanged =\n        this.embed.bounds.start !== bounds.start || this.embed.bounds.end !== bounds.end;\n      const isFixedTimespanMode = this.openmct.time.isFixed();\n\n      let message = '';\n      if (isTimeBoundChanged) {\n        this.openmct.time.bounds({\n          start: this.embed.bounds.start,\n          end: this.embed.bounds.end\n        });\n        message = 'Time bound values changed';\n      }\n\n      if (!isFixedTimespanMode) {\n        message = 'Time bound values changed to fixed timespan mode';\n      }\n\n      if (message.length) {\n        this.openmct.notifications.alert(message);\n      }\n\n      if (this.openmct.editor.isEditing()) {\n        this.previewEmbed();\n      } else {\n        const relativeHash = hash.slice(hash.indexOf('#'));\n        const url = new URL(\n          relativeHash,\n          `${location.protocol}//${location.host}${location.pathname}`\n        );\n        this.openmct.router.navigate(url.hash);\n      }\n    },\n    formatTime(unixTime, timeFormat) {\n      return Moment.utc(unixTime).format(timeFormat);\n    },\n    getRemoveDialog() {\n      const options = {\n        name: this.removeActionString,\n        callback: this.removeEmbed.bind(this)\n      };\n      const removeDialog = new RemoveDialog(this.openmct, options);\n      removeDialog.show();\n    },\n    openSnapshot() {\n      const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;\n      if (!fullSizeImageObjectIdentifier) {\n        // legacy image data stored in embed\n        this.openSnapshotOverlay(this.embed.snapshot.src);\n\n        return;\n      }\n\n      if (this.isSnapshotContainer) {\n        const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);\n        const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;\n        this.openSnapshotOverlay(fullSizeImageURL);\n\n        return;\n      }\n\n      this.openmct.objects.get(fullSizeImageObjectIdentifier).then((object) => {\n        this.openSnapshotOverlay(object.configuration.fullSizeImageURL);\n      });\n    },\n    openSnapshotOverlay(src) {\n      const self = this;\n\n      const { vNode, destroy } = mount(\n        {\n          data: () => {\n            return {\n              createdOn: this.createdOn,\n              name: this.embed.name,\n              cssClass: this.embed.cssClass,\n              src\n            };\n          },\n          methods: {\n            formatTime: self.formatTime,\n            annotateSnapshot: self.annotateSnapshot,\n            exportImage: self.exportImage\n          },\n          template: SnapshotTemplate\n        },\n        {\n          app: this.openmct.app\n        }\n      );\n\n      this.snapshot = vNode.componentInstance;\n      this.snapshotOverlay = this.openmct.overlays.overlay({\n        element: vNode.el,\n        onDestroy: destroy,\n        size: 'large',\n        autoHide: false,\n        dismissible: true,\n        buttons: [\n          {\n            label: 'Done',\n            emphasis: true,\n            callback: () => {\n              this.snapshotOverlay.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    exportImage(type) {\n      let element = this.snapshot.$refs['snapshot-image'];\n\n      if (type === 'png') {\n        this.imageExporter.exportPNG(element, this.embed.name);\n      } else {\n        this.imageExporter.exportJPG(element, this.embed.name);\n      }\n    },\n    previewEmbed() {\n      const previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n      this.openmct.objects\n        .get(this.embed.domainObject.identifier)\n        .then((domainObject) => previewAction.invoke([domainObject]));\n    },\n    removeEmbed(success) {\n      if (!success) {\n        return;\n      }\n\n      this.$emit('remove-embed', this.embed.id);\n    },\n    updateEmbed(embed) {\n      this.$emit('update-embed', embed);\n    },\n    updateSnapshot(snapshotObject) {\n      this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;\n\n      this.updateNotebookImageDomainObjectSnapshot(snapshotObject);\n      this.updateEmbed(this.embed);\n    },\n    updateNotebookImageDomainObjectSnapshot(snapshotObject) {\n      if (this.isSnapshotContainer) {\n        const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);\n\n        snapshot.embedObject.snapshot.thumbnailImage = snapshotObject.thumbnailImage;\n        snapshot.notebookImageDomainObject.configuration.fullSizeImageURL =\n          snapshotObject.fullSizeImage.src;\n\n        this.snapshotContainer.updateSnapshot(snapshot);\n      } else {\n        updateNotebookImageDomainObject(\n          this.openmct,\n          this.embed.snapshot.fullSizeImageObjectIdentifier,\n          snapshotObject.fullSizeImage\n        );\n      }\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(\n        await this.getObjectPath(this.embed.domainObject.identifier),\n        BELOW,\n        'notebookEmbed'\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookEntry.vue",
    "content": "<!-- eslint-disable vue/no-v-html -->\n<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    ref=\"entry\"\n    class=\"c-notebook__entry c-ne has-local-controls\"\n    aria-label=\"Notebook Entry\"\n    :class=\"{ locked: isLocked, 'is-selected': isSelectedEntry, 'is-editing': editMode }\"\n    @dragover=\"changeCursor\"\n    @drop.capture=\"cancelEditMode\"\n    @drop.prevent=\"dropOnEntry\"\n    @click=\"selectAndEmitEntry($event, entry)\"\n    @paste=\"handlePaste\"\n  >\n    <div class=\"c-ne__time-and-content\">\n      <div class=\"c-ne__time-and-creator-and-delete\">\n        <div class=\"c-ne__time-and-creator\">\n          <span class=\"c-ne__created-date\">{{ createdOnDate }}</span>\n          <span class=\"c-ne__created-time\">{{ createdOnTime }}</span>\n          <span v-if=\"entry.createdBy\" class=\"c-ne__creator\">\n            <span class=\"icon-person\"></span>\n            {{\n              entry.createdByRole ? `${entry.createdBy}: ${entry.createdByRole}` : entry.createdBy\n            }}\n          </span>\n        </div>\n        <span v-if=\"!readOnly && !isLocked\" class=\"c-ne__local-controls--hidden\">\n          <button\n            class=\"c-ne__remove c-icon-button c-icon-button--major icon-trash\"\n            title=\"Delete this entry\"\n            aria-label=\"Delete this entry\"\n            tabindex=\"-1\"\n            @click.stop.prevent=\"deleteEntry\"\n          ></button>\n        </span>\n      </div>\n      <div class=\"c-ne__content\">\n        <template v-if=\"readOnly && result\">\n          <div :id=\"entry.id\" class=\"c-ne__text highlight\" tabindex=\"0\">\n            <TextHighlight\n              :text=\"convertMarkDownToHtml(entry.text)\"\n              :highlight=\"highlightText\"\n              :highlight-class=\"'search-highlight'\"\n            />\n          </div>\n        </template>\n        <template v-else-if=\"!isLocked\">\n          <div\n            v-if=\"!editMode\"\n            v-bind.prop=\"formattedText\"\n            :id=\"entry.id\"\n            tabindex=\"-1\"\n            aria-label=\"Notebook Entry Display\"\n            class=\"c-ne__text\"\n            @mouseover=\"checkEditability($event)\"\n            @click=\"editingEntry($event)\"\n          ></div>\n          <textarea\n            v-else\n            :id=\"entry.id\"\n            ref=\"entryInput\"\n            v-model=\"entry.text\"\n            class=\"c-ne__input\"\n            aria-label=\"Notebook Entry Input\"\n            tabindex=\"-1\"\n            @mouseleave=\"canEdit = true\"\n            @blur=\"updateEntryValue($event)\"\n          ></textarea>\n          <div v-if=\"editMode\" class=\"c-ne__save-button\">\n            <button class=\"c-button c-button--major icon-check\"></button>\n          </div>\n        </template>\n\n        <template v-else>\n          <div\n            v-bind.prop=\"formattedText\"\n            :id=\"entry.id\"\n            class=\"c-ne__text\"\n            contenteditable=\"false\"\n            tabindex=\"0\"\n          ></div>\n        </template>\n\n        <div class=\"c-ne__tags c-tag-holder\">\n          <div\n            v-for=\"(tag, index) in entryTags\"\n            :key=\"index\"\n            class=\"c-tag\"\n            :style=\"{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }\"\n          >\n            {{ tag.label }}\n          </div>\n        </div>\n\n        <div :class=\"{ 'c-scrollcontainer': enableEmbedsWrapperScroll }\">\n          <div ref=\"embedsWrapper\" class=\"c-snapshots c-ne__embeds-wrapper\">\n            <NotebookEmbed\n              v-for=\"embed in entry.embeds\"\n              ref=\"embeds\"\n              :key=\"embed.id\"\n              :embed=\"embed\"\n              :is-locked=\"isLocked\"\n              @remove-embed=\"removeEmbed\"\n              @update-embed=\"updateEmbed\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n    <div v-if=\"readOnly\" class=\"c-ne__section-and-page\">\n      <a\n        class=\"c-click-link\"\n        :class=\"{ 'search-highlight': result.metadata.sectionHit }\"\n        @click=\"navigateToSection()\"\n      >\n        {{ result.section.name }}\n      </a>\n      <span class=\"icon-arrow-right\"></span>\n      <a\n        class=\"c-click-link\"\n        :class=\"{ 'search-highlight': result.metadata.pageHit }\"\n        @click=\"navigateToPage()\"\n      >\n        {{ result.page.name }}\n      </a>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { Marked } from 'marked';\nimport Moment from 'moment';\nimport sanitizeHtml from 'sanitize-html';\n\nimport TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';\nimport { createNewEmbed, createNewImageEmbed, selectEntry } from '../utils/notebook-entries.js';\nimport {\n  saveNotebookImageDomainObject,\n  updateNamespaceOfDomainObject\n} from '../utils/notebook-image.js';\nimport NotebookEmbed from './NotebookEmbed.vue';\n\nconst SANITIZATION_SCHEMA = {\n  allowedTags: [\n    'h1',\n    'h2',\n    'h3',\n    'h4',\n    'h5',\n    'h6',\n    'blockquote',\n    'p',\n    'a',\n    'ul',\n    'ol',\n    'li',\n    'b',\n    'i',\n    'strong',\n    'em',\n    's',\n    'strike',\n    'code',\n    'hr',\n    'br',\n    'div',\n    'table',\n    'thead',\n    'caption',\n    'tbody',\n    'tr',\n    'th',\n    'td',\n    'pre',\n    'del',\n    'ins',\n    'mark',\n    'abbr'\n  ],\n  allowedAttributes: {\n    a: ['href', 'target', 'class', 'title'],\n    code: ['class'],\n    abbr: ['title']\n  }\n};\n\nconst UNKNOWN_USER = 'Unknown';\n\nexport default {\n  components: {\n    NotebookEmbed,\n    TextHighlight\n  },\n  inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'],\n  props: {\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    notebookAnnotations: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    entry: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    result: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    selectedPage: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    selectedSection: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    readOnly: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    isLocked: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    selectedEntryId: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: [\n    'delete-entry',\n    'change-section-page',\n    'update-entry',\n    'editing-entry',\n    'entry-selection',\n    'update-annotations'\n  ],\n  data() {\n    return {\n      editMode: false,\n      canEdit: true,\n      enableEmbedsWrapperScroll: false,\n      urlWhitelist: []\n    };\n  },\n  computed: {\n    createdOnDate() {\n      return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');\n    },\n    createdOnTime() {\n      return this.formatTime(this.entry.createdOn, 'HH:mm:ss');\n    },\n    formattedText() {\n      const text = this.entry.text;\n\n      if (this.editMode) {\n        return { innerText: text };\n      }\n\n      const markDownHtml = this.convertMarkDownToHtml(text);\n\n      return { innerHTML: markDownHtml };\n    },\n    isSelectedEntry() {\n      return this.selectedEntryId === this.entry.id;\n    },\n    entryTags() {\n      const tagsFromAnnotations = this.openmct.annotation.getTagsFromAnnotations(\n        this.notebookAnnotations\n      );\n\n      return tagsFromAnnotations;\n    },\n    entryText() {\n      let text = this.entry.text;\n\n      if (!this.result.metadata.entryHit) {\n        text = `[ no result for '${this.result.metadata.originalSearchText}' in entry ]`;\n      }\n\n      return text;\n    },\n    highlightText() {\n      let text = '';\n\n      if (this.result.metadata.entryHit) {\n        text = this.result.metadata.originalSearchText;\n      }\n\n      return text;\n    }\n  },\n  watch: {\n    editMode() {\n      this.$nextTick(() => {\n        // waiting for textarea to be rendered\n        this.$refs.entryInput?.focus();\n        this.adjustTextareaHeight();\n      });\n    }\n  },\n  beforeMount() {\n    this.marked = new Marked();\n    this.marked.use({\n      breaks: true,\n      extensions: [\n        {\n          name: 'link',\n          renderer: (options) => {\n            return this.validateLink(options);\n          }\n        }\n      ]\n    });\n  },\n  mounted() {\n    this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);\n\n    if (this.$refs.embedsWrapper) {\n      this.embedsWrapperResizeObserver = new ResizeObserver(this.manageEmbedLayout);\n      this.embedsWrapperResizeObserver.observe(this.$refs.embedsWrapper);\n    }\n\n    this.manageEmbedLayout();\n    this.dropOnEntry = this.dropOnEntry.bind(this);\n    if (this.entryUrlWhitelist?.length > 0) {\n      this.urlWhitelist = this.entryUrlWhitelist;\n    }\n  },\n  beforeUnmount() {\n    if (this.embedsWrapperResizeObserver) {\n      this.embedsWrapperResizeObserver.disconnect();\n    }\n  },\n  methods: {\n    handlePaste(event) {\n      const clipboardItems = Array.from(\n        (event.clipboardData || event.originalEvent.clipboardData).items\n      );\n      const hasClipboardText = clipboardItems.some(\n        (clipboardItem) => clipboardItem.kind === 'string'\n      );\n      const clipboardImages = clipboardItems.filter(\n        (clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')\n      );\n      const hasClipboardImages = clipboardImages?.length > 0;\n\n      if (hasClipboardImages) {\n        if (hasClipboardText) {\n          console.warn('Image and text kinds found in paste. Only processing images.');\n        }\n\n        this.addImageFromPaste(clipboardImages, event);\n      } else if (hasClipboardText) {\n        this.addTextFromPaste(event);\n      }\n    },\n    async addNewEmbed(objectPath) {\n      const bounds = this.openmct.time.getBounds();\n      const snapshotMeta = {\n        bounds,\n        link: null,\n        objectPath,\n        openmct: this.openmct\n      };\n      const newEmbed = await createNewEmbed(snapshotMeta);\n      if (!this.entry.embeds) {\n        this.entry.embeds = [];\n      }\n      this.entry.embeds.push(newEmbed);\n\n      this.manageEmbedLayout();\n    },\n    addTextFromPaste(event) {\n      if (!this.editMode) {\n        event.preventDefault();\n      }\n    },\n    async addImageFromPaste(clipboardImages, event) {\n      event?.preventDefault();\n      let updated = false;\n\n      await Promise.all(\n        Array.from(clipboardImages).map(async (clipboardImage) => {\n          const imageFile = clipboardImage.getAsFile();\n          const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);\n\n          if (!this.entry.embeds) {\n            this.entry.embeds = [];\n          }\n\n          this.entry.embeds.push(imageEmbed);\n\n          updated = true;\n        })\n      );\n\n      if (updated) {\n        this.manageEmbedLayout();\n        this.timestampAndUpdate();\n      }\n    },\n    convertMarkDownToHtml(text = '') {\n      let markDownHtml = this.marked.parse(text);\n      markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);\n      return markDownHtml;\n    },\n    adjustTextareaHeight() {\n      if (this.$refs.entryInput) {\n        this.$refs.entryInput.style.height = 'auto';\n        this.$refs.entryInput.style.height = `${this.$refs?.entryInput.scrollHeight}px`;\n        this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      }\n    },\n    validateLink(options) {\n      const { href, text } = options;\n      try {\n        const domain = new URL(href).hostname;\n        const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {\n          return domain.endsWith(partialDomain);\n        });\n\n        if (!urlIsWhitelisted) {\n          return text;\n        }\n\n        return `<a class=\"c-hyperlink\" target=\"_blank\" href=\"${href}\">${text}</a>`;\n      } catch (error) {\n        // had error parsing this URL, just return the text\n        return text;\n      }\n    },\n    cancelEditMode(event) {\n      const isEditing = this.openmct.editor.isEditing();\n      if (isEditing) {\n        this.openmct.editor.cancel();\n      }\n    },\n    changeCursor(event) {\n      event.preventDefault();\n\n      if (!this.isLocked) {\n        event.dataTransfer.dropEffect = 'copy';\n      } else {\n        event.dataTransfer.dropEffect = 'none';\n        event.dataTransfer.effectAllowed = 'none';\n      }\n    },\n    checkEditability($event) {\n      if ($event.target.nodeName === 'A') {\n        this.canEdit = false;\n      }\n    },\n    deleteEntry() {\n      this.$emit('delete-entry', this.entry.id);\n    },\n    manageEmbedLayout() {\n      if (this.$refs.embeds) {\n        const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;\n        const embedsTotalWidth = this.$refs.embeds.reduce((total, embed) => {\n          return embed.$el.clientWidth + total;\n        }, 0);\n\n        this.enableEmbedsWrapperScroll = embedsTotalWidth > embedsWrapperLength;\n      }\n    },\n    async dropOnEntry(dropEvent) {\n      dropEvent.stopImmediatePropagation();\n      const dataTransferFiles = Array.from(dropEvent.dataTransfer.files);\n\n      const localImageDropped = dataTransferFiles.some((file) => file.type.includes('image'));\n      const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id');\n      const domainObjectData = dropEvent.dataTransfer.getData('openmct/domain-object-path');\n      const imageUrl = dropEvent.dataTransfer.getData('URL');\n      if (localImageDropped) {\n        // local image(s) dropped from disk (file)\n        await Promise.all(\n          dataTransferFiles.map(async (file) => {\n            if (file.type.includes('image')) {\n              const imageData = file;\n              const imageEmbed = await createNewImageEmbed(\n                imageData,\n                this.openmct,\n                imageData?.name\n              );\n              if (!this.entry.embeds) {\n                this.entry.embeds = [];\n              }\n              this.entry.embeds.push(imageEmbed);\n            }\n          })\n        );\n        this.manageEmbedLayout();\n      } else if (imageUrl) {\n        try {\n          // remote image dropped (URL)\n          const response = await fetch(imageUrl);\n          const imageData = await response.blob();\n          const imageEmbed = await createNewImageEmbed(imageData, this.openmct);\n          if (!this.entry.embeds) {\n            this.entry.embeds = [];\n          }\n          this.entry.embeds.push(imageEmbed);\n          this.manageEmbedLayout();\n        } catch (error) {\n          this.openmct.notifications.error(`Unable to add image: ${error.message} `);\n          console.error(`Problem embedding remote image`, error);\n        }\n      } else if (snapshotId.length) {\n        // snapshot object\n        const snapshot = this.snapshotContainer.getSnapshot(snapshotId);\n        if (!this.entry.embeds) {\n          this.entry.embeds = [];\n        }\n        this.entry.embeds.push(snapshot.embedObject);\n        this.snapshotContainer.removeSnapshot(snapshotId);\n\n        const namespace = this.domainObject.identifier.namespace;\n        const notebookImageDomainObject = updateNamespaceOfDomainObject(\n          snapshot.notebookImageDomainObject,\n          namespace\n        );\n        saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);\n      } else if (domainObjectData) {\n        // plain domain object\n        const objectPath = JSON.parse(domainObjectData);\n        await this.addNewEmbed(objectPath);\n      } else {\n        this.openmct.notifications.error(\n          `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.`\n        );\n        console.warn(\n          `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.`\n        );\n        return;\n      }\n\n      this.timestampAndUpdate();\n    },\n    findPositionInArray(array, id) {\n      let position = -1;\n      array.some((item, index) => {\n        const found = item.id === id;\n        if (found) {\n          position = index;\n        }\n\n        return found;\n      });\n\n      return position;\n    },\n    formatTime(unixTime, timeFormat) {\n      return Moment.utc(unixTime).format(timeFormat);\n    },\n    navigateToPage() {\n      this.$emit('change-section-page', {\n        sectionId: this.result.section.id,\n        pageId: this.result.page.id\n      });\n    },\n    navigateToSection() {\n      this.$emit('change-section-page', {\n        sectionId: this.result.section.id,\n        pageId: null\n      });\n    },\n    removeEmbed(id) {\n      const embedPosition = this.findPositionInArray(this.entry.embeds, id);\n      // TODO: remove notebook snapshot object using object remove API\n      this.entry.embeds.splice(embedPosition, 1);\n\n      this.timestampAndUpdate();\n\n      this.manageEmbedLayout();\n    },\n    updateEmbed(newEmbed) {\n      this.entry.embeds.some((e) => {\n        const found = e.id === newEmbed.id;\n        if (found) {\n          e = newEmbed;\n        }\n\n        return found;\n      });\n\n      this.timestampAndUpdate();\n    },\n    async timestampAndUpdate() {\n      const [user, activeRole] = await Promise.all([\n        this.openmct.user.getCurrentUser(),\n        this.openmct.user.getActiveRole?.()\n      ]);\n      if (user === undefined) {\n        this.entry.modifiedBy = UNKNOWN_USER;\n      } else {\n        this.entry.modifiedBy = user.getName();\n        if (activeRole) {\n          this.entry.modifiedByRole = activeRole;\n        }\n      }\n\n      this.entry.modified = this.openmct.time.now();\n\n      this.$emit('update-entry', this.entry);\n    },\n    editingEntry(event) {\n      this.selectAndEmitEntry(event, this.entry);\n      if (this.isSelectedEntry) {\n        // selected and click, so we're ready to edit\n        this.selectAndEmitEntry(event, this.entry);\n        this.editMode = true;\n        this.adjustTextareaHeight();\n        this.$emit('editing-entry');\n      }\n    },\n    updateEntryValue($event) {\n      this.editMode = false;\n      const rawEntryValue = $event.target.value;\n      const sanitizeInput = sanitizeHtml(rawEntryValue, { allowedAttributes: [], allowedTags: [] });\n      // change &gt back to > for markdown to do blockquotes\n      const restoredQuoteBrackets = sanitizeInput.replace(/&gt;/g, '>');\n      this.entry.text = restoredQuoteBrackets;\n      this.timestampAndUpdate();\n    },\n    updateAnnotations(newAnnotations) {\n      this.$emit('update-annotations', newAnnotations);\n    },\n    selectAndEmitEntry(event, entry) {\n      selectEntry({\n        element: this.$refs.entry,\n        entryId: entry.id,\n        domainObject: this.domainObject,\n        openmct: this.openmct,\n        onAnnotationChange: this.updateAnnotations,\n        notebookAnnotations: this.notebookAnnotations\n      });\n      event.stopPropagation();\n      this.$emit('entry-selection', this.entry);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookMenuSwitcher.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n    <button\n      class=\"c-icon-button c-button--menu icon-camera\"\n      :aria-label=\"snapshotMenuLabel\"\n      :title=\"snapshotMenuLabel\"\n      @click.stop.prevent=\"showMenu\"\n    >\n      <span class=\"c-icon-button__label\">Snapshot</span>\n    </button>\n  </div>\n</template>\n\n<script>\nimport { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants.js';\nimport Snapshot from '../snapshot.js';\nimport { getMenuItems } from '../utils/notebook-snapshot-menu.js';\nimport { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    currentView: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    isPreview: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    objectPath: {\n      type: Array,\n      default() {\n        return null;\n      }\n    }\n  },\n  data() {\n    return {\n      notebookSnapshot: undefined,\n      notebookTypes: []\n    };\n  },\n  computed: {\n    snapshotMenuLabel() {\n      return 'Open the Notebook Snapshot Menu';\n    }\n  },\n  mounted() {\n    validateNotebookStorageObject();\n\n    this.notebookSnapshot = new Snapshot(this.openmct);\n    this.setDefaultNotebookStatus();\n  },\n  methods: {\n    getPreviewObjectLink() {\n      const relativePath = this.openmct.objects.getRelativePath(this.objectPath);\n      const urlParams = this.openmct.router.getParams();\n      urlParams.view = this.currentView.key;\n\n      const urlParamsString = Object.entries(urlParams)\n        .map(([key, value]) => `${key}=${value}`)\n        .join('&');\n\n      return `#/browse/${relativePath}?${urlParamsString}`;\n    },\n    async showMenu(event) {\n      const menuItemOptions = {\n        default: {\n          cssClass: 'icon-notebook',\n          name: `Save to Notebook`,\n          onItemClicked: () => this.snapshot(NOTEBOOK_DEFAULT, event.target)\n        },\n        snapshot: {\n          cssClass: 'icon-camera',\n          name: 'Save to Notebook Snapshots',\n          onItemClicked: () => this.snapshot(NOTEBOOK_SNAPSHOT, event.target)\n        }\n      };\n\n      const notebookTypes = await getMenuItems(this.openmct, menuItemOptions);\n      const elementBoundingClientRect = this.$el.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y + elementBoundingClientRect.height;\n      this.openmct.menus.showMenu(x, y, notebookTypes);\n    },\n    snapshot(notebookType, target) {\n      this.$nextTick(() => {\n        const wrapper =\n          (target && target.closest('.js-notebook-snapshot-item-wrapper')) || document;\n        const element = wrapper.querySelector('.js-notebook-snapshot-item');\n        const objectPath = this.objectPath || this.openmct.router.path;\n        const link = this.isPreview ? this.getPreviewObjectLink() : window.location.hash;\n        const snapshotMeta = {\n          bounds: this.openmct.time.getBounds(),\n          link,\n          objectPath,\n          openmct: this.openmct\n        };\n\n        this.notebookSnapshot.capture(snapshotMeta, notebookType, element);\n      });\n    },\n    setDefaultNotebookStatus() {\n      let defaultNotebookObject = getDefaultNotebook();\n      if (defaultNotebookObject) {\n        let notebookIdentifier = defaultNotebookObject.identifier;\n\n        this.openmct.status.set(notebookIdentifier, 'notebook-default');\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookSnapshotContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-snapshots-h\">\n    <div class=\"l-browse-bar\">\n      <div class=\"l-browse-bar__start\">\n        <div class=\"l-browse-bar__object-name--w c-snapshots-h__title\">\n          <div class=\"l-browse-bar__object-name c-object-label\">\n            <div class=\"c-object-label__type-icon icon-camera\"></div>\n            <div class=\"c-object-label__name\">Notebook Snapshots</div>\n            <div v-if=\"snapshots.length\" class=\"l-browse-bar__object-details\">\n              {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}\n            </div>\n          </div>\n          <PopupMenu v-if=\"snapshots.length > 0\" :popup-menu-items=\"popupMenuItems\" />\n        </div>\n      </div>\n      <div class=\"l-browse-bar__end\">\n        <button class=\"c-click-icon c-click-icon--major icon-x\" @click=\"close\"></button>\n      </div>\n    </div>\n    <!-- closes l-browse-bar -->\n    <div class=\"c-snapshots\">\n      <span\n        v-for=\"snapshot in snapshots\"\n        :key=\"snapshot.embedObject.id\"\n        draggable=\"true\"\n        @dragstart=\"startEmbedDrag(snapshot, $event)\"\n      >\n        <NotebookEmbed\n          ref=\"notebookEmbed\"\n          :key=\"snapshot.embedObject.id\"\n          :embed=\"snapshot.embedObject\"\n          :is-snapshot-container=\"true\"\n          :remove-action-string=\"'Delete Snapshot'\"\n          @remove-embed=\"removeSnapshot\"\n        />\n      </span>\n      <div v-if=\"!snapshots.length > 0\" class=\"hint\">\n        There are no Notebook Snapshots currently.\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants.js';\nimport { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container.js';\nimport RemoveDialog from '../utils/removeDialog.js';\nimport NotebookEmbed from './NotebookEmbed.vue';\nimport PopupMenu from './PopupMenu.vue';\n\nexport default {\n  components: {\n    NotebookEmbed,\n    PopupMenu\n  },\n  inject: ['openmct', 'snapshotContainer'],\n  props: {\n    toggleSnapshot: {\n      type: Function,\n      default() {\n        return () => {};\n      }\n    }\n  },\n  data() {\n    return {\n      popupMenuItems: [],\n      removeActionString: 'Delete all snapshots',\n      snapshots: []\n    };\n  },\n  mounted() {\n    this.addPopupMenuItems();\n    this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);\n    this.snapshots = this.snapshotContainer.getSnapshots();\n  },\n  methods: {\n    addPopupMenuItems() {\n      const removeSnapshot = {\n        cssClass: 'icon-trash',\n        name: this.removeActionString,\n        onItemClicked: this.getRemoveDialog.bind(this)\n      };\n\n      this.popupMenuItems = [removeSnapshot];\n    },\n    close() {\n      this.toggleSnapshot();\n    },\n    getNotebookSnapshotMaxCount() {\n      return NOTEBOOK_SNAPSHOT_MAX_COUNT;\n    },\n    getRemoveDialog() {\n      const options = {\n        name: this.removeActionString,\n        callback: this.removeAllSnapshots.bind(this)\n      };\n      const removeDialog = new RemoveDialog(this.openmct, options);\n      removeDialog.show();\n    },\n    removeAllSnapshots(success) {\n      if (!success) {\n        return;\n      }\n\n      this.snapshotContainer.removeAllSnapshots();\n    },\n    removeSnapshot(id) {\n      this.snapshotContainer.removeSnapshot(id);\n    },\n    snapshotsUpdated() {\n      this.snapshots = this.snapshotContainer.getSnapshots();\n    },\n    startEmbedDrag(snapshot, event) {\n      event.dataTransfer.setData('text/plain', snapshot.embedObject.id);\n      event.dataTransfer.setData('openmct/snapshot/id', snapshot.embedObject.id);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/NotebookSnapshotIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <aside aria-label=\"Snapshot Indicator\">\n    <div\n      class=\"c-indicator c-indicator--clickable icon-camera\"\n      :class=\"[\n        { 's-status-off': snapshotCount === 0 },\n        { 's-status-on': snapshotCount > 0 },\n        { 's-status-caution': snapshotCount === snapshotMaxCount },\n        { 'has-new-snapshot': flashIndicator }\n      ]\"\n    >\n      <span class=\"label c-indicator__label\">\n        {{ indicatorTitle }}\n        <button\n          :aria-label=\"expanded ? 'Hide Snapshots' : 'Show Snapshots'\"\n          @click=\"toggleSnapshot\"\n        >\n          {{ expanded ? 'Hide' : 'Show' }}\n        </button>\n      </span>\n      <span class=\"c-indicator__count\">{{ snapshotCount }}</span>\n    </div>\n  </aside>\n</template>\n\n<script>\nimport mount from 'utils/mount';\n\nimport { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants.js';\nimport { getSnapshotContainer } from '../plugin.js';\nimport { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container.js';\nimport SnapshotContainerComponent from './NotebookSnapshotContainer.vue';\n\nexport default {\n  inject: ['openmct'],\n  data() {\n    return {\n      expanded: false,\n      indicatorTitle: '',\n      snapshotCount: 0,\n      snapshotMaxCount: NOTEBOOK_SNAPSHOT_MAX_COUNT,\n      flashIndicator: false\n    };\n  },\n  created() {\n    this.snapshotContainer = getSnapshotContainer(this.openmct);\n  },\n  mounted() {\n    this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);\n    this.updateSnapshotIndicatorTitle();\n  },\n  methods: {\n    notifyNewSnapshot() {\n      this.flashIndicator = true;\n      setTimeout(this.removeNotify, 15000);\n    },\n    removeNotify() {\n      this.flashIndicator = false;\n    },\n    snapshotsUpdated() {\n      if (this.snapshotContainer.getSnapshots().length > this.snapshotCount) {\n        this.notifyNewSnapshot();\n      }\n\n      this.updateSnapshotIndicatorTitle();\n    },\n    toggleSnapshot() {\n      this.expanded = !this.expanded;\n\n      const drawerElement = document.querySelector('.l-shell__drawer');\n      drawerElement.classList.toggle('is-expanded');\n\n      this.updateSnapshotContainer();\n    },\n    updateSnapshotContainer() {\n      const { openmct, snapshotContainer } = this;\n      const toggleSnapshot = this.toggleSnapshot.bind(this);\n      const drawerElement = document.querySelector('.l-shell__drawer');\n      drawerElement.innerHTML = '<div></div>';\n      const divElement = document.querySelector('.l-shell__drawer div');\n\n      if (this.destroySnapshotContainer) {\n        this.destroySnapshotContainer();\n      }\n      const { destroy } = mount(\n        {\n          el: divElement,\n          components: {\n            SnapshotContainerComponent\n          },\n          provide: {\n            openmct,\n            snapshotContainer\n          },\n          data() {\n            return {\n              toggleSnapshot\n            };\n          },\n          template:\n            '<SnapshotContainerComponent :toggleSnapshot=\"toggleSnapshot\"></SnapshotContainerComponent>'\n        },\n        {\n          app: openmct.app,\n          element: divElement\n        }\n      );\n      this.destroySnapshotContainer = destroy;\n    },\n    updateSnapshotIndicatorTitle() {\n      const snapshotCount = this.snapshotContainer.getSnapshots().length;\n      this.snapshotCount = snapshotCount;\n      const snapshotTitleSuffix = snapshotCount === 1 ? 'Snapshot' : 'Snapshots';\n      this.indicatorTitle = `${snapshotCount} ${snapshotTitleSuffix}`;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/PageCollection.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul class=\"c-list c-notebook__pages\">\n    <li v-for=\"page in pages\" :key=\"page.id\" class=\"c-list__item-h\">\n      <Page\n        ref=\"pageComponent\"\n        :default-page-id=\"defaultPageId\"\n        :selected-page-id=\"selectedPageId\"\n        :page=\"page\"\n        :page-title=\"pageTitle\"\n        @delete-page=\"deletePage\"\n        @rename-page=\"updatePage\"\n        @select-page=\"selectPage\"\n      />\n    </li>\n  </ul>\n</template>\n\n<script>\nimport { deleteNotebookEntries } from '../utils/notebook-entries.js';\nimport { getDefaultNotebook } from '../utils/notebook-storage.js';\nimport Page from './PageComponent.vue';\n\nexport default {\n  components: {\n    Page\n  },\n  inject: ['openmct'],\n  props: {\n    defaultPageId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedPageId: {\n      type: String,\n      required: true\n    },\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    pages: {\n      type: Array,\n      required: true,\n      default() {\n        return [];\n      }\n    },\n    sections: {\n      type: Array,\n      required: true,\n      default() {\n        return [];\n      }\n    },\n    pageTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    sidebarCoversEntries: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['default-page-deleted', 'update-page', 'select-page', 'toggle-nav'],\n  watch: {\n    pages: {\n      handler(val, oldVal) {\n        if (!this.containsPage(this.selectedPageId)) {\n          this.selectPage(this.pages[0].id);\n        }\n      },\n      deep: true\n    }\n  },\n  methods: {\n    containsPage(pageId) {\n      return this.pages.some((page) => page.id === pageId);\n    },\n    deletePage(id) {\n      const selectedSection = this.sections.find((s) => s.isSelected);\n      const page = this.pages.find((p) => p.id === id);\n      deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);\n\n      const selectedPage = this.pages.find((p) => p.isSelected);\n      const defaultNotebook = getDefaultNotebook();\n      const defaultPageId = defaultNotebook && defaultNotebook.defaultPageId;\n      const isPageSelected = selectedPage && selectedPage.id === id;\n      const isPageDefault = defaultPageId === id;\n      const pages = this.pages.filter((s) => s.id !== id);\n      let selectedPageId;\n\n      if (isPageSelected && defaultPageId) {\n        pages.forEach((s) => {\n          s.isSelected = false;\n          if (defaultPageId === s.id) {\n            selectedPageId = s.id;\n          }\n        });\n      }\n\n      if (isPageDefault) {\n        this.$emit('default-page-deleted');\n      }\n\n      if (pages.length && isPageSelected && (!defaultPageId || isPageDefault)) {\n        selectedPageId = pages[0].id;\n      }\n\n      this.$emit('update-page', {\n        pages,\n        id\n      });\n      this.$emit('select-page', selectedPageId);\n    },\n    selectPage(id) {\n      this.$emit('select-page', id);\n\n      // Add test here for whether or not to toggle the nav\n      if (this.sidebarCoversEntries) {\n        this.$emit('toggle-nav');\n      }\n    },\n    updatePage(newPage) {\n      const id = newPage.id;\n      const pages = this.pages.map((page) => (page.id === id ? newPage : page));\n      this.$emit('update-page', {\n        pages,\n        id\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/PageComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-list__item js-list__item\"\n    :class=\"[\n      {\n        'is-selected': isSelected,\n        'is-notebook-default': defaultPageId === page.id,\n        'icon-lock': page.isLocked\n      }\n    ]\"\n    :data-id=\"page.id\"\n    @click=\"selectPage\"\n  >\n    <template v-if=\"!page.isLocked\">\n      <div\n        class=\"c-list__item__name js-list__item__name\"\n        :class=\"[{ 'c-input-inline': isSelected }]\"\n        :data-id=\"page.id\"\n        :contenteditable=\"isSelected\"\n        @keydown.escape=\"updateName\"\n        @keydown.enter=\"updateName\"\n        @blur=\"updateName\"\n      >\n        {{ pageName }}\n      </div>\n      <PopupMenu :popup-menu-items=\"popupMenuItems\" />\n    </template>\n    <template v-else>\n      <div\n        class=\"c-list__item__name js-list__item__name\"\n        :data-id=\"page.id\"\n        :contenteditable=\"false\"\n      >\n        {{ pageName }}\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code.js';\nimport RemoveDialog from '../utils/removeDialog.js';\nimport PopupMenu from './PopupMenu.vue';\n\nexport default {\n  components: {\n    PopupMenu\n  },\n  inject: ['openmct'],\n  props: {\n    defaultPageId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedPageId: {\n      type: String,\n      required: true\n    },\n    page: {\n      type: Object,\n      required: true\n    },\n    pageTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['delete-page', 'select-page', 'rename-page'],\n  data() {\n    return {\n      popupMenuItems: [],\n      removeActionString: `Delete ${this.pageTitle}`\n    };\n  },\n  computed: {\n    isSelected() {\n      return this.selectedPageId === this.page.id;\n    },\n    pageName() {\n      return this.page.name.length ? this.page.name : `Unnamed ${this.pageTitle}`;\n    }\n  },\n  mounted() {\n    this.addPopupMenuItems();\n  },\n  methods: {\n    addPopupMenuItems() {\n      const removePage = {\n        cssClass: 'icon-trash',\n        name: this.removeActionString,\n        onItemClicked: this.getRemoveDialog.bind(this)\n      };\n\n      this.popupMenuItems = [removePage];\n    },\n    deletePage(success) {\n      if (!success) {\n        return;\n      }\n\n      this.$emit('delete-page', this.page.id);\n    },\n    getRemoveDialog() {\n      const message =\n        'Other users may be editing entries in this page, and deleting it is permanent. Do you want to continue?';\n      const options = {\n        name: this.removeActionString,\n        callback: this.deletePage.bind(this),\n        message\n      };\n      const removeDialog = new RemoveDialog(this.openmct, options);\n      removeDialog.show();\n    },\n    selectPage(event) {\n      const {\n        target: {\n          dataset: { id }\n        }\n      } = event;\n\n      if (this.isSelected || !id) {\n        return;\n      }\n\n      this.$emit('select-page', id);\n    },\n    renamePage(target) {\n      if (!target) {\n        return;\n      }\n\n      target.textContent = target.textContent\n        ? target.textContent.trim()\n        : `Unnamed ${this.pageTitle}`;\n\n      if (this.page.name === target.textContent) {\n        return;\n      }\n\n      this.$emit('rename-page', Object.assign(this.page, { name: target.textContent }));\n    },\n    updateName(event) {\n      const { target, keyCode, type } = event;\n\n      if (keyCode === KEY_ESCAPE) {\n        target.textContent = this.page.name;\n      } else if (keyCode === KEY_ENTER || type === 'blur') {\n        this.renamePage(target);\n      }\n\n      target.scrollLeft = '0';\n\n      target.blur();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/PopupMenu.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <button\n    class=\"c-popup-menu-button c-disclosure-button\"\n    title=\"Open context menu\"\n    @click.stop=\"showMenuItems($event)\"\n  ></button>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    popupMenuItems: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  methods: {\n    showMenuItems($event) {\n      this.openmct.menus.showMenu($event.x, $event.y, this.popupMenuItems);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/SearchResults.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-notebook__search-results\">\n    <div class=\"c-notebook__search-results__header\">Search Results ({{ results.length }})</div>\n    <div class=\"c-notebook__entries\">\n      <NotebookEntry\n        v-for=\"(result, index) in results\"\n        :key=\"index\"\n        :domain-object=\"domainObject\"\n        :result=\"result\"\n        :entry=\"result.entry\"\n        :read-only=\"true\"\n        :selected-page=\"result.page\"\n        :selected-section=\"result.section\"\n        :is-locked=\"result.page.isLocked\"\n        @editing-entry=\"editingEntry\"\n        @cancel-edit=\"cancelEdit\"\n        @change-section-page=\"changeSectionPage\"\n        @update-entries=\"updateEntries\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport NotebookEntry from './NotebookEntry.vue';\n\nexport default {\n  components: {\n    NotebookEntry\n  },\n  inject: ['openmct', 'snapshotContainer'],\n  props: {\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    results: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['editing-entry', 'cancel-edit', 'change-section-page', 'update-entries'],\n  methods: {\n    editingEntry() {\n      this.$emit('editing-entry');\n    },\n    cancelEdit() {\n      this.$emit('cancel-edit');\n    },\n    changeSectionPage(data) {\n      this.$emit('change-section-page', data);\n    },\n    updateEntries(entries) {\n      this.$emit('update-entries', entries);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/SectionCollection.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul class=\"c-list c-notebook__sections\">\n    <li v-for=\"section in sections\" :key=\"section.id\" class=\"c-list__item-h\">\n      <NotebookSection\n        ref=\"sectionComponent\"\n        :default-section-id=\"defaultSectionId\"\n        :selected-section-id=\"selectedSectionId\"\n        :section=\"section\"\n        :section-title=\"sectionTitle\"\n        @delete-section=\"deleteSection\"\n        @rename-section=\"updateSection\"\n        @select-section=\"selectSection\"\n      />\n    </li>\n  </ul>\n</template>\n\n<script>\nimport { deleteNotebookEntries } from '../utils/notebook-entries.js';\nimport { getDefaultNotebook } from '../utils/notebook-storage.js';\nimport SectionComponent from './SectionComponent.vue';\n\nexport default {\n  components: {\n    NotebookSection: SectionComponent\n  },\n  inject: ['openmct'],\n  props: {\n    defaultSectionId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedSectionId: {\n      type: String,\n      required: true\n    },\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    sections: {\n      type: Array,\n      required: true,\n      default() {\n        return [];\n      }\n    },\n    sectionTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['select-section', 'default-section-deleted', 'update-section'],\n  watch: {\n    sections: {\n      handler(val, oldVal) {\n        if (!this.containsSection(this.selectedSectionId)) {\n          this.selectSection(this.sections[0].id);\n        }\n      },\n      deep: true\n    }\n  },\n  methods: {\n    containsSection(sectionId) {\n      return this.sections.some((section) => section.id === sectionId);\n    },\n    deleteSection(id) {\n      const section = this.sections.find((s) => s.id === id);\n      deleteNotebookEntries(this.openmct, this.domainObject, section);\n\n      const selectedSection = this.sections.find((s) => s.id === this.selectedSectionId);\n      const defaultNotebook = getDefaultNotebook();\n      const defaultSectionId = defaultNotebook && defaultNotebook.defaultSectionId;\n      const isSectionSelected = selectedSection && selectedSection.id === id;\n      const isSectionDefault = defaultSectionId === id;\n      const sections = this.sections.filter((s) => s.id !== id);\n\n      if (isSectionSelected && defaultSectionId) {\n        sections.forEach((s) => {\n          s.isSelected = false;\n          if (defaultSectionId === s.id) {\n            s.isSelected = true;\n          }\n        });\n      }\n\n      if (isSectionDefault) {\n        this.$emit('default-section-deleted');\n      }\n\n      if (sections.length && isSectionSelected && (!defaultSectionId || isSectionDefault)) {\n        sections[0].isSelected = true;\n      }\n\n      this.$emit('update-section', {\n        sections,\n        id\n      });\n    },\n    selectSection(id) {\n      this.$emit('select-section', id);\n    },\n    updateSection(newSection) {\n      const id = newSection.id;\n      const sections = this.sections.map((section) => (section.id === id ? newSection : section));\n      this.$emit('update-section', {\n        sections,\n        id\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/SectionComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-list__item js-list__item\"\n    :class=\"[{ 'is-selected': isSelected, 'is-notebook-default': defaultSectionId === section.id }]\"\n    :data-id=\"section.id\"\n    @click=\"selectSection\"\n  >\n    <span\n      class=\"c-list__item__name js-list__item__name\"\n      :class=\"[{ 'c-input-inline': isSelected && !section.isLocked }]\"\n      :data-id=\"section.id\"\n      :contenteditable=\"isSelected && !section.isLocked\"\n      @keydown.escape=\"updateName\"\n      @keydown.enter=\"updateName\"\n      @blur=\"updateName\"\n      >{{ sectionName }}</span\n    >\n    <PopupMenu v-if=\"!section.isLocked\" :popup-menu-items=\"popupMenuItems\" />\n  </div>\n</template>\n\n<script>\nimport { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code.js';\nimport RemoveDialog from '../utils/removeDialog.js';\nimport PopupMenu from './PopupMenu.vue';\n\nexport default {\n  components: {\n    PopupMenu\n  },\n  inject: ['openmct'],\n  props: {\n    defaultSectionId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedSectionId: {\n      type: String,\n      required: true\n    },\n    section: {\n      type: Object,\n      required: true\n    },\n    sectionTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['delete-section', 'rename-section', 'select-section'],\n  data() {\n    return {\n      popupMenuItems: [],\n      removeActionString: `Delete ${this.sectionTitle}`\n    };\n  },\n  computed: {\n    isSelected() {\n      return this.selectedSectionId === this.section.id;\n    },\n    sectionName() {\n      return this.section.name.length ? this.section.name : `Unnamed ${this.sectionTitle}`;\n    }\n  },\n  mounted() {\n    this.addPopupMenuItems();\n  },\n  methods: {\n    addPopupMenuItems() {\n      const removeSection = {\n        cssClass: 'icon-trash',\n        name: this.removeActionString,\n        onItemClicked: this.getRemoveDialog\n      };\n\n      this.popupMenuItems = [removeSection];\n    },\n    deleteSection(success) {\n      if (!success) {\n        return;\n      }\n\n      this.$emit('delete-section', this.section.id);\n    },\n    getRemoveDialog() {\n      const message =\n        'Other users may be editing entries in this section, and deleting it is permanent. Do you want to continue?';\n      const options = {\n        name: this.removeActionString,\n        callback: this.deleteSection.bind(this),\n        message\n      };\n\n      const removeDialog = new RemoveDialog(this.openmct, options);\n      removeDialog.show();\n    },\n    selectSection(event) {\n      const {\n        target: {\n          dataset: { id }\n        }\n      } = event;\n\n      if (this.isSelected || !id) {\n        return;\n      }\n\n      this.$emit('select-section', id);\n    },\n    renameSection(target) {\n      if (!target) {\n        return;\n      }\n\n      target.textContent = target.textContent\n        ? target.textContent.trim()\n        : `Unnamed ${this.sectionTitle}`;\n\n      if (this.section.name === target.textContent) {\n        return;\n      }\n\n      this.$emit('rename-section', Object.assign(this.section, { name: target.textContent }));\n    },\n    updateName(event) {\n      const { target, keyCode, type } = event;\n\n      if (keyCode === KEY_ESCAPE) {\n        target.textContent = this.section.name;\n      } else if (keyCode === KEY_ENTER || type === 'blur') {\n        this.renameSection(target);\n      }\n\n      target.scrollLeft = '0';\n\n      target.blur();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/SidebarComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-sidebar c-drawer c-drawer--align-left\">\n    <div class=\"c-sidebar__pane js-sidebar-sections\">\n      <div class=\"c-sidebar__header-w\">\n        <div class=\"c-sidebar__header\">\n          <span class=\"c-sidebar__header-label\">{{ sectionTitle }}</span>\n          <button\n            class=\"c-icon-button c-icon-button--major icon-plus\"\n            aria-label=\"Add Section\"\n            @click=\"addSection\"\n          >\n            <span class=\"c-list-button__label\">Add</span>\n          </button>\n        </div>\n      </div>\n      <div class=\"c-sidebar__contents-and-controls\">\n        <SectionCollection\n          class=\"c-sidebar__contents\"\n          :default-section-id=\"defaultSectionId\"\n          :selected-section-id=\"selectedSectionId\"\n          :domain-object=\"domainObject\"\n          :sections=\"sections\"\n          :section-title=\"sectionTitle\"\n          @default-section-deleted=\"defaultSectionDeleted\"\n          @update-section=\"sectionsChanged\"\n          @select-section=\"selectSection\"\n        />\n      </div>\n    </div>\n    <div class=\"c-sidebar__pane js-sidebar-pages\">\n      <div class=\"c-sidebar__header-w\">\n        <div class=\"c-sidebar__header\">\n          <span class=\"c-sidebar__header-label\">{{ pageTitle }}</span>\n\n          <button\n            class=\"c-icon-button c-icon-button--major icon-plus\"\n            aria-label=\"Add Page\"\n            @click=\"addPage\"\n          >\n            <span class=\"c-icon-button__label\">Add</span>\n          </button>\n        </div>\n      </div>\n\n      <div class=\"c-sidebar__contents-and-controls\">\n        <PageCollection\n          ref=\"pageCollection\"\n          class=\"c-sidebar__contents\"\n          :default-page-id=\"defaultPageId\"\n          :selected-page-id=\"selectedPageId\"\n          :domain-object=\"domainObject\"\n          :pages=\"pages\"\n          :sections=\"sections\"\n          :sidebar-covers-entries=\"sidebarCoversEntries\"\n          :page-title=\"pageTitle\"\n          @default-page-deleted=\"defaultPageDeleted\"\n          @toggle-nav=\"toggleNav\"\n          @update-page=\"pagesChanged\"\n          @select-page=\"selectPage\"\n        />\n      </div>\n    </div>\n    <div class=\"c-sidebar__right-edge\">\n      <button class=\"c-icon-button c-icon-button--major icon-line-horz\" @click=\"toggleNav\"></button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { v4 as uuid } from 'uuid';\n\nimport PageCollection from './PageCollection.vue';\nimport SectionCollection from './SectionCollection.vue';\n\nexport default {\n  components: {\n    SectionCollection,\n    PageCollection\n  },\n  inject: ['openmct'],\n  props: {\n    defaultPageId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedPageId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    defaultSectionId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    selectedSectionId: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    domainObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    pageTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    sections: {\n      type: Array,\n      required: true,\n      default() {\n        return [];\n      }\n    },\n    sectionTitle: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    sidebarCoversEntries: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: [\n    'select-page',\n    'select-section',\n    'toggle-nav',\n    'default-section-deleted',\n    'default-page-deleted',\n    'pages-changed',\n    'sections-changed'\n  ],\n  computed: {\n    pages() {\n      const selectedSection = this.sections.find(\n        (section) => section.id === this.selectedSectionId\n      );\n\n      return (selectedSection && selectedSection.pages) || [];\n    }\n  },\n  watch: {\n    pages: {\n      handler(newPages, oldPages) {\n        if (!newPages.length) {\n          this.addPage();\n        }\n      },\n      deep: true\n    },\n    sections: {\n      handler(newSections, oldSections) {\n        if (!newSections.length) {\n          this.addSection();\n        }\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    if (!this.sections.length) {\n      this.addSection();\n    }\n  },\n  methods: {\n    addPage() {\n      const newPage = this.createNewPage();\n      const pages = this.addNewPage(newPage);\n\n      this.pagesChanged({\n        pages,\n        id: newPage.id\n      });\n      this.$emit('select-page', newPage.id);\n    },\n    addSection() {\n      const newSection = this.createNewSection();\n      const sections = this.addNewSection(newSection);\n\n      this.sectionsChanged({\n        sections,\n        id: newSection.id\n      });\n\n      this.$emit('select-section', newSection.id);\n    },\n    addNewPage(newPage) {\n      this.pages.forEach((page) => {\n        page.isSelected = false;\n      });\n\n      this.pages.push(newPage);\n      return this.pages;\n    },\n    addNewSection(newSection) {\n      this.sections.forEach((section) => {\n        section.isSelected = false;\n      });\n\n      this.sections.push(newSection);\n      return this.sections;\n    },\n    createNewPage() {\n      const pageTitle = this.pageTitle;\n      const id = uuid();\n\n      return {\n        id,\n        isDefault: false,\n        isSelected: true,\n        name: `Unnamed ${pageTitle}`,\n        pageTitle\n      };\n    },\n    createNewSection() {\n      const sectionTitle = this.sectionTitle;\n      const id = uuid();\n      const page = this.createNewPage();\n      const pages = [page];\n\n      return {\n        id,\n        isDefault: false,\n        isSelected: true,\n        name: `Unnamed ${sectionTitle}`,\n        pages,\n        sectionTitle\n      };\n    },\n    defaultPageDeleted() {\n      this.$emit('default-page-deleted');\n    },\n    defaultSectionDeleted() {\n      this.$emit('default-section-deleted');\n    },\n    toggleNav() {\n      this.$emit('toggle-nav');\n    },\n    pagesChanged({ pages, id }) {\n      this.$emit('pages-changed', {\n        pages,\n        id\n      });\n    },\n    selectPage(pageId) {\n      this.$emit('select-page', pageId);\n    },\n    selectSection(sectionId) {\n      this.$emit('select-section', sectionId);\n    },\n    sectionsChanged({ sections, id }) {\n      this.$emit('sections-changed', {\n        sections,\n        id\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notebook/components/sidebar.scss",
    "content": ".c-sidebar {\n  @include userSelectNone();\n  background: $sideBarBg;\n  display: flex;\n  justify-content: stretch;\n  max-width: 600px;\n\n  &.c-drawer--push.is-expanded {\n    margin-right: $interiorMargin;\n    width: 30%;\n  }\n\n  &.c-drawer--overlays.is-expanded {\n    width: 95%;\n  }\n\n  &__pane {\n    background: $sideBarBg;\n    display: flex;\n    flex: 1 1 50%;\n    flex-direction: column;\n\n    + * {\n      margin-left: $interiorMarginSm;\n    }\n\n    > * + * {\n      // Add margin-top to first and second level children\n      margin-top: $interiorMargin;\n    }\n  }\n\n  &__right-edge {\n    flex: 0 0 auto;\n    padding: $interiorMarginSm;\n  }\n\n  &__header-w {\n    // Wraps header, used for page pane with collapse buttons\n    display: flex;\n    flex: 0 0 auto;\n    background: $sideBarHeaderBg;\n    align-items: center;\n  }\n\n  &__header {\n    color: $sideBarHeaderFg;\n    display: flex;\n    align-items: center;\n    flex: 1 1 auto;\n    padding: $interiorMarginSm $interiorMargin;\n    text-transform: uppercase;\n\n    &-label {\n      @include ellipsize();\n      flex: 1 1 auto;\n    }\n  }\n\n  &__contents-and-controls {\n    // Encloses pane buttons and contents elements\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n  }\n\n  &__contents {\n    flex: 1 1 auto;\n    overflow-x: hidden;\n    overflow-y: auto;\n    padding: auto $interiorMargin;\n  }\n\n  .c-list__item {\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n\n    &__name {\n      flex: 1 1 auto;\n    }\n\n    &__menu-indicator {\n      // Not sure this is being used\n      flex: 0 0 auto;\n      font-size: 0.8em;\n      opacity: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/components/snapshot-template.html",
    "content": "<div class=\"c-notebook-snapshot\">\n  <div class=\"c-notebook-snapshot__header l-browse-bar\">\n    <div class=\"l-browse-bar__start\">\n      <div class=\"l-browse-bar__object-name--w\">\n        <span class=\"c-object-label l-browse-bar__object-name\">\n          <span class=\"c-object-label__type-icon\" :class=\"cssClass\"></span>\n          <span class=\"c-object-label__name\">{{ name }}</span>\n        </span>\n      </div>\n    </div>\n    <div id=\"snapshotDescriptor\" class=\"l-browse-bar__snapshot-datetime\">\n      SNAPSHOT {{ createdOn }}\n    </div>\n    <div class=\"c-button-set c-button-set--strip-h\" role=\"toolbar\">\n      <button class=\"c-button icon-download\" aria-label=\"Export as PNG\" @click=\"exportImage('png')\">\n        <span class=\"c-button__label\">PNG</span>\n      </button>\n      <button class=\"c-button icon-download\" aria-label=\"Export as JPG\" @click=\"exportImage('jpg')\">\n        <span class=\"c-button__label\">JPG</span>\n      </button>\n    </div>\n    <div class=\"l-browse-bar__end\">\n      <button\n        class=\"l-browse-bar__annotate-button c-button icon-pencil\"\n        aria-label=\"Annotate this snapshot\"\n        @click=\"annotateSnapshot\"\n      >\n        <span class=\"title-label\">Annotate</span>\n      </button>\n    </div>\n  </div>\n\n  <div\n    ref=\"snapshot-image\"\n    class=\"c-notebook-snapshot__image\"\n    :style=\"{ backgroundImage: 'url(' + src + ')' }\"\n    role=\"img\"\n    alt=\"Annotatable Snapshot\"\n  ></div>\n</div>\n"
  },
  {
    "path": "src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js",
    "content": "import _ from 'lodash';\nimport { toRaw } from 'vue';\n\nimport {\n  isAnnotationType,\n  isNotebookOrAnnotationType,\n  isNotebookType\n} from './notebook-constants.js';\n\nexport default function (openmct) {\n  const apiSave = openmct.objects.save.bind(openmct.objects);\n\n  openmct.objects.save = async (domainObject) => {\n    if (!isNotebookOrAnnotationType(domainObject)) {\n      return apiSave(domainObject);\n    }\n\n    const isNewMutable = !domainObject.isMutable;\n    const localMutable = openmct.objects.toMutable(domainObject);\n    let result;\n\n    try {\n      result = await apiSave(localMutable);\n    } catch (error) {\n      if (error instanceof openmct.objects.errors.Conflict) {\n        result = await resolveConflicts(domainObject, localMutable, openmct);\n      } else {\n        result = Promise.reject(error);\n      }\n    } finally {\n      if (isNewMutable) {\n        openmct.objects.destroyMutable(localMutable);\n      }\n    }\n\n    return result;\n  };\n}\n\nfunction resolveConflicts(domainObject, localMutable, openmct) {\n  if (isNotebookType(domainObject)) {\n    return resolveNotebookEntryConflicts(localMutable, openmct);\n  } else if (isAnnotationType(domainObject)) {\n    return resolveNotebookTagConflicts(localMutable, openmct);\n  }\n}\n\nasync function resolveNotebookTagConflicts(localAnnotation, openmct) {\n  const localClonedAnnotation = structuredClone(toRaw(localAnnotation));\n  const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);\n\n  // should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the\n  // same targetID, entryID, and tags for this conflict\n  if (!_.isEqual(remoteMutable.tags, localClonedAnnotation.tags)) {\n    throw new Error(\"Conflict on annotation's tag has different tags than remote\");\n  }\n\n  localClonedAnnotation.targets.forEach((target) => {\n    const targetKey = target.keyString;\n\n    const remoteMutableTarget = remoteMutable.targets.find((remoteTarget) => {\n      return remoteTarget.keyString === targetKey;\n    });\n    if (!remoteMutableTarget) {\n      throw new Error(`Conflict on annotation's target is missing ${targetKey}`);\n    }\n    const localMutableTarget = localClonedAnnotation.targets.find((localTarget) => {\n      return localTarget.keyString === targetKey;\n    });\n\n    if (remoteMutableTarget.entryId !== localMutableTarget.entryId) {\n      throw new Error(\n        `Conflict on annotation's entryID ${remoteMutableTarget.entryId} has a different entry Id ${localMutableTarget.entryId}`\n      );\n    }\n  });\n\n  if (remoteMutable._deleted && remoteMutable._deleted !== localClonedAnnotation._deleted) {\n    // not deleting wins 😘\n    openmct.objects.mutate(remoteMutable, '_deleted', false);\n  }\n\n  openmct.objects.destroyMutable(remoteMutable);\n\n  return true;\n}\n\nasync function resolveNotebookEntryConflicts(localMutable, openmct) {\n  if (localMutable.configuration.entries) {\n    const FORCE_REMOTE = true;\n    const localEntries = structuredClone(toRaw(localMutable.configuration.entries));\n    const remoteObject = await openmct.objects.get(\n      localMutable.identifier,\n      undefined,\n      FORCE_REMOTE\n    );\n\n    return applyLocalEntries(remoteObject, localEntries, openmct);\n  }\n\n  return true;\n}\n\nfunction applyLocalEntries(remoteObject, entries, openmct) {\n  let shouldSave = false;\n\n  Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {\n    Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {\n      const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];\n      const mergedEntries = [].concat(remoteEntries);\n      let shouldMutate = false;\n\n      const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');\n      const locallyModifiedEntries = _.differenceWith(\n        localEntries,\n        remoteEntries,\n        (localEntry, remoteEntry) => {\n          return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;\n        }\n      );\n\n      locallyAddedEntries.forEach((localEntry) => {\n        mergedEntries.push(localEntry);\n        shouldMutate = true;\n      });\n\n      locallyModifiedEntries.forEach((locallyModifiedEntry) => {\n        let mergedEntry = mergedEntries.find((entry) => entry.id === locallyModifiedEntry.id);\n        if (mergedEntry !== undefined && locallyModifiedEntry.text.match(/\\S/)) {\n          mergedEntry.text = locallyModifiedEntry.text;\n          shouldMutate = true;\n        }\n      });\n\n      if (shouldMutate) {\n        shouldSave = true;\n        openmct.objects.mutate(\n          remoteObject,\n          `configuration.entries.${sectionKey}.${pageKey}`,\n          mergedEntries\n        );\n      }\n    });\n  });\n\n  if (shouldSave) {\n    return openmct.objects.save(remoteObject);\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/notebook-constants.js",
    "content": "export const NOTEBOOK_TYPE = 'notebook';\nexport const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';\nexport const ANNOTATION_TYPE = 'annotation';\nexport const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';\nexport const NOTEBOOK_DEFAULT = 'DEFAULT';\nexport const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';\nexport const NOTEBOOK_VIEW_TYPE = 'notebook-vue';\nexport const RESTRICTED_NOTEBOOK_VIEW_TYPE = 'restricted-notebook-vue';\nexport const NOTEBOOK_BASE_INSTALLED = '_NOTEBOOK_BASE_FUNCTIONALITY_INSTALLED';\n\n// these only deals with constants, figured this could skip going into a utils file\nexport function isNotebookOrAnnotationType(domainObject) {\n  return isNotebookType(domainObject) || isAnnotationType(domainObject);\n}\n\nexport function isNotebookType(domainObject) {\n  return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);\n}\n\nexport function isAnnotationType(domainObject) {\n  return [ANNOTATION_TYPE].includes(domainObject.type);\n}\n\nexport function isNotebookViewType(view) {\n  return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);\n}\n"
  },
  {
    "path": "src/plugins/notebook/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { notebookImageMigration } from '../notebook/utils/notebook-migration.js';\nimport CopyToNotebookAction from './actions/CopyToNotebookAction.js';\nimport ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction.js';\nimport NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';\nimport monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';\nimport {\n  NOTEBOOK_BASE_INSTALLED,\n  NOTEBOOK_TYPE,\n  NOTEBOOK_VIEW_TYPE,\n  RESTRICTED_NOTEBOOK_TYPE,\n  RESTRICTED_NOTEBOOK_VIEW_TYPE\n} from './notebook-constants.js';\nimport NotebookType from './NotebookType.js';\nimport NotebookViewProvider from './NotebookViewProvider.js';\nimport SnapshotContainer from './snapshot-container.js';\n\nlet notebookSnapshotContainer;\nexport function getSnapshotContainer(openmct) {\n  if (!notebookSnapshotContainer) {\n    notebookSnapshotContainer = new SnapshotContainer(openmct);\n  }\n\n  return notebookSnapshotContainer;\n}\n\nfunction addLegacyNotebookGetInterceptor(openmct) {\n  openmct.objects.addGetInterceptor({\n    appliesTo: (identifier, domainObject) => {\n      return domainObject && domainObject.type === NOTEBOOK_TYPE;\n    },\n    invoke: (identifier, domainObject) => {\n      notebookImageMigration(openmct, domainObject);\n\n      return domainObject;\n    }\n  });\n}\n\nfunction installBaseNotebookFunctionality(openmct) {\n  // only need to do this once\n  if (openmct[NOTEBOOK_BASE_INSTALLED]) {\n    return;\n  }\n\n  const notebookSnapshotImageType = {\n    name: 'Notebook Snapshot Image Storage',\n    description: 'Notebook Snapshot Image Storage object',\n    creatable: false,\n    initialize: (domainObject) => {\n      domainObject.configuration = {\n        fullSizeImageURL: undefined,\n        thumbnailImageURL: undefined\n      };\n    }\n  };\n  openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);\n  openmct.actions.register(new CopyToNotebookAction(openmct));\n  openmct.actions.register(new ExportNotebookAsTextAction(openmct));\n\n  const indicator = {\n    vueComponent: NotebookSnapshotIndicator,\n    key: 'notebook-snapshot-indicator',\n    priority: openmct.priority.DEFAULT\n  };\n\n  openmct.indicators.add(indicator);\n\n  monkeyPatchObjectAPIForNotebooks(openmct);\n\n  openmct[NOTEBOOK_BASE_INSTALLED] = true;\n}\n\nfunction NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {\n  return function install(openmct) {\n    const icon = 'icon-notebook';\n    const description = 'Create and save timestamped notes with embedded object snapshots.';\n    const snapshotContainer = getSnapshotContainer(openmct);\n\n    addLegacyNotebookGetInterceptor(openmct);\n\n    const notebookType = new NotebookType(name, description, icon);\n    openmct.types.addType(NOTEBOOK_TYPE, notebookType);\n\n    const notebookView = new NotebookViewProvider(\n      openmct,\n      name,\n      NOTEBOOK_VIEW_TYPE,\n      NOTEBOOK_TYPE,\n      icon,\n      snapshotContainer,\n      entryUrlWhitelist\n    );\n    openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);\n\n    installBaseNotebookFunctionality(openmct);\n  };\n}\n\nfunction RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) {\n  return function install(openmct) {\n    const icon = 'icon-notebook-shift-log';\n    const description =\n      'Create and save timestamped notes with embedded object snapshots with the ability to commit and lock pages.';\n    const snapshotContainer = getSnapshotContainer(openmct);\n\n    const notebookType = new NotebookType(name, description, icon);\n    openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType);\n\n    const notebookView = new NotebookViewProvider(\n      openmct,\n      name,\n      RESTRICTED_NOTEBOOK_VIEW_TYPE,\n      RESTRICTED_NOTEBOOK_TYPE,\n      icon,\n      snapshotContainer,\n      entryUrlWhitelist\n    );\n    openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);\n\n    installBaseNotebookFunctionality(openmct);\n  };\n}\n\nexport { NotebookPlugin, RestrictedNotebookPlugin };\n"
  },
  {
    "path": "src/plugins/notebook/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { NotebookPlugin } from './plugin.js';\n\ndescribe('Notebook plugin:', () => {\n  let openmct;\n  let notebookDefinition;\n  let element;\n  let child;\n  let appHolder;\n  let objectProviderObserver;\n\n  let notebookDomainObject;\n  let originalAnnotations;\n\n  beforeEach((done) => {\n    notebookDomainObject = {\n      identifier: {\n        key: 'notebook',\n        namespace: 'test-namespace'\n      },\n      type: 'notebook'\n    };\n\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n    document.body.appendChild(appHolder);\n\n    openmct = createOpenMct();\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.install(NotebookPlugin());\n    originalAnnotations = openmct.annotation.getNotebookAnnotation;\n    // eslint-disable-next-line require-await\n    openmct.annotation.getNotebookAnnotation = async function () {\n      return null;\n    };\n\n    notebookDefinition = openmct.types.get('notebook').definition;\n    notebookDefinition.initialize(notebookDomainObject);\n\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    appHolder.remove();\n    openmct.annotation.getNotebookAnnotation = originalAnnotations;\n\n    return resetApplicationState(openmct);\n  });\n\n  it('has type as Notebook', () => {\n    expect(notebookDefinition.name).toEqual('Notebook');\n  });\n\n  it('is creatable', () => {\n    expect(notebookDefinition.creatable).toEqual(true);\n  });\n\n  describe('Notebook view:', () => {\n    let notebookViewProvider;\n    let notebookView;\n    let notebookViewObject;\n    let mutableNotebookObject;\n\n    beforeEach(async () => {\n      notebookViewObject = {\n        ...notebookDomainObject,\n        id: 'test-object',\n        name: 'Notebook',\n        configuration: {\n          defaultSort: 'oldest',\n          entries: {\n            'test-section-1': {\n              'test-page-1': [\n                {\n                  id: 'entry-0',\n                  createdOn: 0,\n                  text: 'First Test Entry',\n                  embeds: []\n                },\n                {\n                  id: 'entry-1',\n                  createdOn: 0,\n                  text: 'Second Test Entry',\n                  embeds: []\n                }\n              ]\n            }\n          },\n          pageTitle: 'Page',\n          sections: [\n            {\n              id: 'test-section-1',\n              isDefault: false,\n              isSelected: false,\n              name: 'Test Section',\n              pages: [\n                {\n                  id: 'test-page-1',\n                  isDefault: false,\n                  isSelected: false,\n                  name: 'Test Page 1',\n                  pageTitle: 'Page'\n                },\n                {\n                  id: 'test-page-2',\n                  isDefault: false,\n                  isSelected: false,\n                  name: 'Test Page 2',\n                  pageTitle: 'Page'\n                }\n              ]\n            },\n            {\n              id: 'test-section-2',\n              isDefault: false,\n              isSelected: false,\n              name: 'Test Section 2',\n              pages: [\n                {\n                  id: 'test-page-3',\n                  isDefault: false,\n                  isSelected: false,\n                  name: 'Test Page 3',\n                  pageTitle: 'Page'\n                }\n              ]\n            }\n          ],\n          sectionTitle: 'Section',\n          type: 'General'\n        }\n      };\n      const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [\n        'get',\n        'create',\n        'update',\n        'observe'\n      ]);\n\n      openmct.editor = {};\n      openmct.editor.isEditing = () => false;\n      openmct.editor.on = () => {};\n      openmct.editor.off = () => {};\n\n      const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);\n      notebookViewProvider = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'notebook-vue'\n      );\n\n      testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));\n      testObjectProvider.create.and.returnValue(Promise.resolve(notebookViewObject));\n      openmct.objects.addProvider('test-namespace', testObjectProvider);\n      testObjectProvider.observe.and.returnValue(() => {});\n      testObjectProvider.create.and.returnValue(Promise.resolve(true));\n      testObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n      const mutableObject = await openmct.objects.getMutable(notebookViewObject.identifier);\n      mutableNotebookObject = mutableObject;\n      objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];\n\n      notebookView = notebookViewProvider.view(mutableNotebookObject, [mutableNotebookObject]);\n      notebookView.show(child);\n\n      await nextTick();\n    });\n\n    afterEach(() => {\n      notebookView.destroy();\n      openmct.objects.destroyMutable(mutableNotebookObject);\n    });\n\n    it('provides notebook view', () => {\n      expect(notebookViewProvider).toBeDefined();\n    });\n\n    it('renders notebook element', () => {\n      const notebookElement = element.querySelectorAll('.c-notebook');\n      expect(notebookElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const notebookElement = element.querySelector('.c-notebook');\n      const searchElement = notebookElement.querySelector('.c-search');\n      const sidebarElement = notebookElement.querySelector('.c-sidebar');\n      const pageViewElement = notebookElement.querySelector('.c-notebook__page-view');\n      const hasMajorElements = Boolean(searchElement && sidebarElement && pageViewElement);\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('renders a row for each entry', () => {\n      const notebookEntryElements = element.querySelectorAll('.c-notebook__entry');\n      const firstEntryText = getEntryText(0);\n      expect(notebookEntryElements.length).toBe(2);\n      expect(firstEntryText.innerText.trim()).toBe('First Test Entry');\n    });\n\n    describe('synchronization', () => {\n      let objectCloneToSyncFrom;\n\n      beforeEach(() => {\n        objectCloneToSyncFrom = structuredClone(notebookViewObject);\n        objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;\n      });\n\n      it('updates an entry when another user modifies it', () => {\n        expect(getEntryText(0).innerText.trim()).toBe('First Test Entry');\n        objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'][0].text =\n          'Modified entry text';\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        return nextTick().then(() => {\n          expect(getEntryText(0).innerText.trim()).toBe('Modified entry text');\n        });\n      });\n\n      it('shows new entry when another user adds one', () => {\n        expect(allNotebookEntryElements().length).toBe(2);\n        objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'].push({\n          id: 'entry-3',\n          createdOn: 0,\n          text: 'Third Test Entry',\n          embeds: []\n        });\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        return nextTick().then(() => {\n          expect(allNotebookEntryElements().length).toBe(3);\n        });\n      });\n      it('removes an entry when another user removes one', () => {\n        expect(allNotebookEntryElements().length).toBe(2);\n        let entries = objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'];\n        objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'] =\n          entries.splice(0, 1);\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        return nextTick().then(() => {\n          expect(allNotebookEntryElements().length).toBe(1);\n        });\n      });\n\n      xit('updates the notebook when a user adds a page', async () => {\n        const newPage = {\n          id: 'test-page-4',\n          isDefault: false,\n          isSelected: false,\n          name: 'Test Page 4',\n          pageTitle: 'Page'\n        };\n\n        expect(allNotebookPageElements().length).toBe(2);\n        objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage);\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        await nextTick();\n        expect(allNotebookPageElements().length).toBe(3);\n      });\n\n      xit('updates the notebook when a user removes a page', async () => {\n        expect(allNotebookPageElements().length).toBe(2);\n        objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1);\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        await nextTick();\n        expect(allNotebookPageElements().length).toBe(1);\n      });\n\n      xit('updates the notebook when a user adds a section', () => {\n        const newSection = {\n          id: 'test-section-3',\n          isDefault: false,\n          isSelected: false,\n          name: 'Test Section 3',\n          pages: [\n            {\n              id: 'test-page-4',\n              isDefault: false,\n              isSelected: false,\n              name: 'Test Page 4',\n              pageTitle: 'Page'\n            }\n          ]\n        };\n\n        expect(allNotebookSectionElements().length).toBe(2);\n        objectCloneToSyncFrom.configuration.sections.push(newSection);\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        return nextTick().then(() => {\n          expect(allNotebookSectionElements().length).toBe(3);\n        });\n      });\n\n      xit('updates the notebook when a user removes a section', () => {\n        expect(allNotebookSectionElements().length).toBe(2);\n        objectCloneToSyncFrom.configuration.sections.splice(0, 1);\n        objectProviderObserver(objectCloneToSyncFrom);\n\n        return nextTick().then(() => {\n          expect(allNotebookSectionElements().length).toBe(1);\n        });\n      });\n    });\n  });\n\n  describe('Notebook Snapshots view:', () => {\n    let snapshotIndicator;\n    let drawerElement;\n\n    async function clickSnapshotIndicator() {\n      const button =\n        appHolder.querySelector('[aria-label=\"Show Snapshots\"]') ??\n        appHolder.querySelector('[aria-label=\"Hide Snapshots\"]');\n      const clickEvent = createMouseEvent('click');\n\n      button.dispatchEvent(clickEvent);\n      await nextTick();\n    }\n\n    beforeEach(async () => {\n      snapshotIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'notebook-snapshot-indicator'\n      ).vueComponent;\n\n      await nextTick();\n      drawerElement = document.querySelector('.l-shell__drawer');\n    });\n\n    afterEach(() => {\n      if (drawerElement) {\n        drawerElement.classList.remove('is-expanded');\n      }\n\n      snapshotIndicator = undefined;\n\n      if (drawerElement) {\n        drawerElement.remove();\n        drawerElement = undefined;\n      }\n    });\n\n    it('has Snapshots indicator', () => {\n      const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;\n      expect(hasSnapshotIndicator).toBe(true);\n    });\n\n    it('snapshots container has class isExpanded', async () => {\n      let classes = drawerElement.classList;\n      const isExpandedBefore = classes.contains('is-expanded');\n\n      await clickSnapshotIndicator();\n      classes = drawerElement.classList;\n      const isExpandedAfterFirstClick = classes.contains('is-expanded');\n\n      expect(isExpandedBefore).toBeFalse();\n      expect(isExpandedAfterFirstClick).toBeTrue();\n    });\n\n    it('snapshots container does not have class isExpanded', async () => {\n      let classes = drawerElement.classList;\n      const isExpandedBefore = classes.contains('is-expanded');\n\n      await clickSnapshotIndicator();\n      classes = drawerElement.classList;\n      const isExpandedAfterFirstClick = classes.contains('is-expanded');\n\n      await clickSnapshotIndicator();\n      classes = drawerElement.classList;\n      const isExpandedAfterSecondClick = classes.contains('is-expanded');\n\n      expect(isExpandedBefore).toBeFalse();\n      expect(isExpandedAfterFirstClick).toBeTrue();\n      expect(isExpandedAfterSecondClick).toBeFalse();\n    });\n\n    it('show notebook snapshots container text', async () => {\n      await clickSnapshotIndicator();\n\n      const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');\n      const snapshotsText = notebookSnapshots.textContent.trim();\n\n      expect(snapshotsText).toBe('Notebook Snapshots');\n    });\n  });\n\n  function getEntryText(entryNumber) {\n    return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber];\n  }\n\n  function allNotebookEntryElements() {\n    return element.querySelectorAll('.c-notebook__entry');\n  }\n\n  function allNotebookSectionElements() {\n    return element.querySelectorAll('.js-sidebar-sections .js-list__item');\n  }\n\n  function allNotebookPageElements() {\n    return element.querySelectorAll('.js-sidebar-pages .js-list__item');\n  }\n});\n"
  },
  {
    "path": "src/plugins/notebook/snapshot-container.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nimport { EVENT_SNAPSHOTS_UPDATED } from './notebook-constants.js';\nconst NOTEBOOK_SNAPSHOT_STORAGE = 'notebook-snapshot-storage';\n\nexport const NOTEBOOK_SNAPSHOT_MAX_COUNT = 5;\n\nexport default class SnapshotContainer extends EventEmitter {\n  constructor(openmct) {\n    super();\n\n    if (!SnapshotContainer.instance) {\n      SnapshotContainer.instance = this;\n    }\n\n    this.openmct = openmct;\n\n    // eslint-disable-next-line\n    return SnapshotContainer.instance;\n  }\n\n  addSnapshot(notebookImageDomainObject, embedObject) {\n    const snapshots = this.getSnapshots();\n    if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {\n      snapshots.pop();\n    }\n\n    const snapshotObject = {\n      notebookImageDomainObject,\n      embedObject\n    };\n\n    snapshots.unshift(snapshotObject);\n\n    return this.saveSnapshots(snapshots);\n  }\n\n  getSnapshot(id) {\n    const snapshots = this.getSnapshots();\n\n    return snapshots.find((s) => s.embedObject.id === id);\n  }\n\n  getSnapshots() {\n    const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]';\n\n    return JSON.parse(snapshots);\n  }\n\n  removeSnapshot(id) {\n    if (!id) {\n      return;\n    }\n\n    const snapshots = this.getSnapshots();\n    const filteredsnapshots = snapshots.filter((snapshot) => snapshot.embedObject.id !== id);\n\n    return this.saveSnapshots(filteredsnapshots);\n  }\n\n  removeAllSnapshots() {\n    return this.saveSnapshots([]);\n  }\n\n  saveSnapshots(snapshots) {\n    try {\n      window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots));\n      this.emit(EVENT_SNAPSHOTS_UPDATED, true);\n\n      return true;\n    } catch (e) {\n      const message =\n        'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!';\n      this.openmct.notifications.error(message);\n\n      return false;\n    }\n  }\n\n  updateSnapshot(snapshot) {\n    const snapshots = this.getSnapshots();\n    const updatedSnapshots = snapshots.map((s) => {\n      return s.embedObject.id === snapshot.embedObject.id ? snapshot : s;\n    });\n\n    return this.saveSnapshots(updatedSnapshots);\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/snapshot.js",
    "content": "import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';\n\nimport ImageExporter from '../../exporters/ImageExporter.js';\nimport SnapshotContainer from './snapshot-container.js';\nimport { addNotebookEntry, createNewEmbed } from './utils/notebook-entries.js';\nimport {\n  createNotebookImageDomainObject,\n  DEFAULT_SIZE,\n  saveNotebookImageDomainObject,\n  updateNamespaceOfDomainObject\n} from './utils/notebook-image.js';\nimport {\n  getDefaultNotebook,\n  getDefaultNotebookLink,\n  getNotebookSectionAndPage,\n  setDefaultNotebook\n} from './utils/notebook-storage.js';\n\nexport default class Snapshot {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.snapshotContainer = new SnapshotContainer(openmct);\n    this.imageExporter = new ImageExporter(openmct);\n\n    this.capture = this.capture.bind(this);\n    this._saveSnapShot = this._saveSnapShot.bind(this);\n  }\n\n  capture(snapshotMeta, notebookType, domElement) {\n    const options = {\n      className: 's-status-taking-snapshot',\n      thumbnailSize: DEFAULT_SIZE\n    };\n    this.imageExporter.exportPNGtoSRC(domElement, options).then(\n      function ({ blob, thumbnail }) {\n        const reader = new window.FileReader();\n        reader.readAsDataURL(blob);\n        reader.onloadend = function () {\n          this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta);\n        }.bind(this);\n      }.bind(this)\n    );\n  }\n\n  /**\n   * @private\n   */\n  _saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {\n    const object = createNotebookImageDomainObject(fullSizeImageURL);\n    const thumbnailImage = { src: thumbnailImageURL || '' };\n    const snapshot = {\n      fullSizeImageObjectIdentifier: object.identifier,\n      thumbnailImage\n    };\n    createNewEmbed(snapshotMeta, snapshot).then((embed) => {\n      if (notebookType === NOTEBOOK_DEFAULT) {\n        const notebookStorage = getDefaultNotebook();\n\n        this._saveToDefaultNoteBook(notebookStorage, embed);\n        const notebookImageDomainObject = updateNamespaceOfDomainObject(\n          object,\n          notebookStorage.identifier.namespace\n        );\n        saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);\n      } else {\n        this._saveToNotebookSnapshots(object, embed);\n      }\n    });\n  }\n\n  /**\n   * @private\n   */\n  _saveToDefaultNoteBook(notebookStorage, embed) {\n    this.openmct.objects.get(notebookStorage.identifier).then((domainObject) => {\n      return addNotebookEntry(this.openmct, domainObject, notebookStorage, embed).then(async () => {\n        let link = notebookStorage.link;\n\n        // Backwards compatibility fix (old notebook model without link)\n        if (!link) {\n          link = await getDefaultNotebookLink(this.openmct, domainObject);\n          notebookStorage.link = link;\n          setDefaultNotebook(this.openmct, notebookStorage);\n        }\n\n        const { section, page } = getNotebookSectionAndPage(\n          domainObject,\n          notebookStorage.defaultSectionId,\n          notebookStorage.defaultPageId\n        );\n        if (!section || !page) {\n          return;\n        }\n\n        const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;\n        const msg = `Saved to Notebook ${defaultPath}`;\n        this._showNotification(msg, link);\n      });\n    });\n  }\n\n  /**\n   * @private\n   */\n  _saveToNotebookSnapshots(notebookImageDomainObject, embed) {\n    this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed);\n  }\n\n  _showNotification(msg, url) {\n    const options = {\n      autoDismissTimeout: 30000\n    };\n\n    if (!this.openmct.editor.isEditing()) {\n      options.link = {\n        cssClass: '',\n        text: 'click to view',\n        onClick: this._navigateToNotebook(url)\n      };\n    }\n\n    this.openmct.notifications.info(msg, options);\n  }\n\n  _navigateToNotebook(url = null) {\n    if (!url) {\n      return () => {};\n    }\n\n    return () => {\n      location.hash = url;\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-entries.js",
    "content": "import { v4 as uuid } from 'uuid';\n\nimport objectLink from '../../../ui/mixins/object-link.js';\nimport {\n  createNotebookImageDomainObject,\n  getThumbnailURLFromImageUrl,\n  saveNotebookImageDomainObject\n} from './notebook-image.js';\n\nasync function getUsername(openmct) {\n  let username = null;\n\n  if (openmct.user.hasProvider()) {\n    const user = await openmct.user.getCurrentUser();\n    username = user.getName();\n  }\n\n  return username;\n}\n\nasync function getActiveRole(openmct) {\n  let role = null;\n  if (openmct.user.hasProvider()) {\n    role = await openmct.user.getActiveRole?.();\n  }\n\n  return role;\n}\n\nexport const DEFAULT_CLASS = 'notebook-default';\nconst TIME_BOUNDS = {\n  START_BOUND: 'tc.startBound',\n  END_BOUND: 'tc.endBound',\n  START_DELTA: 'tc.startDelta',\n  END_DELTA: 'tc.endDelta'\n};\n\nexport function addEntryIntoPage(notebookStorage, entries, entry) {\n  const defaultSectionId = notebookStorage.defaultSectionId;\n  const defaultPageId = notebookStorage.defaultPageId;\n  if (!defaultSectionId || !defaultPageId) {\n    return;\n  }\n\n  const newEntries = JSON.parse(JSON.stringify(entries));\n  let section = newEntries[defaultSectionId];\n  if (!section) {\n    newEntries[defaultSectionId] = {};\n  }\n\n  let page = newEntries[defaultSectionId][defaultPageId];\n  if (!page) {\n    newEntries[defaultSectionId][defaultPageId] = [];\n  }\n\n  newEntries[defaultSectionId][defaultPageId].push(entry);\n\n  return newEntries;\n}\n\nexport function selectEntry({\n  element,\n  entryId,\n  domainObject,\n  openmct,\n  onAnnotationChange,\n  notebookAnnotations\n}) {\n  const keyString = openmct.objects.makeKeyString(domainObject.identifier);\n  const targetDetails = [\n    {\n      entryId,\n      keyString\n    }\n  ];\n  const targetDomainObjects = [domainObject];\n  openmct.selection.select(\n    [\n      {\n        element,\n        context: {\n          type: 'notebook-entry-selection',\n          item: domainObject,\n          targetDetails,\n          targetDomainObjects,\n          annotations: notebookAnnotations,\n          annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n          onAnnotationChange\n        }\n      }\n    ],\n    false\n  );\n}\n\nexport function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {\n  if (historicLink.includes('tc.mode=fixed')) {\n    return historicLink;\n  }\n\n  openmct.time.getAllClocks().forEach((clock) => {\n    if (historicLink.includes(`tc.mode=${clock.key}`)) {\n      historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed');\n\n      return;\n    }\n  });\n\n  const params = historicLink.split('&').map((param) => {\n    if (param.includes(TIME_BOUNDS.START_BOUND) || param.includes(TIME_BOUNDS.START_DELTA)) {\n      param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`;\n    }\n\n    if (param.includes(TIME_BOUNDS.END_BOUND) || param.includes(TIME_BOUNDS.END_DELTA)) {\n      param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`;\n    }\n\n    return param;\n  });\n\n  return params.join('&');\n}\n\nexport function createNewImageEmbed(image, openmct, imageName = '') {\n  return new Promise((resolve) => {\n    const reader = new FileReader();\n    reader.onloadend = async () => {\n      try {\n        const base64Data = reader.result;\n        const blobUrl = URL.createObjectURL(image);\n        const imageDomainObject = createNotebookImageDomainObject(base64Data);\n        await saveNotebookImageDomainObject(openmct, imageDomainObject);\n        const imageThumbnailURL = await getThumbnailURLFromImageUrl(blobUrl);\n\n        const snapshot = {\n          fullSizeImageObjectIdentifier: imageDomainObject.identifier,\n          thumbnailImage: {\n            src: imageThumbnailURL\n          }\n        };\n\n        const embedMetaData = {\n          bounds: openmct.time.getBounds(),\n          link: null,\n          objectPath: null,\n          openmct,\n          userImage: true,\n          imageName\n        };\n\n        const createdEmbed = await createNewEmbed(embedMetaData, snapshot);\n        resolve(createdEmbed);\n      } catch (error) {\n        console.error(`${error.message} - unable to embed image ${imageName}`, error);\n        openmct.notifications.error(`${error.message} -- unable to embed image ${imageName}`);\n      }\n    };\n\n    reader.readAsDataURL(image);\n  });\n}\n\nexport async function createNewEmbed(snapshotMeta, snapshot = '') {\n  const { bounds, link, objectPath, openmct, userImage } = snapshotMeta;\n  let name = null;\n  let type = null;\n  let cssClass = 'icon-object-unknown';\n  let domainObject = null;\n  let historicLink = null;\n  if (objectPath?.length > 0) {\n    domainObject = objectPath[0];\n    const domainObjectType = openmct.types.get(domainObject.type);\n    cssClass = domainObjectType?.definition\n      ? domainObjectType.definition.cssClass\n      : 'icon-object-unknown';\n    name = domainObject.name;\n    type = domainObject.identifier.key;\n    historicLink = link\n      ? getHistoricLinkInFixedMode(openmct, bounds, link)\n      : objectLink.computed.objectLink.call({\n          objectPath,\n          openmct\n        });\n  } else if (userImage) {\n    cssClass = 'icon-image';\n    name = snapshotMeta.imageName;\n  }\n\n  const date = openmct.time.now();\n  const createdBy = await getUsername(openmct);\n\n  return {\n    bounds,\n    createdOn: date,\n    createdBy,\n    cssClass,\n    domainObject,\n    historicLink,\n    id: 'embed-' + date,\n    name,\n    snapshot,\n    type\n  };\n}\n\nexport async function addNotebookEntry(\n  openmct,\n  domainObject,\n  notebookStorage,\n  passedEmbeds = [],\n  entryText = ''\n) {\n  if (!openmct || !domainObject || !notebookStorage) {\n    return;\n  }\n\n  const date = openmct.time.now();\n  const configuration = domainObject.configuration;\n  const entries = configuration.entries || {};\n  // if embeds isn't an array, make it one\n  const embedsNormalized =\n    passedEmbeds && !Array.isArray(passedEmbeds) ? [passedEmbeds] : passedEmbeds;\n\n  const id = `entry-${uuid()}`;\n  const [createdBy, createdByRole] = await Promise.all([\n    getUsername(openmct),\n    getActiveRole(openmct)\n  ]);\n  const entry = {\n    id,\n    createdOn: date,\n    createdBy,\n    createdByRole,\n    text: entryText,\n    embeds: embedsNormalized\n  };\n\n  const newEntries = addEntryIntoPage(notebookStorage, entries, entry);\n\n  addDefaultClass(domainObject, openmct);\n  mutateObject(openmct, domainObject, 'configuration.entries', newEntries);\n\n  return id;\n}\n\nexport function getNotebookEntries(domainObject, selectedSection, selectedPage) {\n  if (!domainObject || !selectedSection || !selectedPage || !domainObject.configuration) {\n    return;\n  }\n\n  const configuration = domainObject.configuration;\n  const entries = configuration.entries || {};\n\n  let section = entries[selectedSection.id];\n  if (!section) {\n    return;\n  }\n\n  let page = entries[selectedSection.id][selectedPage.id];\n  if (!page) {\n    return;\n  }\n\n  const specificEntries = entries[selectedSection.id][selectedPage.id];\n\n  return specificEntries;\n}\n\nexport function getEntryPosById(entryId, domainObject, selectedSection, selectedPage) {\n  if (!domainObject || !selectedSection || !selectedPage) {\n    return;\n  }\n\n  const entries = getNotebookEntries(domainObject, selectedSection, selectedPage);\n  let foundId = -1;\n  entries.forEach((element, index) => {\n    if (element.id === entryId) {\n      foundId = index;\n\n      return;\n    }\n  });\n\n  return foundId;\n}\n\nexport function deleteNotebookEntries(openmct, domainObject, selectedSection, selectedPage) {\n  if (!domainObject || !selectedSection) {\n    return;\n  }\n\n  const configuration = domainObject.configuration;\n  const entries = configuration.entries || {};\n\n  // Delete entire section\n  if (!selectedPage) {\n    delete entries[selectedSection.id];\n\n    return;\n  }\n\n  let section = entries[selectedSection.id];\n  if (!section) {\n    return;\n  }\n\n  delete entries[selectedSection.id][selectedPage.id];\n\n  mutateObject(openmct, domainObject, 'configuration.entries', entries);\n}\n\nexport function mutateObject(openmct, object, key, value) {\n  openmct.objects.mutate(object, key, value);\n}\n\nfunction addDefaultClass(domainObject, openmct) {\n  openmct.status.set(domainObject.identifier, DEFAULT_CLASS);\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-entriesSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport * as NotebookEntries from './notebook-entries.js';\n\nconst notebookStorage = {\n  name: 'notebook',\n  identifier: {\n    namespace: '',\n    key: 'test-notebook'\n  },\n  defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',\n  defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'\n};\n\nconst notebookEntries = {\n  '03a79b6a-971c-4e56-9892-ec536332c3f0': {\n    '8b548fd9-2b8a-4b02-93a9-4138e22eba00': []\n  }\n};\n\nconst notebookDomainObject = {\n  identifier: {\n    key: 'notebook',\n    namespace: ''\n  },\n  type: 'notebook',\n  name: 'Test Notebook',\n  configuration: {\n    defaultSort: 'oldest',\n    entries: notebookEntries,\n    pageTitle: 'Page',\n    sections: [],\n    sectionTitle: 'Section',\n    type: 'General'\n  }\n};\n\nconst selectedSection = {\n  id: '03a79b6a-971c-4e56-9892-ec536332c3f0',\n  isDefault: false,\n  isSelected: true,\n  name: 'Day 1',\n  pages: [\n    {\n      id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',\n      isDefault: false,\n      isSelected: false,\n      name: 'Shift 1',\n      pageTitle: 'Page'\n    },\n    {\n      id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',\n      isDefault: false,\n      isSelected: false,\n      name: 'Shift 2',\n      pageTitle: 'Page'\n    },\n    {\n      id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',\n      isDefault: false,\n      isSelected: true,\n      name: 'Unnamed Page',\n      pageTitle: 'Page'\n    }\n  ],\n  sectionTitle: 'Section'\n};\n\nconst selectedPage = {\n  id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',\n  isDefault: false,\n  isSelected: true,\n  name: 'Unnamed Page',\n  pageTitle: 'Page'\n};\n\nlet openmct;\n\ndescribe('Notebook Entries:', () => {\n  beforeEach(() => {\n    openmct = createOpenMct();\n    openmct.time.setClock('local');\n    openmct.types.addType('notebook', {\n      creatable: true\n    });\n    openmct.objects.addProvider(\n      '',\n      jasmine.createSpyObj('mockNotebookProvider', ['create', 'update'])\n    );\n    openmct.editor = {\n      isEditing: () => false\n    };\n    openmct.objects.isPersistable = () => true;\n    openmct.objects.save = () => Promise.resolve(true);\n\n    window.localStorage.setItem('notebook-storage', null);\n  });\n\n  afterEach(() => {\n    notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = [];\n\n    return resetApplicationState(openmct);\n  });\n\n  it('getNotebookEntries has no entries', () => {\n    const entries = NotebookEntries.getNotebookEntries(\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n\n    expect(entries.length).toEqual(0);\n  });\n\n  it('addNotebookEntry adds entry', async () => {\n    const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {\n      const entries = NotebookEntries.getNotebookEntries(\n        notebookDomainObject,\n        selectedSection,\n        selectedPage\n      );\n\n      expect(entries.length).toEqual(1);\n      unlisten();\n    });\n\n    await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);\n  });\n\n  it('addNotebookEntry adds active user to entry', async () => {\n    const USER = 'Timmy';\n    openmct.user.hasProvider = () => true;\n    openmct.user.getCurrentUser = () => {\n      return Promise.resolve({\n        getName: () => {\n          return USER;\n        }\n      });\n    };\n\n    const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {\n      const entries = NotebookEntries.getNotebookEntries(\n        notebookDomainObject,\n        selectedSection,\n        selectedPage\n      );\n\n      expect(entries[0].createdBy).toEqual(USER);\n      unlisten();\n    });\n\n    await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);\n  });\n\n  it('getEntryPosById returns valid position', async () => {\n    const entryId1 = await NotebookEntries.addNotebookEntry(\n      openmct,\n      notebookDomainObject,\n      notebookStorage\n    );\n    const position1 = NotebookEntries.getEntryPosById(\n      entryId1,\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n\n    const entryId2 = await NotebookEntries.addNotebookEntry(\n      openmct,\n      notebookDomainObject,\n      notebookStorage\n    );\n    const position2 = NotebookEntries.getEntryPosById(\n      entryId2,\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n\n    const entryId3 = await NotebookEntries.addNotebookEntry(\n      openmct,\n      notebookDomainObject,\n      notebookStorage\n    );\n    const position3 = NotebookEntries.getEntryPosById(\n      entryId3,\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n\n    const success = position1 === 0 && position2 === 1 && position3 === 2;\n\n    expect(success).toBe(true);\n  });\n\n  it('deleteNotebookEntries deletes correct page entries', async () => {\n    await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);\n    await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);\n    NotebookEntries.deleteNotebookEntries(\n      openmct,\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n    const afterEntries = NotebookEntries.getNotebookEntries(\n      notebookDomainObject,\n      selectedSection,\n      selectedPage\n    );\n\n    expect(afterEntries).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-image.js",
    "content": "import { v4 as uuid } from 'uuid';\n\nexport const DEFAULT_SIZE = {\n  width: 30,\n  height: 30\n};\n\nexport function createNotebookImageDomainObject(fullSizeImageURL) {\n  const identifier = {\n    key: uuid(),\n    namespace: ''\n  };\n  const viewType = 'notebookSnapshotImage';\n\n  return {\n    name: 'Notebook Snapshot Image',\n    type: viewType,\n    identifier,\n    configuration: {\n      fullSizeImageURL\n    }\n  };\n}\n\nexport function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {\n  const thumbnailCanvas = document.createElement('canvas');\n  thumbnailCanvas.setAttribute('width', size.width);\n  thumbnailCanvas.setAttribute('height', size.height);\n  const ctx = thumbnailCanvas.getContext('2d');\n  ctx.globalCompositeOperation = 'copy';\n  ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);\n\n  return thumbnailCanvas.toDataURL('image/png');\n}\n\nexport function getThumbnailURLFromImageUrl(imageUrl, size = DEFAULT_SIZE) {\n  return new Promise((resolve) => {\n    const image = new Image();\n\n    const canvas = document.createElement('canvas');\n    canvas.width = size.width;\n    canvas.height = size.height;\n\n    image.onload = function () {\n      canvas.getContext('2d').drawImage(image, 0, 0, size.width, size.height);\n      resolve(canvas.toDataURL('image/png'));\n    };\n\n    image.src = imageUrl;\n  });\n}\n\nexport async function saveNotebookImageDomainObject(openmct, object) {\n  await openmct.objects.save(object);\n}\n\nexport async function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {\n  const domainObject = await openmct.objects.get(identifier);\n  const configuration = domainObject.configuration;\n  configuration.fullSizeImageURL = fullSizeImage.src;\n  try {\n    // making a transactions as we can't catch errors on mutations\n    if (!openmct.objects.isTransactionActive()) {\n      openmct.objects.startTransaction();\n    }\n    openmct.objects.mutate(domainObject, 'configuration', configuration);\n    const transaction = openmct.objects.getActiveTransaction();\n    await transaction.commit();\n    openmct.objects.endTransaction();\n  } catch (error) {\n    console.error(`${error.message} -- unable to save image`, error);\n    openmct.notifications.error(`${error.message} -- unable to save image`);\n  }\n}\n\nexport function updateNamespaceOfDomainObject(object, namespace) {\n  object.identifier.namespace = namespace;\n\n  return object;\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-key-code.js",
    "content": "// Key codes for `KeyboardEvent.keyCode`.\nexport const KEY_ENTER = 13;\nexport const KEY_ESCAPE = 27;\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-migration.js",
    "content": "import { mutateObject } from './notebook-entries.js';\nimport {\n  createNotebookImageDomainObject,\n  getThumbnailURLFromImageUrl,\n  saveNotebookImageDomainObject,\n  updateNamespaceOfDomainObject\n} from './notebook-image.js';\n\nexport const IMAGE_MIGRATION_VER = 'v1';\n\nexport function notebookImageMigration(openmct, domainObject) {\n  const configuration = domainObject.configuration;\n  const notebookEntries = configuration.entries;\n\n  const imageMigrationVer = configuration.imageMigrationVer;\n  if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) {\n    return;\n  }\n\n  configuration.imageMigrationVer = IMAGE_MIGRATION_VER;\n\n  // to avoid muliple notebookImageMigration calls updating images.\n  mutateObject(openmct, domainObject, 'configuration', configuration);\n\n  configuration.sections.forEach((section) => {\n    const sectionId = section.id;\n    section.pages.forEach((page) => {\n      const pageId = page.id;\n      const notebookSection = (notebookEntries && notebookEntries[sectionId]) || {};\n      const pageEntries = (notebookSection && notebookSection[pageId]) || [];\n      pageEntries.forEach((entry) => {\n        entry.embeds.forEach(async (embed) => {\n          const snapshot = embed.snapshot;\n          const fullSizeImageURL = snapshot.src;\n          if (fullSizeImageURL) {\n            const thumbnailImageURL = await getThumbnailURLFromImageUrl(fullSizeImageURL);\n            const object = createNotebookImageDomainObject(fullSizeImageURL);\n            const notebookImageDomainObject = updateNamespaceOfDomainObject(\n              object,\n              domainObject.identifier.namespace\n            );\n            embed.snapshot = {\n              fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,\n              thumbnailImage: { src: thumbnailImageURL || '' }\n            };\n\n            mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);\n\n            saveNotebookImageDomainObject(openmct, notebookImageDomainObject);\n          }\n        });\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-snapshot-menu.js",
    "content": "import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage.js';\n\nexport async function getMenuItems(openmct, menuItemOptions) {\n  const notebookTypes = [];\n\n  const defaultNotebook = getDefaultNotebook();\n  const defaultNotebookObject =\n    defaultNotebook && (await openmct.objects.get(defaultNotebook.identifier));\n  if (defaultNotebookObject) {\n    const { section, page } = getNotebookSectionAndPage(\n      defaultNotebookObject,\n      defaultNotebook.defaultSectionId,\n      defaultNotebook.defaultPageId\n    );\n    if (section && page) {\n      const name = defaultNotebookObject.name;\n      const sectionName = section.name;\n      const pageName = page.name;\n      const defaultPath = `${name} - ${sectionName} - ${pageName}`;\n\n      notebookTypes.push({\n        cssClass: menuItemOptions.default.cssClass,\n        name: `${menuItemOptions.default.name} ${defaultPath}`,\n        onItemClicked: menuItemOptions.default.onItemClicked\n      });\n    }\n  }\n\n  notebookTypes.push({\n    cssClass: menuItemOptions.snapshot.cssClass,\n    name: menuItemOptions.snapshot.name,\n    onItemClicked: menuItemOptions.snapshot.onItemClicked\n  });\n\n  return notebookTypes;\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-storage.js",
    "content": "import { makeKeyString } from 'objectUtils';\n\nconst NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';\nlet currentNotebookObjectIdentifier = null;\nlet unlisten = null;\n\nfunction defaultNotebookObjectChanged(newDomainObject) {\n  if (newDomainObject.location !== null) {\n    currentNotebookObjectIdentifier = newDomainObject.identifier;\n\n    return;\n  }\n\n  if (unlisten) {\n    unlisten();\n    unlisten = null;\n  }\n\n  clearDefaultNotebook();\n}\n\nfunction observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {\n  if (\n    currentNotebookObjectIdentifier &&\n    makeKeyString(currentNotebookObjectIdentifier) === makeKeyString(notebookStorage.identifier)\n  ) {\n    return;\n  }\n\n  removeListener();\n\n  unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged);\n}\n\nfunction removeListener() {\n  if (unlisten) {\n    unlisten();\n    unlisten = null;\n  }\n}\n\nfunction saveDefaultNotebook(notebookStorage) {\n  window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));\n}\n\nexport function clearDefaultNotebook() {\n  currentNotebookObjectIdentifier = null;\n  removeListener();\n\n  window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);\n}\n\nexport function getDefaultNotebook() {\n  const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE);\n\n  return JSON.parse(notebookStorage);\n}\n\nexport function getNotebookSectionAndPage(domainObject, sectionId, pageId) {\n  const configuration = domainObject.configuration;\n  const section = configuration && configuration.sections.find((s) => s.id === sectionId);\n  const page = section && section.pages.find((p) => p.id === pageId);\n\n  return {\n    section,\n    page\n  };\n}\n\nexport async function getDefaultNotebookLink(openmct, domainObject = null) {\n  if (!domainObject) {\n    return null;\n  }\n\n  const path = await openmct.objects\n    .getOriginalPath(domainObject.identifier)\n    .then(openmct.objects.getRelativePath);\n  const { defaultPageId, defaultSectionId } = getDefaultNotebook();\n\n  return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;\n}\n\nexport function setDefaultNotebook(openmct, notebookStorage, domainObject) {\n  observeDefaultNotebookObject(openmct, notebookStorage, domainObject);\n  saveDefaultNotebook(notebookStorage);\n}\n\nexport function setDefaultNotebookSectionId(sectionId) {\n  const notebookStorage = getDefaultNotebook();\n  notebookStorage.defaultSectionId = sectionId;\n  saveDefaultNotebook(notebookStorage);\n}\n\nexport function setDefaultNotebookPageId(pageId) {\n  const notebookStorage = getDefaultNotebook();\n  notebookStorage.defaultPageId = pageId;\n  saveDefaultNotebook(notebookStorage);\n}\n\nexport function validateNotebookStorageObject() {\n  const notebookStorage = getDefaultNotebook();\n  if (!notebookStorage) {\n    return true;\n  }\n\n  let valid = false;\n  if (notebookStorage) {\n    const oldInvalidKeys = ['notebookMeta', 'page', 'section'];\n    valid = Object.entries(notebookStorage).every(([key, value]) => {\n      const validKey = key !== undefined && key !== null;\n      const validValue = value !== undefined && value !== null;\n      const hasOldInvalidKeys = oldInvalidKeys.includes(key);\n\n      return validKey && validValue && !hasOldInvalidKeys;\n    });\n  }\n\n  if (valid) {\n    return notebookStorage;\n  }\n\n  console.warn('Invalid Notebook object, clearing default notebook storage');\n\n  clearDefaultNotebook();\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/notebook-storageSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport * as NotebookStorage from './notebook-storage.js';\n\nconst notebookSection = {\n  id: 'temp-section',\n  isDefault: false,\n  isSelected: true,\n  name: 'section',\n  pages: [\n    {\n      id: 'temp-page',\n      isDefault: false,\n      isSelected: true,\n      name: 'page',\n      pageTitle: 'Page'\n    }\n  ],\n  sectionTitle: 'Section'\n};\n\nconst domainObject = {\n  name: 'notebook',\n  identifier: {\n    namespace: '',\n    key: 'test-notebook'\n  },\n  configuration: {\n    sections: [notebookSection]\n  }\n};\n\nconst notebookStorage = {\n  name: 'notebook',\n  identifier: {\n    namespace: '',\n    key: 'test-notebook'\n  },\n  defaultSectionId: 'temp-section',\n  defaultPageId: 'temp-page'\n};\n\nlet openmct;\n\ndescribe('Notebook Storage:', () => {\n  beforeEach(() => {\n    openmct = createOpenMct();\n\n    window.localStorage.setItem('notebook-storage', null);\n    openmct.objects.addProvider(\n      '',\n      jasmine.createSpyObj('mockNotebookProvider', ['create', 'update'])\n    );\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('has empty local Storage', () => {\n    expect(window.localStorage).not.toBeNull();\n  });\n\n  it('has null notebookstorage on clearDefaultNotebook', () => {\n    window.localStorage.setItem('notebook-storage', notebookStorage);\n    NotebookStorage.clearDefaultNotebook();\n    const defaultNotebook = NotebookStorage.getDefaultNotebook();\n\n    expect(defaultNotebook).toBeNull();\n  });\n\n  it('has correct notebookstorage on setDefaultNotebook', () => {\n    NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);\n    const defaultNotebook = NotebookStorage.getDefaultNotebook();\n\n    expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));\n  });\n\n  it('has correct section on setDefaultNotebookSectionId', () => {\n    const section = {\n      id: 'new-temp-section',\n      isDefault: true,\n      isSelected: true,\n      name: 'new section',\n      pages: [],\n      sectionTitle: 'Section'\n    };\n\n    NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);\n    NotebookStorage.setDefaultNotebookSectionId(section.id);\n\n    const defaultNotebook = NotebookStorage.getDefaultNotebook();\n    const defaultSectionId = defaultNotebook.defaultSectionId;\n    expect(section.id).toBe(defaultSectionId);\n  });\n\n  it('has correct page on setDefaultNotebookPageId', () => {\n    const page = {\n      id: 'new-temp-page',\n      isDefault: true,\n      isSelected: true,\n      name: 'new page',\n      pageTitle: 'Page'\n    };\n\n    NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);\n    NotebookStorage.setDefaultNotebookPageId(page.id);\n\n    const defaultNotebook = NotebookStorage.getDefaultNotebook();\n    const newPageId = defaultNotebook.defaultPageId;\n    expect(page.id).toBe(newPageId);\n  });\n\n  describe('is getNotebookSectionAndPage function searches and returns correct,', () => {\n    let section;\n    let page;\n\n    beforeEach(() => {\n      const sectionId = 'temp-section';\n      const pageId = 'temp-page';\n\n      const sectionAndpage = NotebookStorage.getNotebookSectionAndPage(\n        domainObject,\n        sectionId,\n        pageId\n      );\n      section = sectionAndpage.section;\n      page = sectionAndpage.page;\n    });\n\n    it('id for section from notebook domain object', () => {\n      expect(section.id).toEqual('temp-section');\n    });\n\n    it('name for section from notebook domain object', () => {\n      expect(section.name).toEqual('section');\n    });\n\n    it('sectionTitle for section from notebook domain object', () => {\n      expect(section.sectionTitle).toEqual('Section');\n    });\n\n    it('number of pages for section from notebook domain object', () => {\n      expect(section.pages.length).toEqual(1);\n    });\n\n    it('id for page from notebook domain object', () => {\n      expect(page.id).toEqual('temp-page');\n    });\n\n    it('name for page from notebook domain object', () => {\n      expect(page.name).toEqual('page');\n    });\n\n    it('pageTitle for page from notebook domain object', () => {\n      expect(page.pageTitle).toEqual('Page');\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/notebook/utils/painterroInstance.js",
    "content": "import Painterro from 'painterro';\n\nimport { getThumbnailURLFromImageUrl } from './notebook-image.js';\n\nconst DEFAULT_CONFIG = {\n  activeColor: '#ff0000',\n  activeColorAlpha: 1.0,\n  activeFillColor: '#fff',\n  activeFillColorAlpha: 0.0,\n  backgroundFillColor: '#000',\n  backgroundFillColorAlpha: 0.0,\n  defaultFontSize: 16,\n  defaultLineWidth: 2,\n  defaultTool: 'ellipse',\n  hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],\n  translation: {\n    name: 'en',\n    strings: {\n      lineColor: 'Line',\n      fillColor: 'Fill',\n      lineWidth: 'Size',\n      textColor: 'Color',\n      fontSize: 'Size',\n      fontStyle: 'Style'\n    }\n  }\n};\n\nexport default class PainterroInstance {\n  constructor(element, openmct) {\n    this.elementId = element.id;\n    this.isSave = false;\n    this.painterroInstance = undefined;\n    this.saveCallback = undefined;\n    this.openmct = openmct;\n  }\n\n  dismiss() {\n    this.isSave = false;\n    this.painterroInstance.save();\n  }\n\n  initialize() {\n    this.config = Object.assign({}, DEFAULT_CONFIG);\n\n    this.config.id = this.elementId;\n    this.config.saveHandler = this.saveHandler.bind(this);\n\n    this.painterro = Painterro(this.config);\n  }\n\n  save(callback) {\n    this.saveCallback = callback;\n    this.isSave = true;\n    this.painterroInstance.save();\n  }\n\n  saveHandler(image, done) {\n    if (this.isSave) {\n      const url = image.asBlob();\n\n      const reader = new window.FileReader();\n      reader.readAsDataURL(url);\n      reader.onloadend = async () => {\n        const fullSizeImageURL = reader.result;\n        const thumbnailURL = await getThumbnailURLFromImageUrl(fullSizeImageURL);\n        const snapshotObject = {\n          fullSizeImage: {\n            src: fullSizeImageURL,\n            type: url.type,\n            size: url.size,\n            modified: this.openmct.time.now()\n          },\n          thumbnailImage: {\n            src: thumbnailURL,\n            modified: this.openmct.time.now()\n          }\n        };\n\n        this.saveCallback(snapshotObject);\n\n        done(true);\n      };\n    } else {\n      done(true);\n    }\n  }\n\n  show(src) {\n    this.painterroInstance = this.painterro.show(src);\n  }\n}\n"
  },
  {
    "path": "src/plugins/notebook/utils/removeDialog.js",
    "content": "export default class RemoveDialog {\n  constructor(openmct, options) {\n    this.name = options.name;\n    this.openmct = openmct;\n\n    this.callback = options.callback;\n    this.cssClass = options.cssClass || 'icon-trash';\n    this.description = options.description || 'Remove action dialog';\n    this.iconClass = 'error';\n    this.key = 'remove';\n    this.message =\n      options.message ||\n      `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`;\n  }\n\n  show() {\n    const dialog = this.openmct.overlays.dialog({\n      iconClass: this.iconClass,\n      message: this.message,\n      buttons: [\n        {\n          label: 'Ok',\n          callback: () => {\n            this.callback(true);\n            dialog.dismiss();\n          }\n        },\n        {\n          label: 'Cancel',\n          callback: () => {\n            this.callback(false);\n            dialog.dismiss();\n          }\n        }\n      ]\n    });\n  }\n}\n"
  },
  {
    "path": "src/plugins/notificationIndicator/components/NotificationIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"notifications.length > 0 || showNotificationsOverlay\"\n    class=\"c-indicator c-indicator--clickable icon-bell\"\n    :class=\"[severityClass]\"\n  >\n    <span class=\"c-indicator__label\">\n      <button\n        :aria-label=\"'Review ' + notificationsCountMessage(notifications.length)\"\n        @click=\"toggleNotificationsList(true)\"\n      >\n        {{ notificationsCountMessage(notifications.length) }}\n      </button>\n      <button aria-label=\"Clear all notifications\" @click=\"dismissAllNotifications()\">\n        Clear All\n      </button>\n    </span>\n    <span class=\"c-indicator__count\">{{ notifications.length }}</span>\n\n    <NotificationsList\n      v-if=\"showNotificationsOverlay\"\n      :notifications=\"notifications\"\n      @close=\"toggleNotificationsList\"\n      @clear-all=\"dismissAllNotifications\"\n    />\n  </div>\n</template>\n\n<script>\nimport NotificationsList from './NotificationsList.vue';\n\nexport default {\n  components: {\n    NotificationsList\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      notifications: this.openmct.notifications.notifications,\n      highest: this.openmct.notifications.highest,\n      showNotificationsOverlay: false\n    };\n  },\n  computed: {\n    severityClass() {\n      return `s-status-${this.highest.severity}`;\n    }\n  },\n  mounted() {\n    this.openmct.notifications.on('notification', this.updateNotifications);\n    this.openmct.notifications.on('dismiss-all', this.updateNotifications);\n  },\n  unmounted() {\n    this.openmct.notifications.off('notification', this.updateNotifications);\n    this.openmct.notifications.off('dismiss-all', this.updateNotifications);\n  },\n  methods: {\n    dismissAllNotifications() {\n      this.openmct.notifications.dismissAllNotifications();\n    },\n    toggleNotificationsList(flag) {\n      this.showNotificationsOverlay = flag;\n    },\n    updateNotifications() {\n      this.notifications = [...this.openmct.notifications.notifications];\n      this.highest = this.openmct.notifications.highest;\n    },\n    notificationsCountMessage(count) {\n      if (count > 1) {\n        return `${count} Notifications`;\n      } else {\n        return `${count} Notification`;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notificationIndicator/components/NotificationMessage.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-message\" role=\"listitem\" :class=\"'message-severity-' + notification.model.severity\">\n    <div class=\"c-ne__time-and-content\">\n      <div class=\"c-ne__time\">\n        <span>{{ notification.model.timestamp }}</span>\n      </div>\n      <div class=\"c-ne__content\">\n        <div class=\"w-message-contents\">\n          <div class=\"c-message__top-bar\">\n            <div class=\"c-message__title\">{{ notification.model.message }}</div>\n          </div>\n          <div class=\"message-body\">\n            <ProgressBar v-if=\"isProgressNotification\" :model=\"progressObject\" />\n          </div>\n        </div>\n      </div>\n      <button\n        :aria-label=\"'Dismiss notification of ' + notification.model.message\"\n        class=\"c-click-icon c-overlay__close-button icon-x\"\n        @click=\"dismiss()\"\n      ></button>\n      <div class=\"c-overlay__button-bar\">\n        <button\n          v-for=\"(dialogOption, index) in notification.model.options\"\n          :key=\"index\"\n          class=\"c-button\"\n          @click=\"dialogOption.callback()\"\n        >\n          {{ dialogOption.label }}\n        </button>\n        <button\n          v-if=\"notification.model.primaryOption\"\n          class=\"c-button c-button--major\"\n          @click=\"notification.model.primaryOption.callback()\"\n        >\n          {{ notification.model.primaryOption.label }}\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ProgressBar from '../../../ui/components/ProgressBar.vue';\n\nexport default {\n  components: {\n    ProgressBar\n  },\n  props: {\n    notification: {\n      type: Object,\n      required: true\n    },\n    closeOverlay: {\n      type: Function,\n      required: true\n    },\n    notificationsCount: {\n      type: Number,\n      required: true\n    }\n  },\n  data() {\n    return {\n      isProgressNotification: false,\n      progressPerc: this.notification.model.progressPerc,\n      progressText: this.notification.model.progressText\n    };\n  },\n  computed: {\n    progressObject() {\n      return {\n        progressPerc: this.progressPerc,\n        progressText: this.progressText\n      };\n    }\n  },\n  mounted() {\n    if (this.notification.model.progressPerc) {\n      this.isProgressNotification = true;\n      this.notification.on('progress', this.updateProgressBar);\n    }\n  },\n  methods: {\n    updateProgressBar(progressPerc, progressText) {\n      this.progressPerc = progressPerc;\n      this.progressText = progressText;\n    },\n    dismiss() {\n      this.notification.dismiss();\n      if (this.notificationsCount === 1) {\n        this.closeOverlay();\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notificationIndicator/components/NotificationsList.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"t-message-list c-overlay__contents\">\n    <div class=\"c-overlay__top-bar\">\n      <div class=\"c-overlay__dialog-title\">Notifications</div>\n      <div class=\"c-overlay__dialog-hint\">\n        {{ notificationsCountDisplayMessage(notifications.length) }}\n      </div>\n    </div>\n    <div role=\"list\" class=\"w-messages c-overlay__messages\">\n      <NotificationMessage\n        v-for=\"(notification, notificationIndex) in notifications\"\n        :key=\"notificationIndex\"\n        :close-overlay=\"closeOverlay\"\n        :notification=\"notification\"\n        :notifications-count=\"notifications.length\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport NotificationMessage from './NotificationMessage.vue';\n\nexport default {\n  components: {\n    NotificationMessage\n  },\n  inject: ['openmct'],\n  props: {\n    notifications: {\n      type: Array,\n      required: true\n    }\n  },\n  emits: ['close', 'clear-all'],\n  data() {\n    return {};\n  },\n  mounted() {\n    this.openOverlay();\n  },\n  methods: {\n    openOverlay() {\n      this.overlay = this.openmct.overlays.overlay({\n        element: this.$el,\n        size: 'large',\n        dismissible: true,\n        buttons: [\n          {\n            label: 'Clear All Notifications',\n            emphasis: true,\n            callback: () => {\n              this.$emit('clear-all');\n              this.overlay.dismiss();\n            }\n          }\n        ],\n        onDestroy: () => {\n          this.$emit('close', false);\n        }\n      });\n    },\n    closeOverlay() {\n      this.overlay.dismiss();\n    },\n    notificationsCountDisplayMessage(count) {\n      if (count > 1 || count === 0) {\n        return `Displaying ${count} notifications`;\n      } else {\n        return `Displaying ${count} notification`;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/notificationIndicator/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport NotificationIndicator from './components/NotificationIndicator.vue';\n\nexport default function plugin() {\n  return function install(openmct) {\n    let indicator = {\n      key: 'notifications-indicator',\n      vueComponent: NotificationIndicator,\n      priority: openmct.priority.DEFAULT\n    };\n    openmct.indicators.add(indicator);\n  };\n}\n"
  },
  {
    "path": "src/plugins/notificationIndicator/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport NotificationIndicatorPlugin from './plugin.js';\n\ndescribe('the plugin', () => {\n  let notificationIndicatorPlugin;\n  let openmct;\n  let indicatorElement;\n  let parentElement;\n  let mockMessages = ['error', 'test', 'notifications'];\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    notificationIndicatorPlugin = new NotificationIndicatorPlugin();\n    openmct.install(notificationIndicatorPlugin);\n\n    parentElement = document.createElement('div');\n\n    openmct.on('start', () => {\n      mockMessages.forEach((message) => {\n        openmct.notifications.error(message);\n      });\n      done();\n    });\n\n    openmct.start();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the indicator plugin element', () => {\n    beforeEach(() => {\n      parentElement.append(indicatorElement);\n\n      return nextTick();\n    });\n\n    it('notifies the user of the number of notifications', () => {\n      let notificationCountElement = document.querySelector('.c-indicator__count');\n\n      expect(notificationCountElement.innerText).toEqual('1');\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/objectMigration/Migrations.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { v4 as uuid } from 'uuid';\n\nexport default function Migrations(openmct) {\n  function getColumnNameKeyMap(domainObject) {\n    let composition = openmct.composition.get(domainObject);\n    if (composition) {\n      return composition.load().then((composees) => {\n        return composees.reduce((nameKeyMap, composee) => {\n          let metadata = openmct.telemetry.getMetadata(composee);\n          if (metadata !== undefined) {\n            metadata.values().forEach((value) => {\n              nameKeyMap[value.name] = value.key;\n            });\n          }\n\n          return nameKeyMap;\n        }, {});\n      });\n    } else {\n      return Promise.resolve([]);\n    }\n  }\n\n  function isTelemetry(domainObject) {\n    if (\n      openmct.telemetry.isTelemetryObject(domainObject) &&\n      domainObject.type !== 'summary-widget' &&\n      domainObject.type !== 'example.imagery'\n    ) {\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  function migrateDisplayLayout(domainObject, childObjects) {\n    const DEFAULT_GRID_SIZE = [32, 32];\n    let migratedObject = Object.assign({}, domainObject);\n    let panels = migratedObject.configuration.layout.panels;\n    let items = [];\n\n    Object.keys(panels).forEach((key) => {\n      let panel = panels[key];\n      let childDomainObject = childObjects[key];\n      let identifier = undefined;\n\n      if (isTelemetry(childDomainObject)) {\n        // If object is a telemetry point, convert it to a plot and\n        // replace the object in migratedObject composition with the plot.\n        identifier = {\n          key: uuid(),\n          namespace: migratedObject.identifier.namespace\n        };\n        let plotObject = {\n          identifier: identifier,\n          location: childDomainObject.location,\n          name: childDomainObject.name,\n          type: 'telemetry.plot.overlay'\n        };\n        let plotType = openmct.types.get('telemetry.plot.overlay');\n        plotType.definition.initialize(plotObject);\n        plotObject.composition.push(childDomainObject.identifier);\n        openmct.objects.mutate(plotObject, 'persisted', Date.now());\n\n        let keyString = openmct.objects.makeKeyString(childDomainObject.identifier);\n        let clonedComposition = Object.assign([], migratedObject.composition);\n        clonedComposition.forEach((objIdentifier, index) => {\n          if (openmct.objects.makeKeyString(objIdentifier) === keyString) {\n            migratedObject.composition[index] = plotObject.identifier;\n          }\n        });\n      }\n\n      items.push({\n        width: panel.dimensions[0],\n        height: panel.dimensions[1],\n        x: panel.position[0],\n        y: panel.position[1],\n        identifier: identifier || childDomainObject.identifier,\n        id: uuid(),\n        type: 'subobject-view',\n        hasFrame: panel.hasFrame\n      });\n    });\n\n    migratedObject.configuration.items = items;\n    migratedObject.configuration.layoutGrid = migratedObject.layoutGrid || DEFAULT_GRID_SIZE;\n    delete migratedObject.layoutGrid;\n    delete migratedObject.configuration.layout;\n\n    return migratedObject;\n  }\n\n  function migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize) {\n    const DEFAULT_STROKE = 'transparent';\n    const DEFAULT_SIZE = '13px';\n    const DEFAULT_COLOR = '';\n    const DEFAULT_FILL = '';\n    let items = [];\n\n    elements.forEach((element) => {\n      let item = {\n        x: element.x,\n        y: element.y,\n        width: element.width,\n        height: element.height,\n        id: uuid()\n      };\n\n      if (!element.useGrid) {\n        item.x = Math.round(item.x / gridSize[0]);\n        item.y = Math.round(item.y / gridSize[1]);\n        item.width = Math.round(item.width / gridSize[0]);\n        item.height = Math.round(item.height / gridSize[1]);\n      }\n\n      if (element.type === 'fixed.telemetry') {\n        item.type = 'telemetry-view';\n        item.stroke = element.stroke || DEFAULT_STROKE;\n        item.fill = element.fill || DEFAULT_FILL;\n        item.color = element.color || DEFAULT_COLOR;\n        item.size = element.size || DEFAULT_SIZE;\n        item.identifier = telemetryObjects[element.id].identifier;\n        item.displayMode = element.titled ? 'all' : 'value';\n        item.value = openmct.telemetry\n          .getMetadata(telemetryObjects[element.id])\n          .getDefaultDisplayValue()?.key;\n      } else if (element.type === 'fixed.box') {\n        item.type = 'box-view';\n        item.stroke = element.stroke || DEFAULT_STROKE;\n        item.fill = element.fill || DEFAULT_FILL;\n      } else if (element.type === 'fixed.line') {\n        item.type = 'line-view';\n        item.x2 = element.x2;\n        item.y2 = element.y2;\n        item.stroke = element.stroke || DEFAULT_STROKE;\n        delete item.height;\n        delete item.width;\n      } else if (element.type === 'fixed.text') {\n        item.type = 'text-view';\n        item.text = element.text;\n        item.stroke = element.stroke || DEFAULT_STROKE;\n        item.fill = element.fill || DEFAULT_FILL;\n        item.color = element.color || DEFAULT_COLOR;\n        item.size = element.size || DEFAULT_SIZE;\n      } else if (element.type === 'fixed.image') {\n        item.type = 'image-view';\n        item.url = element.url;\n        item.stroke = element.stroke || DEFAULT_STROKE;\n      }\n\n      items.push(item);\n    });\n\n    return items;\n  }\n\n  return [\n    {\n      check(domainObject) {\n        return (\n          domainObject?.type === 'layout' &&\n          domainObject.configuration &&\n          domainObject.configuration.layout\n        );\n      },\n      migrate(domainObject) {\n        let childObjects = {};\n        let promises = Object.keys(domainObject.configuration.layout.panels).map((key) => {\n          return openmct.objects.get(key).then((object) => {\n            childObjects[key] = object;\n          });\n        });\n\n        return Promise.all(promises).then(function () {\n          return migrateDisplayLayout(domainObject, childObjects);\n        });\n      }\n    },\n    {\n      check(domainObject) {\n        return (\n          domainObject?.type === 'telemetry.fixed' &&\n          domainObject.configuration &&\n          domainObject.configuration['fixed-display']\n        );\n      },\n      migrate(domainObject) {\n        const DEFAULT_GRID_SIZE = [64, 16];\n        let newLayoutObject = {\n          identifier: domainObject.identifier,\n          location: domainObject.location,\n          name: domainObject.name,\n          type: 'layout'\n        };\n        let gridSize = domainObject.layoutGrid || DEFAULT_GRID_SIZE;\n        let layoutType = openmct.types.get('layout');\n        layoutType.definition.initialize(newLayoutObject);\n        newLayoutObject.composition = domainObject.composition;\n        newLayoutObject.configuration.layoutGrid = gridSize;\n\n        let elements = domainObject.configuration['fixed-display'].elements;\n        let telemetryObjects = {};\n        let promises = elements.map((element) => {\n          if (element.id) {\n            return openmct.objects.get(element.id).then((object) => {\n              telemetryObjects[element.id] = object;\n            });\n          } else {\n            return Promise.resolve(false);\n          }\n        });\n\n        return Promise.all(promises).then(function () {\n          newLayoutObject.configuration.items = migrateFixedPositionConfiguration(\n            elements,\n            telemetryObjects,\n            gridSize\n          );\n\n          return newLayoutObject;\n        });\n      }\n    },\n    {\n      check(domainObject) {\n        return (\n          domainObject?.type === 'table' &&\n          domainObject.configuration &&\n          domainObject.configuration.table\n        );\n      },\n      migrate(domainObject) {\n        let currentTableConfiguration = domainObject.configuration.table || {};\n        let currentColumnConfiguration = currentTableConfiguration.columns || {};\n\n        return getColumnNameKeyMap(domainObject).then((nameKeyMap) => {\n          let hiddenColumns = Object.keys(currentColumnConfiguration)\n            .filter((columnName) => {\n              return currentColumnConfiguration[columnName] === false;\n            })\n            .reduce((hiddenColumnsMap, hiddenColumnName) => {\n              let key = nameKeyMap[hiddenColumnName];\n              hiddenColumnsMap[key] = true;\n\n              return hiddenColumnsMap;\n            }, {});\n\n          domainObject.configuration.hiddenColumns = hiddenColumns;\n          delete domainObject.configuration.table;\n\n          return domainObject;\n        });\n      }\n    }\n  ];\n}\n"
  },
  {
    "path": "src/plugins/objectMigration/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Migrations from './Migrations.js';\n\nexport default function () {\n  return function (openmct) {\n    let migrations = Migrations(openmct);\n\n    function needsMigration(domainObject) {\n      return migrations.some((m) => m.check(domainObject));\n    }\n\n    function migrateObject(domainObject) {\n      return migrations.filter((m) => m.check(domainObject))[0].migrate(domainObject);\n    }\n\n    let wrappedFunction = openmct.objects.get;\n    openmct.objects.get = function migrate() {\n      return wrappedFunction.apply(openmct.objects, [...arguments]).then(function (object) {\n        if (needsMigration(object)) {\n          migrateObject(object).then((newObject) => {\n            openmct.objects.mutate(newObject, 'persisted', Date.now());\n\n            return newObject;\n          });\n        }\n\n        return object;\n      });\n    };\n  };\n}\n"
  },
  {
    "path": "src/plugins/openInNewTabAction/openInNewTabAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { objectPathToUrl } from '/src/tools/url.js';\n\nconst NEW_TAB_ACTION_KEY = 'newTab';\n\nclass OpenInNewTab {\n  constructor(openmct) {\n    this.name = 'Open In New Tab';\n    this.key = NEW_TAB_ACTION_KEY;\n    this.description = 'Open in a new browser tab';\n    this.group = 'windowing';\n    this.priority = 10;\n    this.cssClass = 'icon-new-window';\n\n    this._openmct = openmct;\n  }\n\n  /**\n   * Invokes the \"Open in New Tab\" action. This will open the object in a new\n   * browser tab. The URL for the new tab is determined by the current object\n   * path and any custom time bounds.\n   *\n   * @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path\n   * @param {ViewContext} _view The view context for the object being opened (unused)\n   * @param {Object<string, string | number>} customUrlParams Provides the ability to override\n   * the global time conductor bounds. It is an object with the following key/value pairs:\n   * ```\n   * {\n   *  'tc.start': <number>,\n   *  'tc.end': <number>,\n   *  'tc.mode': 'fixed' | 'local' | <string>\n   * }\n   * ```\n   */\n  invoke(objectPath, _view, customUrlParams) {\n    const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);\n    window.open(url, undefined, 'noopener');\n  }\n}\n\nexport { NEW_TAB_ACTION_KEY };\n\nexport default OpenInNewTab;\n"
  },
  {
    "path": "src/plugins/openInNewTabAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport OpenInNewTabAction from './openInNewTabAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new OpenInNewTabAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/openInNewTabAction/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';\n\ndescribe('the plugin', () => {\n  let openmct;\n  let openInNewTabAction;\n  let mockObjectPath;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    openInNewTabAction = openmct.actions._allActions.newTab;\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('installs the open in new tab action', () => {\n    expect(openInNewTabAction).toBeDefined();\n  });\n\n  describe('when invoked', () => {\n    beforeEach(async () => {\n      mockObjectPath = [\n        {\n          name: 'mock folder',\n          type: 'folder',\n          identifier: {\n            key: 'mock-folder',\n            namespace: ''\n          }\n        }\n      ];\n      spyOn(openmct.objects, 'get').and.returnValue(\n        Promise.resolve({\n          identifier: {\n            namespace: '',\n            key: 'test'\n          }\n        })\n      );\n      spyOnBuiltins(['open']);\n      await openInNewTabAction.invoke(mockObjectPath);\n    });\n\n    it('it opens in a new tab', () => {\n      expect(window.open).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/operatorStatus/AbstractStatusIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport raf from '@/utils/raf';\n\nexport default class AbstractStatusIndicator {\n  #popupComponent;\n  #indicator;\n  #configuration;\n\n  /**\n   * @param {*} openmct the Open MCT API (proper typescript doc to come)\n   * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI\n   */\n  constructor(openmct, configuration) {\n    this.openmct = openmct;\n    this.#configuration = configuration;\n\n    this.showPopup = this.showPopup.bind(this);\n    this.clearPopup = this.clearPopup.bind(this);\n    this.positionBox = this.positionBox.bind(this);\n    this.positionBox = raf(this.positionBox);\n\n    this.#indicator = this.createIndicator();\n    this.#popupComponent = this.createPopupComponent();\n  }\n\n  install() {\n    this.openmct.indicators.add(this.#indicator);\n  }\n\n  showPopup() {\n    const popupElement = this.getPopupElement();\n\n    document.body.appendChild(popupElement.$el);\n    //Use capture so we don't trigger immediately on the same iteration of the event loop\n    document.addEventListener('click', this.clearPopup, {\n      capture: true\n    });\n\n    this.positionBox();\n\n    window.addEventListener('resize', this.positionBox);\n  }\n\n  positionBox() {\n    const popupElement = this.getPopupElement();\n    const indicator = this.getIndicator();\n\n    let indicatorBox = indicator.element.getBoundingClientRect();\n    popupElement.positionX = indicatorBox.left;\n    popupElement.positionY = indicatorBox.bottom;\n\n    const popupRight = popupElement.positionX + popupElement.$el.clientWidth;\n    const offsetLeft = Math.min(window.innerWidth - popupRight, 0);\n    popupElement.positionX = popupElement.positionX + offsetLeft;\n  }\n\n  clearPopup(clickAwayEvent) {\n    const popupElement = this.getPopupElement();\n\n    if (!popupElement.$el.contains(clickAwayEvent.target)) {\n      popupElement.$el.remove();\n      document.removeEventListener('click', this.clearPopup);\n      window.removeEventListener('resize', this.positionBox);\n    }\n  }\n\n  createPopupComponent() {\n    throw new Error('Must override createPopupElement method');\n  }\n\n  getPopupElement() {\n    return this.#popupComponent;\n  }\n\n  createIndicator() {\n    throw new Error('Must override createIndicator method');\n  }\n\n  getIndicator() {\n    return this.#indicator;\n  }\n\n  getConfiguration() {\n    return this.#configuration;\n  }\n}\n"
  },
  {
    "path": "src/plugins/operatorStatus/operator-status.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n$statusCountWidth: 30px;\n\n.c-status-poll-panel {\n  @include menuOuter();\n  display: flex;\n  flex-direction: column;\n  padding: $interiorMarginLg;\n  min-width: 350px;\n  max-width: 35%;\n\n  > * + * {\n    margin-top: $interiorMarginLg;\n  }\n\n  *:before {\n    font-size: 0.8em;\n    margin-right: $interiorMarginSm;\n  }\n\n  &__section {\n    display: flex;\n    align-items: center;\n    flex-direction: row;\n\n    > * + * {\n      margin-left: $interiorMarginLg;\n    }\n  }\n\n  &__top {\n    text-transform: uppercase;\n  }\n\n  &__user-role,\n  &__updated {\n    opacity: 50%;\n  }\n\n  &__updated {\n    flex: 1 1 auto;\n    text-align: right;\n  }\n\n  &__poll-question {\n    background: $colorBodyFg;\n    color: $colorBodyBg;\n    border-radius: $controlCr;\n    font-weight: bold;\n    padding: $interiorMarginSm $interiorMargin;\n\n    .c-status-poll-panel--admin & {\n      background: rgba($colorBodyFg, 0.1);\n      color: $colorBodyFg;\n    }\n  }\n\n  /****** Admin interface */\n  &__content {\n    $m: $interiorMargin;\n    display: grid;\n    grid-template-columns: max-content 1fr;\n    grid-column-gap: $m;\n    grid-row-gap: $m;\n\n    [class*='__label'] {\n      padding: 3px 0;\n    }\n\n    [class*='__label'] {\n      padding: 3px 0;\n    }\n\n    [class*='__poll-table'] {\n      grid-column: span 2;\n    }\n\n    [class*='new-question'] {\n      align-items: center;\n      display: flex;\n      flex-direction: row;\n      > * + * {\n        margin-left: $interiorMargin;\n      }\n\n      input {\n        flex: 1 1 auto;\n        height: $btnStdH;\n      }\n\n      button {\n        flex: 0 0 auto;\n      }\n    }\n  }\n}\n\n.c-status-poll-report {\n  display: flex;\n  flex-direction: row;\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n\n  &__count {\n    background: rgba($colorBodyFg, 0.2);\n    border-radius: $controlCr;\n    display: flex;\n    flex-direction: row;\n    font-size: 1.25em;\n    align-items: center;\n    padding: $interiorMarginSm $interiorMarginLg;\n\n    &-type {\n      line-height: 1em;\n      opacity: 0.6;\n    }\n  }\n  &__actions {\n    display: flex;\n    flex: auto;\n    flex-direction: row;\n    justify-content: flex-end;\n  }\n}\n\n.c-indicator {\n  &:before {\n    // Indicator icon\n    color: $colorKey;\n  }\n\n  &--operator-status {\n    cursor: pointer;\n    max-width: 150px;\n\n    @include hover() {\n      background: $colorIndicatorBgHov;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div :style=\"position\" class=\"c-status-poll-panel c-status-poll-panel--operator\" @click.stop>\n    <div class=\"c-status-poll-panel__section c-status-poll-panel__top\">\n      <div class=\"c-status-poll-panel__title\">Status Poll</div>\n      <div class=\"c-status-poll-panel__user-role icon-person\">{{ role }}</div>\n      <div class=\"c-status-poll-panel__updated\">{{ pollQuestionUpdated }}</div>\n    </div>\n\n    <div class=\"c-status-poll-panel__section c-status-poll-panel__poll-question\">\n      {{ currentPollQuestion }}\n    </div>\n\n    <div class=\"c-status-poll-panel__section c-status-poll-panel__bottom\">\n      <div class=\"c-status-poll-panel__set-status-label\">My status:</div>\n      <select v-model=\"selectedStatus\" name=\"setStatus\" @change=\"changeStatus\">\n        <option v-for=\"status in allStatuses\" :key=\"status.key\" :value=\"status.key\">\n          {{ status.label }}\n        </option>\n      </select>\n    </div>\n  </div>\n</template>\n\n<script>\nconst DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';\nexport default {\n  inject: ['openmct', 'indicator', 'configuration'],\n  props: {\n    positionX: {\n      type: Number,\n      required: true\n    },\n    positionY: {\n      type: Number,\n      required: true\n    }\n  },\n  data() {\n    return {\n      role: '--',\n      pollQuestionUpdated: '--',\n      currentPollQuestion: DEFAULT_POLL_QUESTION,\n      selectedStatus: undefined,\n      allStatuses: []\n    };\n  },\n  computed: {\n    position() {\n      return {\n        position: 'absolute',\n        left: `${this.positionX}px`,\n        top: `${this.positionY}px`\n      };\n    },\n    canProvideStatusForRole() {\n      return this.openmct.user.canProvideStatusForRole(this.role);\n    }\n  },\n  beforeUnmount() {\n    this.openmct.user.status.off('statusChange', this.setStatus);\n    this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);\n    this.openmct.user.off('roleChanged', this.fetchMyStatus);\n  },\n  async mounted() {\n    this.unsubscribe = [];\n    await this.fetchUser();\n    this.fetchPossibleStatusesForUser();\n    this.fetchCurrentPoll();\n    await this.fetchMyStatus();\n    this.subscribeToMyStatus();\n    this.subscribeToPollQuestion();\n    this.subscribeToRoleChange();\n  },\n  methods: {\n    async fetchUser() {\n      this.user = await this.openmct.user.getCurrentUser();\n    },\n    async fetchCurrentPoll() {\n      const pollQuestion = await this.openmct.user.status.getPollQuestion();\n      if (pollQuestion !== undefined) {\n        this.setPollQuestion(pollQuestion);\n      }\n    },\n    async fetchPossibleStatusesForUser() {\n      this.allStatuses = await this.openmct.user.status.getPossibleStatuses();\n    },\n    setPollQuestion(pollQuestion) {\n      this.currentPollQuestion = pollQuestion.question;\n      this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();\n\n      this.indicator.text(pollQuestion?.question || '');\n    },\n    async fetchMyStatus() {\n      // hide indicator for observer\n      const isStatusCapable = await this.openmct.user.canProvideStatusForRole();\n      if (!isStatusCapable) {\n        this.indicator.text('');\n        this.indicator.statusClass('hidden');\n\n        return;\n      }\n\n      const activeRole = await this.openmct.user.getActiveRole();\n      if (!activeRole) {\n        return;\n      }\n\n      this.role = activeRole;\n      const status = await this.openmct.user.status.getStatusForRole(activeRole);\n      if (status !== undefined) {\n        this.setStatus({ role: this.role, status });\n      }\n    },\n    subscribeToMyStatus() {\n      this.openmct.user.status.on('statusChange', this.setStatus);\n    },\n    subscribeToPollQuestion() {\n      this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);\n    },\n    subscribeToRoleChange() {\n      this.openmct.user.on('roleChanged', this.fetchMyStatus);\n    },\n    setStatus({ role, status }) {\n      if (role !== this.role) {\n        // not my role\n        return;\n      }\n      status = this.applyStyling(status);\n      this.selectedStatus = status.key;\n      this.indicator.iconClass(status.iconClassPoll);\n      this.indicator.statusClass(status.statusClass);\n      if (this.isDefaultStatus(status)) {\n        this.indicator.text(this.currentPollQuestion);\n      } else {\n        this.indicator.text(status.label);\n      }\n    },\n    isDefaultStatus(status) {\n      return status.key === this.allStatuses[0].key;\n    },\n    findStatusByKey(statusKey) {\n      return this.allStatuses.find((possibleMatch) => possibleMatch.key === statusKey);\n    },\n    async changeStatus() {\n      if (!this.openmct.user.canProvideStatusForRole()) {\n        this.openmct.notifications.error('Selected role is ineligible to provide operator status');\n\n        return;\n      }\n\n      if (this.selectedStatus !== undefined) {\n        const statusObject = this.findStatusByKey(this.selectedStatus);\n\n        const result = await this.openmct.user.status.setStatusForRole(statusObject);\n        if (result === true) {\n          this.openmct.notifications.info('Successfully set operator status');\n        } else {\n          this.openmct.notifications.error('Unable to set operator status');\n        }\n      }\n    },\n    applyStyling(status) {\n      const stylesForStatus = this.configuration?.statusStyles?.[status.label];\n\n      if (stylesForStatus !== undefined) {\n        return {\n          ...status,\n          ...stylesForStatus\n        };\n      } else {\n        return status;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport PRIORITIES from '../../../api/priority/PriorityAPI.js';\nimport AbstractStatusIndicator from '../AbstractStatusIndicator.js';\nimport OperatorStatusComponent from './OperatorStatus.vue';\n\nexport default class OperatorStatusIndicator extends AbstractStatusIndicator {\n  createPopupComponent() {\n    const indicator = this.getIndicator();\n    const { vNode } = mount(\n      {\n        components: {\n          OperatorStatus: OperatorStatusComponent\n        },\n        provide: {\n          openmct: this.openmct,\n          indicator: indicator,\n          configuration: this.getConfiguration()\n        },\n        data() {\n          return {\n            positionX: 0,\n            positionY: 0\n          };\n        },\n        template: '<operator-status :positionX=\"positionX\" :positionY=\"positionY\" />'\n      },\n      {\n        app: this.openmct.app\n      }\n    );\n\n    return vNode.componentInstance;\n  }\n\n  createIndicator() {\n    const operatorIndicator = this.openmct.indicators.simpleIndicator();\n\n    operatorIndicator.text('My Operator Status');\n    operatorIndicator.description('Set my operator status');\n    operatorIndicator.iconClass('icon-status-poll-question-mark');\n    operatorIndicator.element.classList.add('c-indicator--operator-status');\n    operatorIndicator.element.classList.add('no-minify');\n    operatorIndicator.on('click', this.showPopup);\n    operatorIndicator.priority = PRIORITIES.HIGHEST;\n\n    return operatorIndicator;\n  }\n}\n"
  },
  {
    "path": "src/plugins/operatorStatus/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport OperatorStatusIndicator from './operatorStatus/OperatorStatusIndicator.js';\nimport PollQuestionIndicator from './pollQuestion/PollQuestionIndicator.js';\n\n/**\n * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration\n * @returns {function} The plugin install function\n */\nexport default function operatorStatusPlugin(configuration) {\n  return function install(openmct) {\n    if (openmct.user.hasProvider()) {\n      const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);\n      operatorStatusIndicator.install();\n\n      openmct.user.status.canSetPollQuestion().then((canSetPollQuestion) => {\n        if (canSetPollQuestion) {\n          const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration);\n\n          pollQuestionIndicator.install();\n        }\n      });\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/operatorStatus/pollQuestion/PollQuestion.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div :style=\"position\" class=\"c-status-poll-panel c-status-poll-panel--admin\" @click.stop=\"noop\">\n    <div class=\"c-status-poll-panel__section c-status-poll-panel__top\">\n      <div class=\"c-status-poll-panel__title\">Manage Status Poll</div>\n      <div class=\"c-status-poll-panel__updated\">Last updated: {{ pollQuestionUpdated }}</div>\n    </div>\n\n    <div class=\"c-status-poll__section c-status-poll-panel__content c-spq\">\n      <!-- Grid layout -->\n      <div class=\"c-spq__label\">Current poll:</div>\n      <div class=\"c-spq__value c-status-poll-panel__poll-question\">{{ currentPollQuestion }}</div>\n\n      <template v-if=\"statusCountViewModel.length > 0\">\n        <div class=\"c-spq__label\">Reporting:</div>\n        <div class=\"c-spq__value c-status-poll-panel__poll-reporting c-status-poll-report\">\n          <div\n            v-for=\"entry in statusCountViewModel\"\n            :key=\"entry.status.key\"\n            :aria-label=\"entry.status.label\"\n            :title=\"entry.status.label\"\n            class=\"c-status-poll-report__count\"\n            :style=\"[\n              {\n                background: entry.status.statusBgColor,\n                color: entry.status.statusFgColor\n              }\n            ]\"\n          >\n            <div class=\"c-status-poll-report__count-type\" :class=\"entry.status.iconClass\"></div>\n            <div class=\"c-status-poll-report__count-value\">\n              {{ entry.roleCount }}\n            </div>\n          </div>\n          <div class=\"c-status-poll-report__actions\">\n            <button\n              class=\"c-button\"\n              title=\"Clear the previous poll question\"\n              @click=\"clearPollQuestion\"\n            >\n              Clear Poll\n            </button>\n          </div>\n        </div>\n      </template>\n\n      <div class=\"c-spq__label\">New poll:</div>\n      <div class=\"c-spq__value c-status-poll-panel__poll-new-question\">\n        <input v-model=\"newPollQuestion\" type=\"text\" name=\"newPollQuestion\" />\n        <button\n          class=\"c-button\"\n          title=\"Publish a new poll question and reset previous responses\"\n          @click=\"updatePollQuestion\"\n        >\n          Update\n        </button>\n      </div>\n      <div class=\"c-table c-spq__poll-table\">\n        <table class=\"c-table__body\">\n          <thead class=\"c-table__header\">\n            <tr>\n              <th>Position</th>\n              <th>Status</th>\n              <th>Age</th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr v-for=\"statusForRole in statusesForRolesViewModel\" :key=\"statusForRole.key\">\n              <td>\n                {{ statusForRole.role }}\n              </td>\n              <td\n                :style=\"{\n                  background: statusForRole.status.statusBgColor,\n                  color: statusForRole.status.statusFgColor\n                }\"\n              >\n                {{ statusForRole.status.label }}\n              </td>\n              <td>\n                {{ statusForRole.age }}\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nexport default {\n  inject: ['openmct', 'indicator', 'configuration'],\n  props: {\n    positionX: {\n      type: Number,\n      required: true\n    },\n    positionY: {\n      type: Number,\n      required: true\n    }\n  },\n  data() {\n    return {\n      pollQuestionUpdated: '--',\n      pollQuestionTimestamp: undefined,\n      currentPollQuestion: '--',\n      newPollQuestion: undefined,\n      statusCountViewModel: [],\n      statusesForRolesViewModel: []\n    };\n  },\n  computed: {\n    position() {\n      return {\n        position: 'absolute',\n        left: `${this.positionX}px`,\n        top: `${this.positionY}px`\n      };\n    }\n  },\n  mounted() {\n    this.fetchCurrentPoll();\n    this.subscribeToPollQuestion();\n    this.fetchStatusSummary();\n    this.openmct.user.status.on('statusChange', this.fetchStatusSummary);\n  },\n  beforeUnmount() {\n    this.openmct.user.status.off('statusChange', this.fetchStatusSummary);\n    this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);\n  },\n  created() {\n    this.fetchStatusSummary = _.debounce(this.fetchStatusSummary);\n  },\n  methods: {\n    async fetchCurrentPoll() {\n      const pollQuestion = await this.openmct.user.status.getPollQuestion();\n      if (pollQuestion !== undefined) {\n        this.setPollQuestion(pollQuestion);\n      }\n    },\n    subscribeToPollQuestion() {\n      this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);\n    },\n    setPollQuestion(pollQuestion) {\n      let pollQuestionText = pollQuestion.question;\n      if (!pollQuestionText || pollQuestionText === '') {\n        pollQuestionText = '--';\n        this.indicator.text('No Poll Question');\n      } else {\n        this.indicator.text(pollQuestionText);\n      }\n\n      this.currentPollQuestion = pollQuestionText;\n      this.pollQuestionTimestamp = pollQuestion.timestamp;\n      this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();\n    },\n    async updatePollQuestion() {\n      const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);\n      if (result === true) {\n        this.openmct.notifications.info('Successfully set new poll question');\n      } else {\n        this.openmct.notifications.error('Unable to set new poll question.');\n      }\n\n      this.newPollQuestion = undefined;\n    },\n    async clearPollQuestion() {\n      this.currentPollQuestion = undefined;\n      await Promise.all([\n        this.openmct.user.status.resetAllStatuses(),\n        this.openmct.user.status.setPollQuestion()\n      ]);\n    },\n    async fetchStatusSummary() {\n      const allStatuses = await this.openmct.user.status.getPossibleStatuses();\n      const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {\n        statusToCountMap[status.key] = 0;\n\n        return statusToCountMap;\n      }, {});\n      const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();\n      const statusesForRoles = await Promise.all(\n        allStatusRoles.map((role) => this.openmct.user.status.getStatusForRole(role))\n      );\n      statusesForRoles.forEach((status, i) => {\n        const currentCount = statusCountMap[status.key];\n        statusCountMap[status.key] = currentCount + 1;\n      });\n\n      this.statusCountViewModel = allStatuses.map((status) => {\n        return {\n          status: this.applyStyling(status),\n          roleCount: statusCountMap[status.key]\n        };\n      });\n      const defaultStatuses = await Promise.all(\n        allStatusRoles.map((role) => this.openmct.user.status.getDefaultStatusForRole(role))\n      );\n      this.statusesForRolesViewModel = [];\n      statusesForRoles.forEach((status, index) => {\n        const isDefaultStatus = defaultStatuses[index].key === status.key;\n        let statusTimestamp = status.timestamp;\n        if (isDefaultStatus) {\n          // if the default is selected, set timestamp to undefined\n          statusTimestamp = undefined;\n        }\n\n        this.statusesForRolesViewModel.push({\n          status: this.applyStyling(status),\n          role: allStatusRoles[index],\n          age: this.formatStatusAge(statusTimestamp, this.pollQuestionTimestamp)\n        });\n      });\n    },\n    formatStatusAge(statusTimestamp, pollQuestionTimestamp) {\n      if (statusTimestamp === undefined || pollQuestionTimestamp === undefined) {\n        return '--';\n      }\n\n      const statusAgeInMs = statusTimestamp - pollQuestionTimestamp;\n      const absoluteTotalSeconds = Math.floor(Math.abs(statusAgeInMs) / 1000);\n      let hours = Math.floor(absoluteTotalSeconds / 3600);\n      let minutes = Math.floor((absoluteTotalSeconds - hours * 3600) / 60);\n      let secondsString = absoluteTotalSeconds - hours * 3600 - minutes * 60;\n\n      if (statusAgeInMs > 0 || absoluteTotalSeconds === 0) {\n        hours = `+ ${hours}`;\n      } else {\n        hours = `- ${hours}`;\n      }\n\n      if (minutes < 10) {\n        minutes = `0${minutes}`;\n      }\n\n      if (secondsString < 10) {\n        secondsString = `0${secondsString}`;\n      }\n\n      const statusAgeString = `${hours}:${minutes}:${secondsString}`;\n\n      return statusAgeString;\n    },\n    applyStyling(status) {\n      const stylesForStatus = this.configuration?.statusStyles?.[status.label];\n\n      if (stylesForStatus !== undefined) {\n        return {\n          ...status,\n          ...stylesForStatus\n        };\n      } else {\n        return status;\n      }\n    },\n    noop() {}\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport AbstractStatusIndicator from '../AbstractStatusIndicator.js';\nimport PollQuestionComponent from './PollQuestion.vue';\n\nexport default class PollQuestionIndicator extends AbstractStatusIndicator {\n  createPopupComponent() {\n    const indicator = this.getIndicator();\n    const { vNode } = mount(\n      {\n        components: {\n          PollQuestion: PollQuestionComponent\n        },\n        provide: {\n          openmct: this.openmct,\n          indicator: indicator,\n          configuration: this.getConfiguration()\n        },\n        data() {\n          return {\n            positionX: 0,\n            positionY: 0\n          };\n        },\n        template: '<poll-question :positionX=\"positionX\" :positionY=\"positionY\" />'\n      },\n      {\n        app: this.openmct.app\n      }\n    );\n\n    return vNode.componentInstance;\n  }\n\n  createIndicator() {\n    const pollQuestionIndicator = this.openmct.indicators.simpleIndicator();\n\n    pollQuestionIndicator.text('No Poll Question');\n    pollQuestionIndicator.description('Set the current poll question');\n    pollQuestionIndicator.iconClass('icon-status-poll-edit');\n    pollQuestionIndicator.element.classList.add('c-indicator--operator-status');\n    pollQuestionIndicator.element.classList.add('no-minify');\n    pollQuestionIndicator.on('click', this.showPopup);\n\n    return pollQuestionIndicator;\n  }\n}\n"
  },
  {
    "path": "src/plugins/performanceIndicator/README.md",
    "content": "# URL Indicator\nAdds an indicator which shows the number of frames that the browser is able to render per second. This is a useful proxy for the maximum number of updates that any telemetry display can perform per second. This may be useful during performance testing, but probably should not be enabled by default. \n\nThis indicator adds adds about 3% points to CPU usage in the Chrome task manager.\n\n## Installation\n```js\nopenmct.install(openmct.plugins.PerformanceIndicator());\n```"
  },
  {
    "path": "src/plugins/performanceIndicator/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nconst PERFORMANCE_OVERLAY_RENDER_INTERVAL = 1000;\n\nexport default function PerformanceIndicator() {\n  return function install(openmct) {\n    let frames = 0;\n    let lastCalculated = performance.now();\n    openmct.performance = {\n      measurements: new Map()\n    };\n\n    const indicator = openmct.indicators.simpleIndicator();\n    indicator.key = 'performance-indicator';\n    indicator.text('~ fps');\n    indicator.description('Performance Indicator');\n    indicator.statusClass('s-status-info');\n    indicator.on('click', showOverlay);\n\n    openmct.indicators.add(indicator);\n\n    let rafHandle = requestAnimationFrame(incrementFrames);\n\n    openmct.on('destroy', () => {\n      cancelAnimationFrame(rafHandle);\n    });\n\n    function incrementFrames() {\n      let now = performance.now();\n      if (now - lastCalculated < 1000) {\n        frames++;\n      } else {\n        updateFPS(frames);\n        lastCalculated = now;\n        frames = 1;\n      }\n\n      rafHandle = requestAnimationFrame(incrementFrames);\n    }\n\n    function updateFPS(fps) {\n      indicator.text(`${fps} fps`);\n      if (fps >= 40) {\n        indicator.statusClass('s-status-on');\n      } else if (fps < 40 && fps >= 20) {\n        indicator.statusClass('s-status-warning');\n      } else {\n        indicator.statusClass('s-status-error');\n      }\n    }\n\n    function showOverlay() {\n      const overlayStylesText = `\n          #c-performance-indicator--overlay {\n            background-color:rgba(0,0,0,0.5);\n            position: absolute;\n            width: 300px;\n            left: calc(50% - 300px);\n          }\n      `;\n      const overlayMarkup = `\n        <div id=\"c-performance-indicator--overlay\" title=\"Performance Overlay\">\n          <table id=\"c-performance-indicator--table\">\n            <tr class=\"c-performance-indicator--row\"><td class=\"c-performance-indicator--measurement-name\"></td><td class=\"c-performance-indicator--measurement-value\"></td></tr>\n          </table>\n        </div>\n      `;\n      const overlayTemplate = document.createElement('div');\n      overlayTemplate.innerHTML = overlayMarkup;\n      const overlay = overlayTemplate.cloneNode(true);\n      overlay.querySelector('.c-performance-indicator--row').remove();\n      const overlayStyles = document.createElement('style');\n      overlayStyles.appendChild(document.createTextNode(overlayStylesText));\n\n      document.head.appendChild(overlayStyles);\n      document.body.appendChild(overlay);\n\n      indicator.off('click', showOverlay);\n\n      const interval = setInterval(() => {\n        overlay.querySelector('#c-performance-indicator--table').innerHTML = '';\n\n        for (const [name, value] of openmct.performance.measurements.entries()) {\n          const newRow = overlayTemplate\n            .querySelector('.c-performance-indicator--row')\n            .cloneNode(true);\n          newRow.querySelector('.c-performance-indicator--measurement-name').innerText = name;\n          newRow.querySelector('.c-performance-indicator--measurement-value').innerText = value;\n          overlay.querySelector('#c-performance-indicator--table').appendChild(newRow);\n        }\n      }, PERFORMANCE_OVERLAY_RENDER_INTERVAL);\n\n      indicator.on(\n        'click',\n        () => {\n          overlayStyles.remove();\n          overlay.remove();\n          indicator.on('click', showOverlay);\n          clearInterval(interval);\n        },\n        { once: true, capture: true }\n      );\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/performanceIndicator/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport PerformancePlugin from './plugin.js';\n\ndescribe('the plugin', () => {\n  let openmct;\n  let element;\n  let child;\n\n  let performanceIndicator;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.install(new PerformancePlugin());\n\n    openmct.on('start', done);\n\n    performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => {\n      return indicator.text && indicator.text() === '~ fps';\n    });\n\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('installs the performance indicator', () => {\n    expect(performanceIndicator).toBeDefined();\n  });\n\n  it('calculates an fps value', async () => {\n    await loopForABit();\n    // eslint-disable-next-line radix\n    const fps = parseInt(performanceIndicator.text().split(' fps')[0]);\n    expect(fps).toBeGreaterThan(0);\n  });\n\n  function loopForABit() {\n    let frames = 0;\n\n    return new Promise((resolve) => {\n      requestAnimationFrame(function loop() {\n        if (++frames > 90) {\n          resolve();\n        } else {\n          requestAnimationFrame(loop);\n        }\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchChangesFeed.js",
    "content": "(function () {\n  const connections = [];\n  let connected = false;\n  let couchEventSource;\n  let changesFeedUrl;\n  const keepAliveTime = 20 * 1000;\n  let keepAliveTimer;\n  const controller = new AbortController();\n\n  self.onconnect = function (e) {\n    let port = e.ports[0];\n    connections.push(port);\n\n    port.postMessage({\n      type: 'connection',\n      connectionId: connections.length\n    });\n\n    port.onmessage = function (event) {\n      if (event.data.request === 'close') {\n        console.debug('🚪 Closing couch connection 🚪');\n        connections.splice(event.data.connectionId - 1, 1);\n        if (connections.length <= 0) {\n          // abort any outstanding requests if there's nobody listening to it.\n          controller.abort();\n        }\n\n        connected = false;\n        // stop listening for events\n        couchEventSource.removeEventListener('message', self.onCouchMessage);\n        couchEventSource.close();\n        console.debug('🚪 Closed couch connection 🚪');\n\n        return;\n      }\n\n      if (event.data.request === 'changes') {\n        if (connected === true) {\n          return;\n        }\n\n        changesFeedUrl = event.data.url;\n        self.listenForChanges();\n      }\n    };\n\n    port.start();\n  };\n\n  self.onerror = function (error) {\n    self.updateCouchStateIndicator();\n    console.error('🚨 Error on CouchDB feed 🚨', error);\n  };\n\n  self.onopen = function () {\n    self.updateCouchStateIndicator();\n  };\n\n  self.onCouchMessage = function (event) {\n    self.updateCouchStateIndicator();\n    console.debug('📩 Received message from CouchDB 📩');\n\n    const objectChanges = JSON.parse(event.data);\n    connections.forEach(function (connection) {\n      connection.postMessage({\n        objectChanges\n      });\n    });\n  };\n\n  self.listenForChanges = function () {\n    if (keepAliveTimer) {\n      clearTimeout(keepAliveTimer);\n    }\n\n    /**\n     * Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly.\n     * If it has, attempt to reconnect.\n     */\n    keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);\n\n    if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {\n      console.debug(`⇿ Opening CouchDB change feed connection for ${changesFeedUrl} ⇿`);\n      couchEventSource = new EventSource(changesFeedUrl);\n      couchEventSource.onerror = self.onerror;\n      couchEventSource.onopen = self.onopen;\n\n      // start listening for events\n      couchEventSource.addEventListener('message', self.onCouchMessage);\n      connected = true;\n      console.debug(`⇿ Opened connection to ${changesFeedUrl} ⇿`);\n    }\n  };\n\n  self.updateCouchStateIndicator = function () {\n    const { readyState } = couchEventSource;\n    let message = {\n      type: 'state',\n      state: 'pending'\n    };\n    switch (readyState) {\n      case EventSource.CONNECTING:\n        message.state = 'pending';\n        break;\n      case EventSource.OPEN:\n        message.state = 'open';\n        break;\n      case EventSource.CLOSED:\n        message.state = 'close';\n        break;\n      default:\n        message.state = 'unknown';\n        console.error(\n          '🚨 Received unexpected readyState value from CouchDB EventSource feed: 🚨',\n          readyState\n        );\n        break;\n    }\n\n    connections.forEach(function (connection) {\n      connection.postMessage(message);\n    });\n  };\n})();\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchDocument.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A CouchDocument describes domain object model in a format\n * which is easily read-written to CouchDB. This includes\n * Couch's _id and _rev fields, as well as a separate\n * metadata field which contains a subset of information found\n * in the model itself (to support search optimization with\n * CouchDB views.)\n * @constructor\n * @param {string} id the id under which to store this mode\n * @param {Object} model the model to store\n * @param {string} rev the revision to include (or undefined,\n *        if no revision should be noted for couch)\n * @param {boolean} whether or not to mark this document as\n *        deleted (see CouchDB docs for _deleted)\n */\nexport default function CouchDocument(id, model, rev, markDeleted) {\n  return {\n    _id: id,\n    _rev: rev,\n    _deleted: markDeleted,\n    metadata: {\n      category: 'domain object',\n      type: model.type,\n      owner: 'admin',\n      name: model.name\n    },\n    model: model\n  };\n}\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchObjectProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\n\nimport { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';\nimport CouchDocument from './CouchDocument.js';\nimport CouchObjectQueue from './CouchObjectQueue.js';\nimport { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator.js';\n\nconst REV = '_rev';\nconst ID = '_id';\nconst HEARTBEAT = 50000;\nconst ALL_DOCS = '_all_docs?include_docs=true';\n\nclass CouchObjectProvider {\n  constructor({ openmct, databaseConfiguration, couchStatusIndicator }) {\n    this.openmct = openmct;\n    this.indicator = couchStatusIndicator;\n    this.url = databaseConfiguration.url;\n    this.readOnly = databaseConfiguration.readOnly;\n    this.useDesignDocuments = databaseConfiguration.useDesignDocuments;\n    this.namespace = databaseConfiguration.namespace;\n    this.objectQueue = {};\n    this.observers = {};\n    this.batchIds = [];\n    this.onEventMessage = this.onEventMessage.bind(this);\n    this.onEventError = this.onEventError.bind(this);\n    this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));\n    this.persistenceQueue = [];\n    this.rootObject = null;\n  }\n\n  /**\n   * @private\n   */\n  #startSharedWorker() {\n    let provider = this;\n    let sharedWorker;\n\n    // eslint-disable-next-line no-undef\n    const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;\n\n    sharedWorker = new SharedWorker(\n      sharedWorkerURL,\n      `CouchDB SSE Shared Worker for ${this.namespace}`\n    );\n    sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);\n    sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);\n    sharedWorker.port.start();\n\n    this.openmct.on('destroy', () => {\n      this.changesFeedSharedWorker.port.postMessage({\n        request: 'close',\n        connectionId: this.changesFeedSharedWorkerConnectionId\n      });\n      this.changesFeedSharedWorker.port.close();\n    });\n\n    return sharedWorker;\n  }\n\n  onSharedWorkerMessageError(event) {\n    console.error('Error', event);\n  }\n\n  isSynchronizedObject(object) {\n    return (\n      object &&\n      object.type &&\n      this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES &&\n      this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)\n    );\n  }\n\n  onSharedWorkerMessage(event) {\n    if (event.data.type === 'connection') {\n      this.changesFeedSharedWorkerConnectionId = event.data.connectionId;\n    } else if (event.data.type === 'state') {\n      const state = this.#messageToIndicatorState(event.data.state);\n      this.indicator?.setIndicatorToState(state);\n    } else {\n      let objectChanges = event.data.objectChanges;\n      const objectIdentifier = {\n        namespace: this.namespace,\n        key: objectChanges.id\n      };\n      let keyString = this.openmct.objects.makeKeyString(objectIdentifier);\n      //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.\n      let observersForObject = this.observers[keyString];\n      let isInTransaction = false;\n\n      if (this.openmct.objects.isTransactionActive()) {\n        isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);\n      }\n\n      if (observersForObject && !isInTransaction) {\n        observersForObject.forEach(async (observer) => {\n          const updatedObject = await this.get(objectIdentifier);\n          if (this.isSynchronizedObject(updatedObject)) {\n            observer(updatedObject);\n          }\n        });\n      }\n    }\n  }\n\n  /**\n   * Takes in a state message from the CouchDB SharedWorker and returns an IndicatorState.\n   * @private\n   * @param {'open'|'close'|'pending'} message\n   * @returns {import('./CouchStatusIndicator').IndicatorState}\n   */\n  #messageToIndicatorState(message) {\n    let state;\n    switch (message) {\n      case 'open':\n        state = CONNECTED;\n        break;\n      case 'close':\n        state = DISCONNECTED;\n        break;\n      case 'pending':\n        state = PENDING;\n        break;\n      case 'unknown':\n        state = UNKNOWN;\n        break;\n    }\n\n    return state;\n  }\n\n  /**\n   * Takes an HTTP status code and returns an IndicatorState\n   * @private\n   * @param {number} statusCode\n   * @returns {import(\"./CouchStatusIndicator\").IndicatorState}\n   */\n  #statusCodeToIndicatorState(statusCode) {\n    let state;\n    switch (statusCode) {\n      case CouchObjectProvider.HTTP_OK:\n      case CouchObjectProvider.HTTP_CREATED:\n      case CouchObjectProvider.HTTP_ACCEPTED:\n      case CouchObjectProvider.HTTP_NOT_MODIFIED:\n      case CouchObjectProvider.HTTP_BAD_REQUEST:\n      case CouchObjectProvider.HTTP_UNAUTHORIZED:\n      case CouchObjectProvider.HTTP_FORBIDDEN:\n      case CouchObjectProvider.HTTP_NOT_FOUND:\n      case CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED:\n      case CouchObjectProvider.HTTP_NOT_ACCEPTABLE:\n      case CouchObjectProvider.HTTP_CONFLICT:\n      case CouchObjectProvider.HTTP_PRECONDITION_FAILED:\n      case CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE:\n      case CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE:\n      case CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:\n      case CouchObjectProvider.HTTP_EXPECTATION_FAILED:\n      case CouchObjectProvider.HTTP_SERVER_ERROR:\n        state = CONNECTED;\n        break;\n      case CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE:\n        state = DISCONNECTED;\n        break;\n      default:\n        state = UNKNOWN;\n    }\n\n    return state;\n  }\n\n  isReadOnly() {\n    return this.readOnly;\n  }\n\n  async request(subPath, method, body, signal) {\n    let fetchOptions = {\n      method,\n      body,\n      priority: 'high',\n      signal\n    };\n\n    // stringify body if needed\n    if (fetchOptions.body) {\n      fetchOptions.body = JSON.stringify(fetchOptions.body);\n      fetchOptions.headers = {\n        'Content-Type': 'application/json'\n      };\n    }\n\n    let response = null;\n\n    if (!this.isObservingObjectChanges()) {\n      this.#observeObjectChanges();\n    }\n\n    try {\n      response = await fetch(this.url + '/' + subPath, fetchOptions);\n      const { status } = response;\n      const json = await response.json();\n      this.#handleResponseCode(status, json, fetchOptions);\n\n      return json;\n    } catch (error) {\n      // abort errors are expected\n      if (error.name === 'AbortError') {\n        return;\n      }\n\n      // Network error, CouchDB unreachable.\n      if (response === null) {\n        this.indicator?.setIndicatorToState(DISCONNECTED);\n        console.error(error.message);\n\n        throw new Error(`CouchDB Error - No response\"`);\n      } else {\n        if (body?.model && isNotebookOrAnnotationType(body.model)) {\n          // warn since we handle conflicts for notebooks\n          console.warn(error.message);\n        } else {\n          console.error(error.message);\n        }\n\n        throw error;\n      }\n    }\n  }\n\n  /**\n   * Handle the response code from a CouchDB request.\n   * Sets the CouchDB indicator status and throws an error if needed.\n   * @private\n   */\n  #handleResponseCode(status, json, fetchOptions) {\n    this.indicator?.setIndicatorToState(this.#statusCodeToIndicatorState(status));\n    if (status === CouchObjectProvider.HTTP_CONFLICT) {\n      const objectName = JSON.parse(fetchOptions.body)?.model?.name;\n      throw new this.openmct.objects.errors.Conflict(`Conflict persisting \"${objectName}\"`);\n    } else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {\n      if (!json.error || !json.reason) {\n        throw new Error(`CouchDB Error ${status}`);\n      }\n\n      throw new Error(`CouchDB Error ${status}: \"${json.error} - ${json.reason}\"`);\n    }\n  }\n\n  /**\n   * Check the response to a create/update/delete request;\n   * track the rev if it's valid, otherwise return false to\n   * indicate that the request failed.\n   * persist any queued objects\n   * @private\n   */\n  #checkResponse(response, intermediateResponse, key) {\n    let requestSuccess = false;\n    const id = response ? response.id : undefined;\n    let rev;\n\n    if (response && response.ok) {\n      rev = response.rev;\n      requestSuccess = true;\n    }\n\n    intermediateResponse.resolve(requestSuccess);\n\n    if (id) {\n      if (!this.objectQueue[id]) {\n        this.objectQueue[id] = new CouchObjectQueue(undefined, rev);\n      }\n\n      this.objectQueue[id].updateRevision(rev);\n      this.objectQueue[id].pending = false;\n      if (this.objectQueue[id].hasNext()) {\n        this.#updateQueued(id);\n      }\n    } else {\n      this.objectQueue[key].pending = false;\n    }\n  }\n\n  /**\n   * @private\n   */\n  #getModel(response) {\n    if (response && response.model) {\n      let key = response[ID];\n      let object = this.fromPersistedModel(response.model, key);\n\n      if (!this.objectQueue[key]) {\n        this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);\n      }\n\n      if (isNotebookOrAnnotationType(object)) {\n        //Temporary measure until object sync is supported for all object types\n        //Always update notebook revision number because we have realtime sync, so always assume it's the latest.\n        this.objectQueue[key].updateRevision(response[REV]);\n      } else if (!this.objectQueue[key].pending) {\n        //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress\n        this.objectQueue[key].updateRevision(response[REV]);\n      }\n\n      return object;\n    } else {\n      return undefined;\n    }\n  }\n\n  get(identifier, abortSignal) {\n    this.batchIds.push(identifier.key);\n\n    if (this.bulkPromise === undefined) {\n      this.bulkPromise = this.#deferBatchedGet(abortSignal);\n    }\n\n    return this.bulkPromise.then((domainObjectMap) => {\n      return domainObjectMap[identifier.key];\n    });\n  }\n\n  /**\n   * @private\n   */\n  #deferBatchedGet(abortSignal) {\n    // We until the next event loop cycle to \"collect\" all of the get\n    // requests triggered in this iteration of the event loop\n\n    return this.#waitOneEventCycle().then(() => {\n      let batchIds = this.batchIds;\n\n      this.#clearBatch();\n\n      if (batchIds.length === 1) {\n        let objectKey = batchIds[0];\n\n        //If there's only one request, just do a regular get\n        return this.request(objectKey, 'GET', undefined, abortSignal).then(\n          this.#returnAsMap(objectKey)\n        );\n      } else {\n        return this.#bulkGet(batchIds, abortSignal);\n      }\n    });\n  }\n\n  /**\n   * @private\n   */\n  #returnAsMap(objectKey) {\n    return (result) => {\n      let objectMap = {};\n      objectMap[objectKey] = this.#getModel(result);\n\n      return objectMap;\n    };\n  }\n\n  /**\n   * @private\n   */\n  #clearBatch() {\n    this.batchIds = [];\n    delete this.bulkPromise;\n  }\n\n  /**\n   * @private\n   */\n  #waitOneEventCycle() {\n    return new Promise((resolve) => {\n      setTimeout(resolve);\n    });\n  }\n\n  /**\n   * @private\n   */\n  #bulkGet(ids, signal) {\n    ids = this.removeDuplicates(ids);\n\n    const query = {\n      keys: ids\n    };\n\n    return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {\n      if (response && response.rows !== undefined) {\n        return response.rows.reduce((map, row) => {\n          //row.doc === null if the document does not exist.\n          //row.doc === undefined if the document is not found.\n          if (row.doc !== undefined) {\n            map[row.key] = this.#getModel(row.doc);\n          }\n\n          return map;\n        }, {});\n      } else {\n        return {};\n      }\n    });\n  }\n\n  /**\n   * @private\n   */\n  removeDuplicates(array) {\n    return Array.from(new Set(array));\n  }\n\n  search() {\n    // Dummy search function. It has to appear to support search,\n    // otherwise the in-memory indexer will index all of its objects,\n    // but actually search results will be provided by a separate search provider\n    // see CouchSearchProvider.js\n    return Promise.resolve([]);\n  }\n\n  async isViewDefined(designDoc, viewName) {\n    const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;\n    const response = await fetch(url, {\n      method: 'HEAD'\n    });\n\n    return response.ok;\n  }\n\n  /**\n   * @typedef GetObjectByViewOptions\n   * @property {String} designDoc the name of the design document that the view belongs to\n   * @property {String} viewName\n   * @property {Array.<String>} [keysToSearch] a list of discrete view keys to search for. View keys are not object identifiers.\n   * @property {String} [startKey] limit the search to a range of keys starting with the provided `startKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided\n   * @property {String} [endKey] limit the search to a range of keys ending with the provided `endKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided\n   * @property {Number} [limit] limit the number of results returned\n   * @property {String} [objectIdField] The field (either key or value) to treat as an object key. If provided, include_docs will be set to false in the request, and the field will be used as an object identifier. A bulk request will be used to resolve objects from identifiers\n   */\n  /**\n   * Return objects based on a call to a view. See https://docs.couchdb.org/en/stable/api/ddoc/views.html.\n   * @param {GetObjectByViewOptions} options\n   * @param {AbortSignal} abortSignal\n   * @returns {Promise<Array.<import('openmct.js').DomainObject>>}\n   */\n  async getObjectsByView(\n    { designDoc, viewName, keysToSearch, startKey, endKey, limit, objectIdField },\n    abortSignal\n  ) {\n    const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;\n    const requestBody = {};\n    let requestBodyString;\n\n    if (objectIdField === undefined) {\n      requestBody.include_docs = true;\n    }\n\n    if (limit !== undefined) {\n      requestBody.limit = limit;\n    }\n\n    if (startKey !== undefined && endKey !== undefined) {\n      /* spell-checker: disable */\n      requestBody.startkey = startKey;\n      requestBody.endkey = endKey;\n      requestBodyString = JSON.stringify(requestBody);\n      requestBodyString = requestBodyString.replace('$START_KEY', startKey);\n      requestBodyString = requestBodyString.replace('$END_KEY', endKey);\n      /* spell-checker: enable */\n    } else {\n      requestBody.keys = keysToSearch;\n      requestBodyString = JSON.stringify(requestBody);\n    }\n\n    let objectModels = [];\n\n    try {\n      const response = await fetch(url, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        signal: abortSignal,\n        body: requestBodyString\n      });\n\n      if (!response.ok) {\n        throw new Error(\n          `HTTP request failed with status ${response.status} ${response.statusText}`\n        );\n      }\n\n      const result = await response.json();\n      const couchRows = result.rows;\n      if (objectIdField !== undefined) {\n        const objectIdsToResolve = [];\n        couchRows.forEach((couchRow) => {\n          objectIdsToResolve.push(couchRow[objectIdField]);\n        });\n        objectModels = Object.values(await this.#bulkGet(objectIdsToResolve), abortSignal);\n      } else {\n        couchRows.forEach((couchRow) => {\n          const couchDoc = couchRow.doc;\n          const objectModel = this.#getModel(couchDoc);\n          if (objectModel) {\n            objectModels.push(objectModel);\n          }\n        });\n      }\n    } catch (error) {\n      // do nothing\n    }\n    return objectModels;\n  }\n\n  async getObjectsByFilter(filter, abortSignal) {\n    let objects = [];\n\n    let url = `${this.url}/_find`;\n    let body = {};\n\n    if (filter) {\n      body = JSON.stringify(filter);\n    }\n\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      signal: abortSignal,\n      body\n    });\n\n    const reader = response.body.getReader();\n    let completed = false;\n    let decoder = new TextDecoder('utf-8');\n    let decodedChunk = '';\n    while (!completed) {\n      const { done, value } = await reader.read();\n      //done is true when we lose connection with the provider\n      if (done) {\n        completed = true;\n      }\n\n      if (value) {\n        let chunk = new Uint8Array(value.length);\n        chunk.set(value, 0);\n        const partial = decoder.decode(chunk, { stream: !completed });\n        decodedChunk = decodedChunk + partial;\n      }\n    }\n\n    try {\n      const json = JSON.parse(decodedChunk);\n      if (json) {\n        let docs = json.docs;\n        docs.forEach((doc) => {\n          let object = this.#getModel(doc);\n          if (object) {\n            objects.push(object);\n          }\n        });\n      }\n    } catch (e) {\n      //do nothing\n    }\n\n    return objects;\n  }\n\n  observe(identifier, callback) {\n    const keyString = this.openmct.objects.makeKeyString(identifier);\n    this.observers[keyString] = this.observers[keyString] || [];\n    this.observers[keyString].push(callback);\n\n    if (!this.isObservingObjectChanges()) {\n      this.#observeObjectChanges();\n    }\n\n    return () => {\n      if (this.observers[keyString]) {\n        this.observers[keyString] = this.observers[keyString].filter(\n          (observer) => observer !== callback\n        );\n        if (this.observers[keyString].length === 0) {\n          delete this.observers[keyString];\n        }\n      }\n    };\n  }\n\n  isObservingObjectChanges() {\n    return this.stopObservingObjectChanges !== undefined;\n  }\n\n  /**\n   * @private\n   */\n  #observeObjectChanges() {\n    const sseChangesPath = `${this.url}/_changes`;\n    const sseURL = new URL(sseChangesPath);\n    sseURL.searchParams.append('feed', 'eventsource');\n    sseURL.searchParams.append('style', 'main_only');\n    sseURL.searchParams.append('heartbeat', HEARTBEAT);\n\n    if (typeof SharedWorker === 'undefined') {\n      this.fetchChanges(sseURL.toString());\n    } else {\n      this.#initiateSharedWorkerFetchChanges(sseURL.toString());\n    }\n  }\n\n  /**\n   * @private\n   */\n  #initiateSharedWorkerFetchChanges(url) {\n    if (!this.changesFeedSharedWorker) {\n      this.changesFeedSharedWorker = this.#startSharedWorker();\n\n      if (this.isObservingObjectChanges()) {\n        this.stopObservingObjectChanges();\n      }\n\n      this.stopObservingObjectChanges = () => {\n        delete this.stopObservingObjectChanges;\n      };\n\n      this.changesFeedSharedWorker.port.postMessage({\n        request: 'changes',\n        url\n      });\n    }\n  }\n\n  onEventError(error) {\n    console.error('Error on feed', error);\n    const { readyState } = error.target;\n    this.#updateIndicatorStatus(readyState);\n  }\n\n  onEventOpen(event) {\n    const { readyState } = event.target;\n    this.#updateIndicatorStatus(readyState);\n  }\n\n  onEventMessage(event) {\n    const { readyState } = event.target;\n    const eventData = JSON.parse(event.data);\n    const identifier = {\n      namespace: this.namespace,\n      key: eventData.id\n    };\n    const keyString = this.openmct.objects.makeKeyString(identifier);\n    this.#updateIndicatorStatus(readyState);\n    let observersForObject = this.observers[keyString];\n\n    if (observersForObject) {\n      observersForObject.forEach(async (observer) => {\n        const updatedObject = await this.get(identifier);\n        if (this.isSynchronizedObject(updatedObject)) {\n          observer(updatedObject);\n        }\n      });\n    }\n  }\n\n  fetchChanges(url) {\n    const controller = new AbortController();\n    let couchEventSource;\n\n    if (this.isObservingObjectChanges()) {\n      this.stopObservingObjectChanges();\n    }\n\n    this.stopObservingObjectChanges = () => {\n      controller.abort();\n      couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));\n      delete this.stopObservingObjectChanges;\n    };\n\n    console.debug('⇿ Opening CouchDB change feed connection ⇿');\n\n    couchEventSource = new EventSource(url);\n    couchEventSource.onerror = this.onEventError.bind(this);\n    couchEventSource.onopen = this.onEventOpen.bind(this);\n\n    // start listening for events\n    couchEventSource.addEventListener('message', this.onEventMessage.bind(this));\n\n    console.debug('⇿ Opened connection ⇿');\n  }\n\n  /**\n   * @private\n   */\n  #getIntermediateResponse() {\n    let intermediateResponse = {};\n    intermediateResponse.promise = new Promise(function (resolve, reject) {\n      intermediateResponse.resolve = resolve;\n      intermediateResponse.reject = reject;\n    });\n\n    return intermediateResponse;\n  }\n\n  /**\n   * Update the indicator status based on the readyState of the EventSource\n   * @private\n   */\n  #updateIndicatorStatus(readyState) {\n    let message;\n    switch (readyState) {\n      case EventSource.CONNECTING:\n        message = 'pending';\n        break;\n      case EventSource.OPEN:\n        message = 'open';\n        break;\n      case EventSource.CLOSED:\n        message = 'close';\n        break;\n      default:\n        message = 'unknown';\n        break;\n    }\n\n    const indicatorState = this.#messageToIndicatorState(message);\n    this.indicator?.setIndicatorToState(indicatorState);\n  }\n\n  /**\n   * @private\n   */\n  enqueueObject(key, model, intermediateResponse) {\n    if (this.objectQueue[key]) {\n      this.objectQueue[key].enqueue({\n        model,\n        intermediateResponse\n      });\n    } else {\n      this.objectQueue[key] = new CouchObjectQueue({\n        model,\n        intermediateResponse\n      });\n    }\n  }\n\n  create(model) {\n    let intermediateResponse = this.#getIntermediateResponse();\n    const key = model.identifier.key;\n    model = this.toPersistableModel(model);\n    this.enqueueObject(key, model, intermediateResponse);\n\n    if (!this.objectQueue[key].pending) {\n      this.objectQueue[key].pending = true;\n      const queued = this.objectQueue[key].dequeue();\n      let couchDocument = new CouchDocument(key, queued.model);\n\n      this.#enqueueForPersistence({\n        key,\n        document: couchDocument\n      })\n        .then((response) => {\n          this.#checkResponse(response, queued.intermediateResponse, key);\n        })\n        .catch((error) => {\n          queued.intermediateResponse.reject(error);\n          this.objectQueue[key].pending = false;\n        });\n    }\n\n    return intermediateResponse.promise;\n  }\n\n  #enqueueForPersistence({ key, document }) {\n    return new Promise((resolve, reject) => {\n      this.persistenceQueue.push({\n        key,\n        document,\n        resolve,\n        reject\n      });\n      this.flushPersistenceQueue();\n    });\n  }\n\n  async flushPersistenceQueue() {\n    if (this.persistenceQueue.length > 1) {\n      const batch = {\n        docs: this.persistenceQueue.map((queued) => queued.document)\n      };\n      const response = await this.request('_bulk_docs', 'POST', batch);\n      response.forEach((responseMetadatum) => {\n        const queued = this.persistenceQueue.find(\n          (queuedMetadatum) => queuedMetadatum.key === responseMetadatum.id\n        );\n        if (responseMetadatum.ok) {\n          queued.resolve(responseMetadatum);\n        } else {\n          queued.reject(responseMetadatum);\n        }\n      });\n    } else if (this.persistenceQueue.length === 1) {\n      const { key, document, resolve, reject } = this.persistenceQueue[0];\n\n      this.request(key, 'PUT', document).then(resolve).catch(reject);\n    }\n    this.persistenceQueue = [];\n  }\n\n  /**\n   * @private\n   */\n  #updateQueued(key) {\n    if (!this.objectQueue[key].pending) {\n      this.objectQueue[key].pending = true;\n      const queued = this.objectQueue[key].dequeue();\n      let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);\n      this.request(key, 'PUT', document)\n        .then((response) => {\n          this.#checkResponse(response, queued.intermediateResponse, key);\n        })\n        .catch((error) => {\n          queued.intermediateResponse.reject(error);\n          this.objectQueue[key].pending = false;\n        });\n    }\n  }\n\n  update(model) {\n    let intermediateResponse = this.#getIntermediateResponse();\n    const key = model.identifier.key;\n    model = this.toPersistableModel(model);\n\n    this.enqueueObject(key, model, intermediateResponse);\n    this.#updateQueued(key);\n\n    return intermediateResponse.promise;\n  }\n\n  toPersistableModel(model) {\n    //First make a copy so we are not mutating the provided model.\n    const persistableModel = JSON.parse(JSON.stringify(model));\n    //Delete the identifier. Couch manages namespaces dynamically.\n    delete persistableModel.identifier;\n\n    return persistableModel;\n  }\n\n  fromPersistedModel(model, key) {\n    model.identifier = {\n      namespace: this.namespace,\n      key\n    };\n\n    return model;\n  }\n}\n\n// https://docs.couchdb.org/en/3.2.0/api/basics.html\nCouchObjectProvider.HTTP_OK = 200;\nCouchObjectProvider.HTTP_CREATED = 201;\nCouchObjectProvider.HTTP_ACCEPTED = 202;\nCouchObjectProvider.HTTP_NOT_MODIFIED = 304;\nCouchObjectProvider.HTTP_BAD_REQUEST = 400;\nCouchObjectProvider.HTTP_UNAUTHORIZED = 401;\nCouchObjectProvider.HTTP_FORBIDDEN = 403;\nCouchObjectProvider.HTTP_NOT_FOUND = 404;\nCouchObjectProvider.HTTP_METHOD_NOT_ALLOWED = 404;\nCouchObjectProvider.HTTP_NOT_ACCEPTABLE = 406;\nCouchObjectProvider.HTTP_CONFLICT = 409;\nCouchObjectProvider.HTTP_PRECONDITION_FAILED = 412;\nCouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE = 413;\nCouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE = 415;\nCouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;\nCouchObjectProvider.HTTP_EXPECTATION_FAILED = 417;\nCouchObjectProvider.HTTP_SERVER_ERROR = 500;\n// If CouchDB is containerized via Docker it will return 503 if service is unavailable.\nCouchObjectProvider.HTTP_SERVICE_UNAVAILABLE = 503;\n\nexport default CouchObjectProvider;\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchObjectQueue.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class CouchObjectQueue {\n  constructor(object, rev) {\n    this.rev = rev;\n    this.objects = object ? [object] : [];\n    this.pending = false;\n  }\n\n  updateRevision(rev) {\n    this.rev = rev;\n  }\n\n  hasNext() {\n    return this.objects.length;\n  }\n\n  enqueue(item) {\n    this.objects.push(item);\n  }\n\n  dequeue() {\n    return this.objects.shift();\n  }\n\n  clear() {\n    this.rev = undefined;\n    this.objects = [];\n  }\n}\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchSearchProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// This provider exists because due to legacy reasons, we need to install\n// two plugins for two namespaces for CouchDB: one for \"mct\", and one for \"\".\n// Because of this, we need to separate out the search provider from the object\n// provider so we don't return two results for each found object.\n// If the above namespace is ever resolved, we can fold this search provider\n// back into the object provider.\n\nconst BATCH_ANNOTATION_DEBOUNCE_MS = 100;\n\nclass CouchSearchProvider {\n  #bulkPromise;\n  #batchIds;\n  #lastAbortSignal;\n  #isSearchByNameViewDefined;\n  /**\n   *\n   * @param {import('./CouchObjectProvider').default} couchObjectProvider\n   */\n  constructor(couchObjectProvider) {\n    this.couchObjectProvider = couchObjectProvider;\n    this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;\n    this.useDesignDocuments = couchObjectProvider.useDesignDocuments;\n    this.supportedSearchTypes = [\n      this.searchTypes.OBJECTS,\n      this.searchTypes.ANNOTATIONS,\n      this.searchTypes.TAGS\n    ];\n    this.#batchIds = [];\n    this.#bulkPromise = null;\n  }\n\n  supportsSearchType(searchType) {\n    return this.supportedSearchTypes.includes(searchType);\n  }\n\n  isReadOnly() {\n    return true;\n  }\n\n  search(query, abortSignal, searchType) {\n    if (searchType === this.searchTypes.OBJECTS) {\n      return this.searchForObjects(query, abortSignal);\n    } else if (searchType === this.searchTypes.ANNOTATIONS) {\n      return this.searchForAnnotations(query, abortSignal);\n    } else if (searchType === this.searchTypes.TAGS) {\n      return this.searchForTags(query, abortSignal);\n    } else {\n      throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);\n    }\n  }\n\n  #isOptimizedSearchByNameSupported() {\n    let isOptimizedSearchAvailable;\n\n    if (this.#isSearchByNameViewDefined === undefined) {\n      isOptimizedSearchAvailable = this.#isSearchByNameViewDefined =\n        this.couchObjectProvider.isViewDefined('object_names', 'object_names');\n    } else {\n      isOptimizedSearchAvailable = this.#isSearchByNameViewDefined;\n    }\n\n    return isOptimizedSearchAvailable;\n  }\n\n  async searchForObjects(query, abortSignal) {\n    const preparedQuery = query.toLowerCase().trim();\n    const supportsOptimizedSearchByName = await this.#isOptimizedSearchByNameSupported();\n\n    if (supportsOptimizedSearchByName) {\n      return this.couchObjectProvider.getObjectsByView(\n        {\n          designDoc: 'object_names',\n          viewName: 'object_names',\n          startKey: preparedQuery,\n          endKey: preparedQuery + `\\ufff0`,\n          objectIdField: 'value',\n          limit: 1000\n        },\n        abortSignal\n      );\n    } else {\n      const filter = {\n        selector: {\n          model: {\n            name: {\n              $regex: `(?i)${query}`\n            }\n          }\n        }\n      };\n      return this.couchObjectProvider.getObjectsByFilter(filter);\n    }\n  }\n\n  async #deferBatchAnnotationSearch() {\n    // We until the next event loop cycle to \"collect\" all of the get\n    // requests triggered in this iteration of the event loop\n    await this.#waitForDebounce();\n    const batchIdsToSearch = [...this.#batchIds];\n    this.#clearBatch();\n    return this.#bulkAnnotationSearch(batchIdsToSearch);\n  }\n\n  #clearBatch() {\n    this.#batchIds = [];\n    this.#bulkPromise = undefined;\n  }\n\n  #waitForDebounce() {\n    let timeoutID;\n    clearTimeout(timeoutID);\n\n    return new Promise((resolve) => {\n      timeoutID = setTimeout(() => {\n        resolve();\n      }, BATCH_ANNOTATION_DEBOUNCE_MS);\n    });\n  }\n\n  #bulkAnnotationSearch(batchIdsToSearch) {\n    if (!batchIdsToSearch?.length) {\n      // nothing to search\n      return;\n    }\n\n    let lastAbortSignal = batchIdsToSearch[batchIdsToSearch.length - 1].abortSignal;\n\n    if (this.useDesignDocuments) {\n      const keysToSearch = batchIdsToSearch.map(({ keyString }) => keyString);\n      return this.couchObjectProvider.getObjectsByView(\n        {\n          designDoc: 'annotation_keystring_index',\n          viewName: 'by_keystring',\n          keysToSearch\n        },\n        lastAbortSignal\n      );\n    }\n\n    const filter = {\n      selector: {\n        $and: [\n          {\n            'model.type': {\n              $eq: 'annotation'\n            }\n          },\n          {\n            'model.targets': {\n              $elemMatch: {\n                keyString: {\n                  $in: []\n                }\n              }\n            }\n          }\n        ]\n      }\n    };\n    // TODO: should remove duplicates from batchIds\n    batchIdsToSearch.forEach(({ keyString, abortSignal }) => {\n      filter.selector.$and[1]['model.targets'].$elemMatch.keyString.$in.push(keyString);\n    });\n\n    return this.couchObjectProvider.getObjectsByFilter(filter, lastAbortSignal);\n  }\n\n  async searchForAnnotations(keyString, abortSignal) {\n    this.#batchIds.push({ keyString, abortSignal });\n    if (!this.#bulkPromise) {\n      this.#bulkPromise = this.#deferBatchAnnotationSearch();\n    }\n\n    const returnedData = await this.#bulkPromise;\n    return returnedData;\n  }\n\n  searchForTags(tagsArray, abortSignal) {\n    if (!tagsArray || !tagsArray.length) {\n      return [];\n    }\n\n    if (this.useDesignDocuments) {\n      return this.couchObjectProvider.getObjectsByView(\n        { designDoc: 'annotation_tags_index', viewName: 'by_tags', keysToSearch: tagsArray },\n        abortSignal\n      );\n    }\n\n    const filter = {\n      selector: {\n        $and: [\n          {\n            'model.type': {\n              $eq: 'annotation'\n            }\n          },\n          {\n            'model.tags': {\n              $elemMatch: {\n                $in: []\n              }\n            }\n          }\n        ]\n      }\n    };\n    tagsArray.forEach((tag) => {\n      filter.selector.$and[1]['model.tags'].$elemMatch.$in.push(tag);\n    });\n\n    return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);\n  }\n}\nexport default CouchSearchProvider;\n"
  },
  {
    "path": "src/plugins/persistence/couch/CouchStatusIndicator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @typedef {Object} IndicatorState\n * An object defining the visible state of the indicator.\n * @property {string} statusClass - The class to apply to the indicator.\n * @property {string} text - The text to display in the indicator.\n * @property {string} description - The description to display in the indicator.\n */\n\n/**\n * Set of CouchDB connection states; changes among these states will be\n * reflected in the indicator's appearance.\n * CONNECTED: Everything nominal, expect to be able to read/write.\n * DISCONNECTED: HTTP request failed (network error). Unable to reach server at all.\n * PENDING: Still trying to connect, and haven't failed yet.\n * MAINTENANCE: CouchDB is connected but not accepting requests.\n */\n\n/** @type {IndicatorState} */\nexport const CONNECTED = {\n  statusClass: 's-status-on',\n  text: 'CouchDB is connected',\n  description: 'CouchDB is online and accepting requests.'\n};\n/** @type {IndicatorState} */\nexport const PENDING = {\n  statusClass: 's-status-warning-lo',\n  text: 'Attempting to connect to CouchDB...',\n  description: 'Checking status of CouchDB, please stand by...'\n};\n/** @type {IndicatorState} */\nexport const DISCONNECTED = {\n  statusClass: 's-status-warning-hi',\n  text: 'CouchDB is offline',\n  description: 'CouchDB is offline and unavailable for requests.'\n};\n/** @type {IndicatorState} */\nexport const UNKNOWN = {\n  statusClass: 's-status-info',\n  text: 'CouchDB connectivity unknown',\n  description: 'CouchDB is in an unknown state of connectivity.'\n};\n\nexport default class CouchStatusIndicator {\n  constructor(simpleIndicator) {\n    this.indicator = simpleIndicator;\n    this.#setDefaults();\n  }\n\n  /**\n   * Set the default values for the indicator.\n   * @private\n   */\n  #setDefaults() {\n    this.setIndicatorToState(PENDING);\n  }\n\n  /**\n   * Set the indicator to the given state.\n   * @param {IndicatorState} state\n   */\n  setIndicatorToState(state) {\n    this.indicator.text(state.text);\n    this.indicator.description(state.description);\n    this.indicator.statusClass(state.statusClass);\n  }\n}\n"
  },
  {
    "path": "src/plugins/persistence/couch/README.md",
    "content": "\n# Installing CouchDB\n\n## Introduction\n\nThese instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:\n<https://docs.couchdb.org/en/main/intro/security.html>\n\n## Docker Quickstart\n\nThe following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.\n\nRequirement:\nGet docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)\n\n1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)\n2. Create and start the `couchdb` container:\n\n```sh\ndocker compose -f ./couchdb-compose.yaml up --detach\n```\n\n3. Copy `.env.ci` file to file named `.env.local`\n4. (Optional) Change the values of `.env.local` if desired\n5. Set the environment variables in bash by sourcing the env file\n\n```sh\nexport $(cat .env.local | xargs)\n```\n\n6. Execute the configuration script:\n\n```sh\nsh ./setup-couchdb.sh\n```\n\n7. `cd` to the workspace root directory (the same directory as `index.html`)\n8. Update `index.html` to use the CouchDB plugin as persistence store:\n\n```sh\nsh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh\n```\n\n9. ✅ Done!\n\nOpen MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.\n\n### Removing CouchDB Container completely\n\nTo completely remove the CouchDB container and volumes:\n\n```sh\ndocker stop couch-couchdb-1;docker rm couch-couchdb-1;docker volume rm couch_couchdb\n```\n\n## macOS\n\nWe highly recommend using the CouchDB `docker compose` method of installation, though it is still possible to install CouchDB through other means.\n\n### Installing CouchDB\n\n1. Install CouchDB using: `brew install couchdb`.\n2. Edit `/usr/local/etc/local.ini` and add the following settings:\n\n  ```ini\n  [admins]\n  admin = youradminpassword\n  ```\n\n  And set the server up for single node:\n\n  ```ini\n  [couchdb]\n  single_node=true\n  ```\n\n  Enable CORS\n\n  ```ini\n  [chttpd]\n  enable_cors = true\n  [cors]\n  origins = http://localhost:8080\n  ```\n\n### Installing CouchDB without admin privileges to your computer\n\nIf `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.\n\n1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.\n1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.\n\n## Other Operating Systems\n\nFollow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>\n\n# Configuring CouchDB\n\n## Configuration script\n\nThe simplest way to config a CouchDB instance is to use our provided tooling:\n\n1. Copy `.env.ci` file to file named `.env.local`\n2. Set the environment variables in bash by sourcing the env file\n\n```sh\nexport $(cat .env.local | xargs)\n```\n\n3. Execute the configuration script:\n\n```sh\nsh ./setup-couchdb.sh\n```\n\n## Manual Configuration\n\n1. Start CouchDB by running: `couchdb`.\n2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`\n3. Navigate to <http://localhost:5984/_utils>\n4. Create a database called `openmct`\n5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>\n6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.\n\n## Document Sizes\n\nCouchDB has size limits on both its internal documents, and its httpd interface. If dealing with larger documents in Open MCT (e.g., users adding images to notebook entries), you may to increase this limit. To do this, add the following to the two sections:\n\n```ini\n  [couchdb]\n  max_document_size = 4294967296 ; approx 4 GB\n  \n  [chttpd]\n  max_http_request_size = 4294967296 ; approx 4 GB\n```\n  \nIf not present, add them under proper sections. The values are in bytes, and can be adjusted to whatever is appropriate for your use case.\n\n# Configuring Open MCT to use CouchDB\n\n## Configuration script\n\nThe simplest way to config a CouchDB instance is to use our provided tooling:\n\n1. `cd` to the workspace root directory (the same directory as `index.html`)\n2. Update `index.html` to use the CouchDB plugin as persistence store:\n\n```sh\nsh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh\n```\n\n## Manual Configuration\n\n1. Edit `openmct/index.html` comment out the following line:\n\n  ```js\n  openmct.install(openmct.plugins.LocalStorage());\n  ```\n\n  Add a line to install the CouchDB plugin for Open MCT:\n\n  ```js\n  openmct.install(\n        openmct.plugins.CouchDB({\n          databases: [\n            {\n              url: 'http://localhost:5984/openmct',\n              namespace: '',\n              additionalNamespaces: [],\n              readOnly: false,\n              useDesignDocuments: false,\n              indicator: true\n            }\n          ]\n        })\n      );\n  ```\n\n### Configuration Options for OpenMCT\n\nWhen installing the CouchDB plugin for OpenMCT, you can specify a list of databases with configuration options for each. Here's a breakdown of the available options for each database:\n\n- `url`: The URL to the CouchDB instance, specifying the protocol, hostname, and port as needed.\n  - Example: `'http://localhost:5984/openmct'`\n\n- `namespace`: The namespace associated with this database.\n  - Example: `'openmct-sandbox'`\n\n- `additionalNamespaces`: Other namespaces that this plugin should respond to requests for.\n  - Example: `['apple-namespace', 'pear-namespace']`\n\n- `readOnly`: A boolean indicating whether the database should be treated as read-only. If set to `true`, OpenMCT will not attempt to write to this database.\n  - Example: `false`\n\n- `useDesignDocuments`: Indicates whether design documents should be used to speed up annotation search.\n  - Example: `false`\n\n- `indicator`: A boolean to specify whether an indicator should show the status of this CouchDB connection in the OpenMCT interface.\n  - Example: `true`\n\nNote: If using the `exampleTags` plugin with non-blank namespaces, you'll need to configure it point to a writable database. For example:\n\n```js\nopenmct.install(\n        openmct.plugins.example.ExampleTags({ namespaceToSaveAnnotations: 'openmct-sandbox' })\n      );\n```\n\nNote: If using the `MyItems` plugin, be sure to configure a root for each writable namespace. E.g., if you have two namespaces called `apple-namespace` and `pear-namespace`:\n\n```js\n      openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));\n      openmct.install(openmct.plugins.MyItems('Pear Items', 'pear-namespace'));\n```\n\nThis will create a root object with the id of `mine` in both namespaces upon load if not already created.\n\n# Validating a successful Installation\n\n1. Start Open MCT by running `npm start` in the `openmct` path.\n2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save.\n3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>\n4. Look at the 'JSON' tab and ensure you can see the specific object you created above.\n5. All done! 🏆\n\n# Maintenance\n\nOne can delete annotations by running inside this directory (i.e., `src/plugins/persistence/couch`):\n\n```sh\nnpm run deleteAnnotations:openmct:PIXEL_SPATIAL\n```\n\nwill delete all image tags.\n\n```sh\nnpm run deleteAnnotations:openmct \n```\n\nwill delete all tags.\n\n```sh\nnpm run deleteAnnotations:openmct -- --help\n```\n\nwill print help options.\n\n# Search Performance\n\nFor large Open MCT installations, it may be helpful to add additional CouchDB capabilities to bear to improve performance.\n\n## Indexing\n\nIndexing the `model.type` field in CouchDB can benefit the performance of queries significantly, particularly if there are a large number of documents in the database. An index can accelerate annotation searches by reducing the number of documents that the database needs to examine.\n\nTo create an index for `model.type`, you can use the following payload [using the API](https://docs.couchdb.org/en/stable/api/database/find.html#post--db-_index):\n\n```json\n{\n  \"index\": {\n    \"fields\": [\"model.type\", \"model.tags\"]\n  },\n  \"name\": \"type_tags_index\",\n  \"type\": \"json\"\n}\n```\n\nThis instructs CouchDB to create an index on the `model.type` field and the `model.tags` field. Once this index is created, queries that include a selector on `model.type` and `model.tags` (like when searching for tags) can use this index to retrieve results faster.\n\nYou can find more detailed information about indexing in CouchDB in the [official documentation](https://docs.couchdb.org/en/stable/api/database/find.html#db-index).\n\n## Design Documents\n\nWe can also add a design document [through the API](https://docs.couchdb.org/en/stable/api/ddoc/common.html#put--db-_design-ddoc) for retrieving domain objects for specific tags:\n\n```json\n{\n  \"_id\": \"_design/annotation_tags_index\",\n  \"views\": {\n    \"by_tags\": {\n      \"map\": \"function (doc) { if (doc.model && doc.model.type === 'annotation' && doc.model.tags) { doc.model.tags.forEach(function (tag) { emit(tag, doc._id); }); } }\"\n    }\n  }\n}\n```\n\nand can be retrieved by issuing a `GET` to <http://localhost:5984/openmct/_design/annotation_tags_index/_view/by_tags?keys=[\"TAG_ID_TO_SEARCH_FOR\"]&include_docs=true>\nwhere `TAG_ID_TO_SEARCH_FOR` is the tag UUID we're looking for.\n\nand for targets:\n\n```javascript\n{\n  \"_id\": \"_design/annotation_keystring_index\",\n  \"views\": {\n    \"by_keystring\": {\n      \"map\": \"function (doc) { if (doc.model && doc.model.type === 'annotation' && doc.model.targets) { doc.model.targets.forEach(function(target) { if(target.keyString) { emit(target.keyString, doc._id); } }); } }\"\n    }\n  }\n}\n```\n\nand can be retrieved by issuing a `GET` to <http://localhost:5984/openmct/_design/annotation_keystring_index/_view/by_keystring?keys=[\"KEY_STRING_TO_SEARCH_FOR\"]&include_docs=true>\nwhere `KEY_STRING_TO_SEARCH_FOR` is the UUID we're looking for.\n\nTo enable them in Open MCT, we need to configure the plugin `useDesignDocuments` like so:\n\n  ```js\n  openmct.install(openmct.plugins.CouchDB({url: \"http://localhost:5984/openmct\", useDesignDocuments: true}));\n  ```\n"
  },
  {
    "path": "src/plugins/persistence/couch/couchdb-compose.yaml",
    "content": "version: '3'\nservices:\n  couchdb:\n    image: couchdb:${COUCHDB_IMAGE_TAG:-3.3.2}\n    ports:\n      - '5984:5984'\n    volumes:\n      - couchdb:/opt/couchdb/data\n    environment:\n      COUCHDB_USER: admin\n      COUCHDB_PASSWORD: password\nvolumes:\n  couchdb:\n"
  },
  {
    "path": "src/plugins/persistence/couch/package.json",
    "content": "{\n    \"name\": \"openmct-couch-plugin\",\n    \"version\": \"1.0.0\",\n    \"description\": \"CouchDB persistence plugin for Open MCT\",\n    \"dependencies\": {\n        \"@cloudant/couchbackup\": \"2.9.9\"\n    },\n    \"scripts\": {\n        \"backup:openmct\": \"npx couchbackup -u http://admin:password@127.0.0.1:5984/ -d openmct -o openmct-couch-backup.txt\",\n        \"restore:openmct\": \"cat openmct-couch-backup.txt | npx couchrestore -u http://admin:password@127.0.0.1:5984/ -d openmct\",\n        \"deleteAnnotations:openmct\": \"node scripts/deleteAnnotations.js $*\",\n        \"deleteAnnotations:openmct:NOTEBOOK\": \"node scripts/deleteAnnotations.js -- --annotationType NOTEBOOK\",\n        \"deleteAnnotations:openmct:GEOSPATIAL\": \"node scripts/deleteAnnotations.js -- --annotationType GEOSPATIAL\",\n        \"deleteAnnotations:openmct:PIXEL_SPATIAL\": \"node scripts/deleteAnnotations.js -- --annotationType PIXEL_SPATIAL\",\n        \"deleteAnnotations:openmct:TEMPORAL\": \"node scripts/deleteAnnotations.js -- --annotationType TEMPORAL\",\n        \"deleteAnnotations:openmct:PLOT_SPATIAL\": \"node scripts/deleteAnnotations.js -- --annotationType PLOT_SPATIAL\"\n    }\n}"
  },
  {
    "path": "src/plugins/persistence/couch/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport CouchObjectProvider from './CouchObjectProvider.js';\nimport CouchSearchProvider from './CouchSearchProvider.js';\nimport CouchStatusIndicator from './CouchStatusIndicator.js';\n\nconst DEFAULT_NAMESPACE = '';\nconst LEGACY_SPACE = 'mct';\n\nexport default function CouchPlugin(options) {\n  function normalizeOptions(unnormalizedOptions) {\n    const normalizedOptions = {};\n    if (typeof unnormalizedOptions === 'string') {\n      normalizedOptions.databases = [\n        {\n          url: options,\n          namespace: DEFAULT_NAMESPACE,\n          additionalNamespaces: [LEGACY_SPACE],\n          readOnly: false,\n          useDesignDocuments: false,\n          indicator: true\n        }\n      ];\n    } else if (!unnormalizedOptions.databases) {\n      normalizedOptions.databases = [\n        {\n          url: unnormalizedOptions.url,\n          namespace: DEFAULT_NAMESPACE,\n          additionalNamespaces: [LEGACY_SPACE],\n          readOnly: false,\n          useDesignDocuments: unnormalizedOptions.useDesignDocuments,\n          indicator: true\n        }\n      ];\n    } else {\n      normalizedOptions.databases = unnormalizedOptions.databases;\n    }\n\n    // final sanity check, ensure we have all options\n    normalizedOptions.databases.forEach((databaseConfiguration) => {\n      if (!databaseConfiguration.url) {\n        throw new Error(\n          `🛑 CouchDB plugin requires a url option. Please check the configuration for namespace ${databaseConfiguration.namespace}`\n        );\n      } else if (databaseConfiguration.namespace === undefined) {\n        // note we can't check for just !databaseConfiguration.namespace because it could be an empty string\n        throw new Error(\n          `🛑 CouchDB plugin requires a namespace option. Please check the configuration for url ${databaseConfiguration.url}`\n        );\n      }\n    });\n\n    return normalizedOptions;\n  }\n\n  return function install(openmct) {\n    const normalizedOptions = normalizeOptions(options);\n    normalizedOptions.databases.forEach((databaseConfiguration) => {\n      let couchStatusIndicator;\n      if (databaseConfiguration.indicator) {\n        const simpleIndicator = openmct.indicators.simpleIndicator();\n        openmct.indicators.add(simpleIndicator);\n        couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);\n      }\n      // the provider is added to the install function to expose couchProvider to unit tests\n      install.couchProvider = new CouchObjectProvider({\n        openmct,\n        databaseConfiguration,\n        couchStatusIndicator\n      });\n      openmct.objects.addProvider(databaseConfiguration.namespace, install.couchProvider);\n      databaseConfiguration.additionalNamespaces?.forEach((additionalNamespace) => {\n        openmct.objects.addProvider(additionalNamespace, install.couchProvider);\n      });\n\n      // need one search provider for whole couch database\n      const searchOnlyNamespace = `COUCH_SEARCH_${databaseConfiguration.namespace}${Date.now()}`;\n      openmct.objects.addProvider(\n        searchOnlyNamespace,\n        new CouchSearchProvider(install.couchProvider)\n      );\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/persistence/couch/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator.js';\nimport CouchPlugin from './plugin.js';\n\ndescribe('the plugin', () => {\n  let openmct;\n  let provider;\n  let testPath = 'http://localhost:9990/openmct';\n  let options;\n  let mockIdentifierService;\n  let mockDomainObject;\n\n  beforeEach((done) => {\n    spyOnBuiltins(['fetch'], window);\n\n    mockDomainObject = {\n      identifier: {\n        namespace: '',\n        key: 'some-value'\n      },\n      type: 'notebook',\n      modified: 0\n    };\n    options = {\n      url: testPath,\n      filter: {}\n    };\n    openmct = createOpenMct();\n\n    openmct.$injector = jasmine.createSpyObj('$injector', ['get']);\n    mockIdentifierService = jasmine.createSpyObj('identifierService', ['parse']);\n    mockIdentifierService.parse.and.returnValue({\n      getSpace: () => {\n        return 'mct';\n      }\n    });\n\n    openmct.$injector.get.and.returnValue(mockIdentifierService);\n\n    openmct.install(new CouchPlugin(options));\n\n    openmct.types.addType('notebook', { creatable: true });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    provider = openmct.objects.getProvider(mockDomainObject.identifier);\n    spyOn(provider, 'get').and.callThrough();\n    spyOn(provider, 'create').and.callThrough();\n    spyOn(provider, 'update').and.callThrough();\n    spyOn(provider, 'observe').and.callThrough();\n    spyOn(provider, 'fetchChanges').and.callThrough();\n    spyOn(provider, 'onSharedWorkerMessage').and.callThrough();\n    spyOn(provider, 'onEventMessage').and.callThrough();\n    spyOn(provider, 'isObservingObjectChanges').and.callThrough();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the provider', () => {\n    let mockPromise;\n    beforeEach(() => {\n      mockPromise = Promise.resolve({\n        json: () => {\n          return {\n            ok: true,\n            _id: 'some-value',\n            id: 'some-value',\n            _rev: 1,\n            model: {}\n          };\n        }\n      });\n      fetch.and.returnValue(mockPromise);\n    });\n\n    it('gets an object', async () => {\n      const result = await openmct.objects.get(mockDomainObject.identifier);\n      expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);\n    });\n\n    it('prioritizes couch requests above other requests', async () => {\n      await openmct.objects.get(mockDomainObject.identifier);\n      const fetchOptions = fetch.calls.mostRecent().args[1];\n      expect(fetchOptions.priority).toEqual('high');\n    });\n\n    it('creates an object and starts shared worker', async () => {\n      const result = await openmct.objects.save(mockDomainObject);\n      expect(provider.create).toHaveBeenCalled();\n      expect(provider.observe).toHaveBeenCalled();\n      expect(result).toBeTrue();\n    });\n\n    it('updates an object', (done) => {\n      openmct.objects.save(mockDomainObject).then((result) => {\n        expect(result).toBeTrue();\n        expect(provider.create).toHaveBeenCalled();\n        //Set modified timestamp it detects a change and persists the updated model.\n        mockDomainObject.modified = mockDomainObject.persisted + 1;\n        openmct.objects.save(mockDomainObject).then((updatedResult) => {\n          expect(updatedResult).toBeTrue();\n          expect(provider.update).toHaveBeenCalled();\n          done();\n        });\n      });\n    });\n\n    it('works without Shared Workers', async () => {\n      let sharedWorkerCallback;\n      const cachedSharedWorker = window.SharedWorker;\n      window.SharedWorker = undefined;\n\n      const mockEventSource = {\n        addEventListener: (topic, addedListener) => {\n          sharedWorkerCallback = addedListener;\n        },\n        removeEventListener: () => {\n          sharedWorkerCallback = null;\n        }\n      };\n      const cachedEventSource = window.EventSource;\n\n      window.EventSource = function (url) {\n        return mockEventSource;\n      };\n\n      mockDomainObject.id = mockDomainObject.identifier.key;\n\n      const fakeUpdateEvent = {\n        data: JSON.stringify(mockDomainObject),\n        target: {\n          readyState: EventSource.CONNECTED\n        }\n      };\n\n      // eslint-disable-next-line require-await\n      provider.request = async function (subPath, method, body, signal) {\n        return {\n          body: fakeUpdateEvent,\n          ok: true,\n          id: mockDomainObject.id,\n          rev: 5\n        };\n      };\n\n      const result = await openmct.objects.save(mockDomainObject);\n      expect(result).toBeTrue();\n      expect(provider.create).toHaveBeenCalled();\n      expect(provider.observe).toHaveBeenCalled();\n      expect(provider.isObservingObjectChanges).toHaveBeenCalled();\n\n      //Set modified timestamp it detects a change and persists the updated model.\n      mockDomainObject.modified = mockDomainObject.persisted + 1;\n      const updatedResult = await openmct.objects.save(mockDomainObject);\n      openmct.objects.observe(mockDomainObject, '*', (updatedObject) => {});\n\n      expect(updatedResult).toBeTrue();\n      expect(provider.update).toHaveBeenCalled();\n      expect(provider.fetchChanges).toHaveBeenCalled();\n      expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);\n      sharedWorkerCallback(fakeUpdateEvent);\n      expect(provider.onEventMessage).toHaveBeenCalled();\n\n      window.SharedWorker = cachedSharedWorker;\n      window.EventSource = cachedEventSource;\n    });\n  });\n  describe('batches requests', () => {\n    let mockPromise;\n    beforeEach(() => {\n      mockPromise = Promise.resolve({\n        json: () => {\n          return {\n            total_rows: 0,\n            rows: []\n          };\n        }\n      });\n      fetch.and.returnValue(mockPromise);\n    });\n    it('for multiple simultaneous gets', async () => {\n      const objectIds = [\n        {\n          namespace: '',\n          key: 'object-1'\n        },\n        {\n          namespace: '',\n          key: 'object-2'\n        },\n        {\n          namespace: '',\n          key: 'object-3'\n        }\n      ];\n\n      await Promise.all(objectIds.map((identifier) => openmct.objects.get(identifier)));\n\n      const requestUrl = fetch.calls.mostRecent().args[0];\n      const requestMethod = fetch.calls.mostRecent().args[1].method;\n      expect(fetch).toHaveBeenCalledTimes(1);\n      expect(requestUrl.includes('_all_docs')).toBeTrue();\n      expect(requestMethod).toEqual('POST');\n    });\n\n    it('but not for single gets', async () => {\n      const objectId = {\n        namespace: '',\n        key: 'object-1'\n      };\n\n      await openmct.objects.get(objectId);\n      const requestUrl = fetch.calls.mostRecent().args[0];\n      const requestMethod = fetch.calls.mostRecent().args[1].method;\n\n      expect(fetch).toHaveBeenCalledTimes(1);\n      expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();\n      expect(requestMethod).toEqual('GET');\n    });\n  });\n  describe('batches persistence', () => {\n    let successfulMockPromise;\n    let partialFailureMockPromise;\n    let objectsToPersist;\n\n    beforeEach(() => {\n      successfulMockPromise = Promise.resolve({\n        json: () => {\n          return [\n            {\n              id: 'object-1',\n              ok: true\n            },\n            {\n              id: 'object-2',\n              ok: true\n            },\n            {\n              id: 'object-3',\n              ok: true\n            }\n          ];\n        }\n      });\n\n      partialFailureMockPromise = Promise.resolve({\n        json: () => {\n          return [\n            {\n              id: 'object-1',\n              ok: true\n            },\n            {\n              id: 'object-2',\n              ok: false\n            },\n            {\n              id: 'object-3',\n              ok: true\n            }\n          ];\n        }\n      });\n\n      objectsToPersist = [\n        {\n          identifier: {\n            namespace: '',\n            key: 'object-1'\n          },\n          name: 'object-1',\n          type: 'folder',\n          modified: 0\n        },\n        {\n          identifier: {\n            namespace: '',\n            key: 'object-2'\n          },\n          name: 'object-2',\n          type: 'folder',\n          modified: 0\n        },\n        {\n          identifier: {\n            namespace: '',\n            key: 'object-3'\n          },\n          name: 'object-3',\n          type: 'folder',\n          modified: 0\n        }\n      ];\n    });\n    it('for multiple simultaneous successful saves', async () => {\n      fetch.and.returnValue(successfulMockPromise);\n\n      await Promise.all(\n        objectsToPersist.map((objectToPersist) => openmct.objects.save(objectToPersist))\n      );\n\n      const requestUrl = fetch.calls.mostRecent().args[0];\n      const requestMethod = fetch.calls.mostRecent().args[1].method;\n      const requestBody = JSON.parse(fetch.calls.mostRecent().args[1].body);\n\n      expect(fetch).toHaveBeenCalledTimes(1);\n      expect(requestUrl.includes('_bulk_docs')).toBeTrue();\n      expect(requestMethod).toEqual('POST');\n      expect(\n        objectsToPersist.every(\n          (object, index) => object.identifier.key === requestBody.docs[index]._id\n        )\n      ).toBeTrue();\n    });\n    it('for multiple simultaneous saves with partial failure', async () => {\n      fetch.and.returnValue(partialFailureMockPromise);\n\n      let saveResults = await Promise.all(\n        objectsToPersist.map((objectToPersist) =>\n          openmct.objects\n            .save(objectToPersist)\n            .then(() => true)\n            .catch(() => false)\n        )\n      );\n      expect(saveResults[0]).toBeTrue();\n      expect(saveResults[1]).toBeFalse();\n      expect(saveResults[2]).toBeTrue();\n    });\n    it('except for a single save', async () => {\n      fetch.and.returnValue({\n        json: () => {\n          return {\n            id: 'object-1',\n            ok: true\n          };\n        }\n      });\n      await openmct.objects.save(objectsToPersist[0]);\n\n      const requestUrl = fetch.calls.mostRecent().args[0];\n      const requestMethod = fetch.calls.mostRecent().args[1].method;\n\n      expect(fetch).toHaveBeenCalledTimes(1);\n      expect(requestUrl.includes('_bulk_docs')).toBeFalse();\n      expect(requestUrl.endsWith('object-1')).toBeTrue();\n      expect(requestMethod).toEqual('PUT');\n    });\n  });\n});\n\ndescribe('the view', () => {\n  let openmct;\n  let options;\n  let appHolder;\n  let testPath = 'http://localhost:9990/openmct';\n  let provider;\n  let mockDomainObject;\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    spyOnBuiltins(['fetch'], window);\n    options = {\n      url: testPath,\n      filter: {}\n    };\n    mockDomainObject = {\n      identifier: {\n        namespace: '',\n        key: 'some-value'\n      },\n      type: 'notebook',\n      modified: 0\n    };\n    openmct.install(new CouchPlugin(options));\n    appHolder = document.createElement('div');\n    document.body.appendChild(appHolder);\n    openmct.on('start', done);\n    openmct.start(appHolder);\n    provider = openmct.objects.getProvider(mockDomainObject.identifier);\n    spyOn(provider, 'onSharedWorkerMessage').and.callThrough();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('updates CouchDB status indicator', () => {\n    let mockPromise;\n\n    function assertCouchIndicatorStatus(status) {\n      const indicator = appHolder.querySelector('.c-indicator--simple');\n      expect(indicator).not.toBeNull();\n      expect(indicator).toHaveClass(status.statusClass);\n      expect(indicator.textContent).toMatch(new RegExp(status.text, 'i'));\n      expect(indicator.title).toMatch(new RegExp(status.title, 'i'));\n    }\n\n    it(\"to 'connected' on successful request\", async () => {\n      mockPromise = Promise.resolve({\n        status: 200,\n        json: () => {\n          return {\n            ok: true,\n            _id: 'some-value',\n            id: 'some-value',\n            _rev: 1,\n            model: {}\n          };\n        }\n      });\n      fetch.and.returnValue(mockPromise);\n\n      await openmct.objects.get({\n        namespace: '',\n        key: 'object-1'\n      });\n      await nextTick();\n\n      assertCouchIndicatorStatus(CONNECTED);\n    });\n\n    it(\"to 'disconnected' on failed request\", async () => {\n      fetch.and.throwError(new TypeError('ERR_CONNECTION_REFUSED'));\n\n      await openmct.objects.get({\n        namespace: '',\n        key: 'object-1'\n      });\n      await nextTick();\n\n      assertCouchIndicatorStatus(DISCONNECTED);\n    });\n\n    it(\"to 'pending'\", async () => {\n      const workerMessage = {\n        data: {\n          type: 'state',\n          state: 'pending'\n        }\n      };\n      mockPromise = Promise.resolve({\n        status: 200,\n        json: () => {\n          return {\n            ok: true,\n            _id: 'some-value',\n            id: 'some-value',\n            _rev: 1,\n            model: {}\n          };\n        }\n      });\n      fetch.and.returnValue(mockPromise);\n\n      await openmct.objects.get({\n        namespace: '',\n        key: 'object-1'\n      });\n\n      // Simulate 'pending' state from worker message\n      provider.onSharedWorkerMessage(workerMessage);\n      await nextTick();\n\n      assertCouchIndicatorStatus(PENDING);\n    });\n\n    it(\"to 'unknown'\", async () => {\n      const workerMessage = {\n        data: {\n          type: 'state',\n          state: 'unknown'\n        }\n      };\n      mockPromise = Promise.resolve({\n        status: 200,\n        json: () => {\n          return {\n            ok: true,\n            _id: 'some-value',\n            id: 'some-value',\n            _rev: 1,\n            model: {}\n          };\n        }\n      });\n      fetch.and.returnValue(mockPromise);\n\n      await openmct.objects.get({\n        namespace: '',\n        key: 'object-1'\n      });\n\n      // Simulate 'pending' state from worker message\n      provider.onSharedWorkerMessage(workerMessage);\n      await nextTick();\n\n      assertCouchIndicatorStatus(UNKNOWN);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh",
    "content": "#!/bin/bash -e\n\nsed -i'.bak' -e 's/LocalStorage()/CouchDB(\\{url: \"http:\\/\\/localhost:5984\\/openmct\", useDesignDocuments: true\\})/g' index.html\n"
  },
  {
    "path": "src/plugins/persistence/couch/scripts/deleteAnnotations.js",
    "content": "#!/usr/bin/env node\n\n/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nconst process = require('process');\n\nasync function main() {\n  try {\n    const { annotationType, serverUrl, databaseName, helpRequested, username, password } =\n      processArguments();\n    if (helpRequested) {\n      return;\n    }\n    const docsToDelete = await gatherDocumentsForDeletion({\n      serverUrl,\n      databaseName,\n      annotationType,\n      username,\n      password\n    });\n    const deletedDocumentCount = await performBulkDelete({\n      docsToDelete,\n      serverUrl,\n      databaseName,\n      username,\n      password\n    });\n    console.log(\n      `Deleted ${deletedDocumentCount} document${deletedDocumentCount === 1 ? '' : 's'}.`\n    );\n  } catch (error) {\n    console.error(`Error: ${error.message}`);\n  }\n}\n\nconst ANNOTATION_TYPES = Object.freeze({\n  NOTEBOOK: 'NOTEBOOK',\n  GEOSPATIAL: 'GEOSPATIAL',\n  PIXEL_SPATIAL: 'PIXEL_SPATIAL',\n  TEMPORAL: 'TEMPORAL',\n  PLOT_SPATIAL: 'PLOT_SPATIAL'\n});\n\nfunction processArguments() {\n  const args = process.argv.slice(2);\n  let annotationType;\n  let databaseName = 'openmct'; // default db name to \"openmct\"\n  let serverUrl = new URL('http://127.0.0.1:5984'); // default db name to \"openmct\"\n  let helpRequested = false;\n\n  args.forEach((val, index) => {\n    switch (val) {\n      case '--help':\n        console.log(\n          'Usage: deleteAnnotations.js [--annotationType type] [--dbName name] <CouchDB URL> \\nFor authentication, set the environment variables COUCHDB_USERNAME and COUCHDB_PASSWORD. \\n'\n        );\n        console.log('Annotation types: ', Object.keys(ANNOTATION_TYPES).join(', '));\n        helpRequested = true;\n        break;\n      case '--annotationType':\n        annotationType = args[index + 1];\n        if (!Object.values(ANNOTATION_TYPES).includes(annotationType)) {\n          throw new Error(`Invalid annotation type: ${annotationType}`);\n        }\n        break;\n      case '--dbName':\n        databaseName = args[index + 1];\n        break;\n      case '--serverUrl':\n        serverUrl = new URL(args[index + 1]);\n        break;\n    }\n  });\n\n  let username = process.env.COUCHDB_USERNAME || '';\n  let password = process.env.COUCHDB_PASSWORD || '';\n\n  return {\n    annotationType,\n    serverUrl,\n    databaseName,\n    helpRequested,\n    username,\n    password\n  };\n}\n\nasync function gatherDocumentsForDeletion({\n  serverUrl,\n  databaseName,\n  annotationType,\n  username,\n  password\n}) {\n  const baseUrl = `${serverUrl.href}${databaseName}/_find`;\n  let bookmark = null;\n  let docsToDelete = [];\n  let hasMoreDocs = true;\n\n  const body = {\n    selector: {\n      _id: { $gt: null },\n      'model.type': 'annotation'\n    },\n    fields: ['_id', '_rev'],\n    limit: 1000\n  };\n\n  if (annotationType !== undefined) {\n    body.selector['model.annotationType'] = annotationType;\n  }\n\n  const findOptions = {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify(body)\n  };\n\n  if (username && password) {\n    findOptions.headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;\n  }\n\n  while (hasMoreDocs) {\n    if (bookmark) {\n      body.bookmark = bookmark;\n    }\n\n    const res = await fetch(baseUrl, findOptions);\n\n    if (!res.ok) {\n      throw new Error(`Server responded with status: ${res.status}`);\n    }\n\n    const findResult = await res.json();\n\n    bookmark = findResult.bookmark;\n    docsToDelete = [...docsToDelete, ...findResult.docs];\n\n    // check if we got less than limit, set hasMoreDocs to false\n    hasMoreDocs = findResult.docs.length === body.limit;\n  }\n\n  return docsToDelete;\n}\n\nasync function performBulkDelete({ docsToDelete, serverUrl, databaseName, username, password }) {\n  docsToDelete.forEach((doc) => (doc._deleted = true));\n\n  const deleteOptions = {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({ docs: docsToDelete })\n  };\n\n  if (username && password) {\n    deleteOptions.headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;\n  }\n\n  const response = await fetch(`${serverUrl.href}${databaseName}/_bulk_docs`, deleteOptions);\n  if (!response.ok) {\n    throw new Error('Failed with status code: ' + response.status);\n  }\n\n  return docsToDelete.length;\n}\n\nmain();\n"
  },
  {
    "path": "src/plugins/persistence/couch/scripts/lockObjects.mjs",
    "content": "import http from 'http';\nimport nano from 'nano';\nimport { parseArgs } from 'util';\n\nconst COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984';\nconst COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct';\n\nconst {\n  values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass }\n} = parseArgs({\n  options: {\n    couchUrl: {\n      type: 'string',\n      default: COUCH_URL\n    },\n    database: {\n      type: 'string',\n      short: 'd',\n      default: COUCH_DB_NAME\n    },\n    lock: {\n      type: 'boolean',\n      short: 'l'\n    },\n    unlock: {\n      type: 'boolean',\n      short: 'u'\n    },\n    startObjectKeystring: {\n      type: 'string',\n      short: 'o',\n      default: 'mine'\n    },\n    user: {\n      type: 'string'\n    },\n    pass: {\n      type: 'string'\n    }\n  }\n});\n\nconst BATCH_SIZE = 100;\nconst SOCKET_POOL_SIZE = 100;\n\nconst locked = lock === true;\nconsole.info(`Connecting to ${couchUrl}/${database}`);\nconsole.info(`${locked ? 'Locking' : 'Unlocking'} all children of ${startObjectKeystring}`);\n\nconst poolingAgent = new http.Agent({\n  keepAlive: true,\n  maxSockets: SOCKET_POOL_SIZE\n});\n\nconst db = nano({\n  url: couchUrl,\n  requestDefaults: {\n    agent: poolingAgent\n  }\n}).use(database);\ndb.auth(user, pass);\n\nif (!unlock && !lock) {\n  throw new Error('Either -l or -u option is required');\n}\n\nconst startObjectIdentifier = keystringToIdentifier(startObjectKeystring);\nconst documentBatch = [];\nconst alreadySeen = new Set();\nlet updatedDocumentCount = 0;\n\nawait processObjectTreeFrom(startObjectIdentifier);\n//Persist final batch\nawait persistBatch();\nconsole.log(`Processed ${updatedDocumentCount} documents`);\n\nfunction processObjectTreeFrom(parentObjectIdentifier) {\n  //1. Fetch document for identifier;\n  return fetchDocument(parentObjectIdentifier)\n    .then(async (document) => {\n      if (document !== undefined) {\n        if (!alreadySeen.has(document._id)) {\n          alreadySeen.add(document._id);\n          //2. Lock or unlock object\n          document.model.locked = locked;\n          document.model.disallowUnlock = locked;\n\n          if (locked) {\n            document.model.lockedBy = 'script';\n          } else {\n            delete document.model.lockedBy;\n          }\n          //3. Push document to a batch\n          documentBatch.push(document);\n          //4. Persist batch if necessary, reporting failures\n          await persistBatchIfNeeded();\n          //5. Repeat for each composee\n          const composition = document.model.composition || [];\n          return Promise.all(\n            composition.map((composee) => {\n              return processObjectTreeFrom(composee);\n            })\n          );\n        }\n      }\n    })\n    .catch((error) => {\n      console.log(`Error ${error}`);\n    });\n}\n\nasync function fetchDocument(identifierOrKeystring) {\n  let keystring;\n  if (typeof identifierOrKeystring === 'object') {\n    keystring = identifierToKeystring(identifierOrKeystring);\n  } else {\n    keystring = identifierOrKeystring;\n  }\n\n  try {\n    const document = await db.get(keystring);\n\n    return document;\n  } catch (error) {\n    return undefined;\n  }\n}\n\nfunction persistBatchIfNeeded() {\n  if (documentBatch.length >= BATCH_SIZE) {\n    return persistBatch();\n  } else {\n    //Noop - batch is not big enough yet\n    return;\n  }\n}\n\nasync function persistBatch() {\n  try {\n    const localBatch = [].concat(documentBatch);\n\n    //Immediately clear the shared batch array. This asynchronous process is non-blocking, and\n    //we don't want to try and persist the same batch multiple times while we are waiting for\n    //the subsequent bulk operation to complete.\n    updatedDocumentCount += documentBatch.length;\n\n    documentBatch.splice(0, documentBatch.length);\n    const response = await db.bulk({ docs: localBatch });\n\n    if (response instanceof Array) {\n      response.forEach((r) => {\n        console.info(JSON.stringify(r));\n      });\n    } else {\n      console.info(JSON.stringify(response));\n    }\n  } catch (error) {\n    if (error instanceof Array) {\n      error.forEach((e) => console.error(JSON.stringify(e)));\n    } else {\n      console.error(`${error.statusCode} - ${error.reason}`);\n    }\n  }\n}\n\nfunction keystringToIdentifier(keystring) {\n  const tokens = keystring.split(':');\n  if (tokens.length === 2) {\n    return {\n      namespace: tokens[0],\n      key: tokens[1]\n    };\n  } else {\n    return {\n      namespace: '',\n      key: tokens[0]\n    };\n  }\n}\n\nfunction identifierToKeystring(identifier) {\n  if (typeof identifier === 'string') {\n    return identifier;\n  } else if (typeof identifier === 'object') {\n    if (identifier.namespace) {\n      return `${identifier.namespace}:${identifier.key}`;\n    } else {\n      return identifier.key;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/persistence/couch/setup-couchdb.sh",
    "content": "#!/bin/bash -e\n\n# Check if required environment variables have values, exit if not.\ncheck_env_var() {\n    if [ -z \"$1\" ]; then\n        echo \"$2 has no value\" 1>&2\n        exit 1\n    fi\n}\n\ncheck_env_var \"${OPENMCT_DATABASE_NAME}\" \"OPENMCT_DATABASE_NAME\"\ncheck_env_var \"${COUCH_ADMIN_USER}\" \"COUCH_ADMIN_USER\"\ncheck_env_var \"${COUCH_BASE_LOCAL}\" \"COUCH_BASE_LOCAL\"\n\n# Construct curl's -u option value based on COUCH_ADMIN_USER and COUCH_ADMIN_PASSWORD environment variables.\nCURL_USERPASS_ARG=\"${COUCH_ADMIN_USER}\"\nif [ \"${COUCH_ADMIN_PASSWORD}\" ]; then\n    CURL_USERPASS_ARG+=\":${COUCH_ADMIN_PASSWORD}\"\nfi\n\nresource_exists() {\n    response=$(curl -u \"${CURL_USERPASS_ARG}\" -s -o /dev/null -I -w \"%{http_code}\" $1);\n    if [ \"200\" == \"${response}\" ]; then\n        echo \"TRUE\"\n    else\n        echo \"FALSE\";\n    fi\n}\n\ndb_exists() {\n    resource_exists \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"\n}\n\ncreate_db() {\n    response=$(curl -su \"${CURL_USERPASS_ARG}\" -XPUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\");\n    echo \"$response\"\n}\n\nadmin_user_exists() {\n    response=$(curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -I -w \"%{http_code}\" \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/admins/\"$COUCH_ADMIN_USER\");\n    if [ \"200\" == \"${response}\" ]; then\n        echo \"TRUE\"\n    else\n        echo \"FALSE\";\n    fi\n}\n\ncreate_admin_user() {\n    echo Creating admin user\n    curl -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/admins/\"$COUCH_ADMIN_USER\" -d \\'\"$COUCH_ADMIN_PASSWORD\"\\'\n}\n\nis_cors_enabled() {\n    resource_exists \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/httpd/enable_cors\n}\n\nenable_cors() {\n    curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/httpd/enable_cors -d '\"true\"'\n    curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/cors/origins -d '\"*\"'\n    curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/cors/credentials -d '\"true\"'\n    curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/cors/methods -d '\"GET, PUT, POST, HEAD, DELETE\"'\n    curl -su \"${CURL_USERPASS_ARG}\" -o /dev/null -X PUT \"$COUCH_BASE_LOCAL\"/_node/\"$COUCH_NODE_NAME\"/_config/cors/headers -d '\"accept, authorization, content-type, origin, referer, x-csrf-token\"'\n}\n\nupdate_db_permissions() {\n    local db_name=$1\n    echo \"Updating ${db_name} database permissions\"\n    response=$(curl -su \"${CURL_USERPASS_ARG}\" --location \\\n        --request PUT \"$COUCH_BASE_LOCAL\"/\"$db_name\"/_security \\\n        --header 'Content-Type: application/json' \\\n        --data-raw '{ \"admins\": {\"roles\": []},\"members\": {\"roles\": []}}')\n    if [ \"{\\\"ok\\\":true}\" == \"${response}\" ]; then\n        echo \"Database permissions successfully updated\"\n    else\n        echo \"Database permissions not updated\"\n    fi\n}\n\ncreate_users_table() {\n    echo \"Creating _users database\"\n    response=$(curl -su \"${CURL_USERPASS_ARG}\" -XPUT \"$COUCH_BASE_LOCAL\"/_users)\n    if [ \"{\\\"ok\\\":true}\" == \"${response}\" ]; then\n        echo \"Successfully created _users database\"\n    else\n        echo \"Unable to create _users database\"\n    fi\n}\n\ncreate_replicator_table() {\n    echo \"Creating _replicator database\"\n    response=$(curl -su \"${CURL_USERPASS_ARG}\" -XPUT \"$COUCH_BASE_LOCAL\"/_replicator)\n    if [ \"{\\\"ok\\\":true}\" == \"${response}\" ]; then\n        echo \"Successfully created _replicator database\"\n    else\n        echo \"Unable to create _replicator database\"\n    fi\n}\n\nadd_index_and_views() {\n    echo \"Adding index and views to $OPENMCT_DATABASE_NAME database\"\n\n    # Add object names search index\n    response=$(curl --silent --user \"${CURL_USERPASS_ARG}\" --request PUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_design/object_names/\\\n    --header 'Content-Type: application/json' \\\n    --data '{\n      \"_id\":\"_design/object_names\",\n      \"views\":{\n        \"object_names\":{\n          \"map\":\"function(doc) { if (doc.model && doc.model.name) { const name = doc.model.name.toLowerCase().trim(); if (name.length > 0) { emit(name, doc._id); const tokens = name.split(/[^a-zA-Z0-9]/); tokens.forEach((token) => { if (token.length > 0) { emit(token, doc._id); } }); } } }\"\n        }\n      }\n    }');\n\n    if [[ $response =~ \"\\\"ok\\\":true\" ]]; then\n        echo \"Successfully created object_names\"\n    elif [[ $response =~ \"\\\"error\\\":\\\"conflict\\\"\" ]]; then\n        echo \"object_names already exists, skipping creation\"\n    else\n        echo \"Unable to create object_names\"\n        echo $response\n    fi\n\n    # Add object types search index\n    response=$(curl --silent --user \"${CURL_USERPASS_ARG}\" --request PUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_design/object_types/\\\n    --header 'Content-Type: application/json' \\\n    --data '{\n      \"_id\":\"_design/object_types\",\n      \"views\":{\n        \"object_types\":{\n          \"map\":\"function(doc) { if (doc.model && doc.model.type) { const type = doc.model.type.toLowerCase().trim(); if (type.length > 0) { emit(type, null); } } }\"\n        }\n      }\n    }')\n\n    if [[ $response =~ \"\\\"ok\\\":true\" ]]; then\n        echo \"Successfully created object_types\"\n    elif [[ $response =~ \"\\\"error\\\":\\\"conflict\\\"\" ]]; then\n        echo \"object_types already exists, skipping creation\"\n    else\n        echo \"Unable to create object_types\"\n        echo $response\n    fi\n\n    # Add type_tags_index\n    response=$(curl --silent --user \"${CURL_USERPASS_ARG}\" --request POST \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_index/\\\n    --header 'Content-Type: application/json' \\\n    --data '{\n        \"index\": {\n            \"fields\": [\"model.type\", \"model.tags\"]\n        },\n        \"name\": \"type_tags_index\",\n        \"type\": \"json\"\n    }')\n\n    if [[ $response =~ \"\\\"result\\\":\\\"created\\\"\" ]]; then\n        echo \"Successfully created type_tags_index\"\n    elif [[ $response =~ \"\\\"result\\\":\\\"exists\\\"\" ]]; then\n        echo \"type_tags_index already exists, skipping creation\"\n    else\n        echo \"Unable to create type_tags_index\"\n        echo $response\n    fi\n\n    # Add annotation_tags_index\n    response=$(curl  --silent --user \"${CURL_USERPASS_ARG}\" --request PUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_design/annotation_tags_index \\\n    --header 'Content-Type: application/json' \\\n    --data '{\n        \"_id\": \"_design/annotation_tags_index\",\n        \"views\": {\n            \"by_tags\": {\n                \"map\": \"function (doc) { if (doc.model && doc.model.type === '\\''annotation'\\'' && doc.model.tags) { doc.model.tags.forEach(function (tag) { emit(tag, doc._id); }); } }\"\n            }\n        }\n    }')\n\n    if [[ $response =~ \"\\\"ok\\\":true\" ]]; then\n        echo \"Successfully created annotation_tags_index\"\n    elif [[ $response =~ \"\\\"error\\\":\\\"conflict\\\"\" ]]; then\n        echo \"annotation_tags_index already exists, skipping creation\"\n    else\n        echo \"Unable to create annotation_tags_index\"\n        echo $response\n    fi\n\n    # Add annotation_keystring_index\n    response=$(curl --silent --user \"${CURL_USERPASS_ARG}\" --request PUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_design/annotation_keystring_index \\\n    --header 'Content-Type: application/json' \\\n    --data '{\n        \"_id\": \"_design/annotation_keystring_index\",\n        \"views\": {\n            \"by_keystring\": {\n                \"map\": \"function (doc) { if (doc.model && doc.model.type === '\\''annotation'\\'' && doc.model.targets) { doc.model.targets.forEach(function(target) { if(target.keyString) { emit(target.keyString, doc._id); } }); } }\"\n            }\n        }\n    }')\n\n    if [[ $response =~ \"\\\"ok\\\":true\" ]]; then\n        echo \"Successfully created annotation_keystring_index\"\n    elif [[ $response =~ \"\\\"error\\\":\\\"conflict\\\"\" ]]; then\n        echo \"annotation_keystring_index already exists, skipping creation\"\n    else\n        echo \"Unable to create annotation_keystring_index\"\n        echo $response\n    fi\n\n    # Add auth database for locked objects\n    response=$(curl --silent --user \"${CURL_USERPASS_ARG}\" --request PUT \"$COUCH_BASE_LOCAL\"/\"$OPENMCT_DATABASE_NAME\"/_design/auth \\\n    --header 'Content-Type: application/json' \\\n    --data '{\n      \"_id\": \"_design/auth\",\n      \"language\": \"javascript\",\n      \"validate_doc_update\": \"function (newDoc, oldDoc, userCtx) {  if (userCtx.roles.indexOf('\\''_admin'\\'') !== -1) {    return;  } else if (oldDoc === null) {    return;  } else if (oldDoc.model.type === '\\''timer'\\'' || oldDoc.model.type === '\\''notebook'\\'' || oldDoc.model.type === '\\''restricted-notebook'\\'') {    if (oldDoc.model.name !== newDoc.model.name) {      throw ({        forbidden: '\\''Read-only object'\\''      });    } else {      return;    }  } else if (oldDoc.model.locked === true && oldDoc.model.disallowUnlock === true) {    throw ({      forbidden: '\\''Read-only object'\\''    });  } else {    return;  }}\"\n    }')\n\n    if [[ $response =~ \"\\\"ok\\\":true\" ]]; then\n        echo \"Successfully created _design/auth design document for locked objects\"\n    elif [[ $response =~ \"\\\"error\\\":\\\"conflict\\\"\" ]]; then\n        echo \"_design/auth already exists, skipping creation\"\n    else\n        echo \"Unable to create _design/auth\"\n        echo $response\n    fi\n}\n\n# Main script execution\n\n# Check if the admin user exists; if not, create it.\nif [ \"$(admin_user_exists)\" == \"FALSE\" ]; then\n    echo \"Admin user does not exist, creating...\"\n    create_admin_user\nelse\n    echo \"Admin user exists\"\nfi\n\n# Check if the _users table exists; if not, create it.\nusers_table_exists=$(resource_exists \"$COUCH_BASE_LOCAL\"/_users)\nif [ \"FALSE\" == \"${users_table_exists}\" ]; then\n    create_users_table\nelse\n    echo \"_users database already exists, skipping creation\"\nfi\n\n# Check if the _replicator database exists; if not, create it.\nreplicator_table_exists=$(resource_exists \"$COUCH_BASE_LOCAL/_replicator\")\nif [ \"FALSE\" == \"${replicator_table_exists}\" ]; then\n    create_replicator_table\nelse\n    echo \"_replicator database already exists, skipping creation\"\nfi\n\n# Check if the database exists; if not, create it.\nif [ \"FALSE\" == \"$(db_exists)\" ]; then\n    response=$(create_db)\n    if [ \"{\\\"ok\\\":true}\" == \"${response}\" ]; then\n        echo \"Database successfully created\"\n    else\n        echo \"Database creation failed\"\n    fi\nelse\n    echo \"Database already exists, nothing to do\"\nfi\n\n# Update _replicator and OPENMCT_DATABASE_NAME database permissions\nupdate_db_permissions \"_replicator\"\nupdate_db_permissions \"${OPENMCT_DATABASE_NAME}\"\n\n# Check if CORS is enabled; if not, enable it.\nif [ \"FALSE\" == \"$(is_cors_enabled)\" ]; then\n    echo \"Enabling CORS\"\n    enable_cors\nelse\n    echo \"CORS enabled, nothing to do\"\nfi\n\n# Add index and views to the database\nadd_index_and_views\n"
  },
  {
    "path": "src/plugins/plan/GanttChartCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nconst ALLOWED_TYPES = ['plan'];\n\nexport default function ganttChartCompositionPolicy(openmct) {\n  return function (parent, child) {\n    if (parent.type === 'gantt-chart') {\n      return ALLOWED_TYPES.includes(child.type);\n    }\n\n    return true;\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/PlanViewConfiguration.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nexport const DEFAULT_CONFIGURATION = {\n  clipActivityNames: false,\n  swimlaneVisibility: {}\n};\n\nexport default class PlanViewConfiguration extends EventEmitter {\n  constructor(domainObject, openmct) {\n    super();\n\n    this.domainObject = domainObject;\n    this.openmct = openmct;\n\n    this.configurationChanged = this.configurationChanged.bind(this);\n    this.unlistenFromMutation = openmct.objects.observe(\n      domainObject,\n      'configuration',\n      this.configurationChanged\n    );\n  }\n\n  /**\n   * @returns {Object.<string, any>}\n   */\n  getConfiguration() {\n    const configuration = this.domainObject.configuration ?? {};\n    for (const configKey of Object.keys(DEFAULT_CONFIGURATION)) {\n      configuration[configKey] = configuration[configKey] ?? DEFAULT_CONFIGURATION[configKey];\n    }\n\n    return configuration;\n  }\n\n  #updateConfiguration(configuration) {\n    this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);\n  }\n\n  /**\n   * @param {string} swimlaneName\n   * @param {boolean} isVisible\n   */\n  setSwimlaneVisibility(swimlaneName, isVisible) {\n    const configuration = this.getConfiguration();\n    const { swimlaneVisibility } = configuration;\n    swimlaneVisibility[swimlaneName] = isVisible;\n    this.#updateConfiguration(configuration);\n  }\n\n  resetSwimlaneVisibility() {\n    const configuration = this.getConfiguration();\n    const swimlaneVisibility = {};\n    configuration.swimlaneVisibility = swimlaneVisibility;\n    this.#updateConfiguration(configuration);\n  }\n\n  initializeSwimlaneVisibility(swimlaneNames) {\n    const configuration = this.getConfiguration();\n    const { swimlaneVisibility } = configuration;\n    let shouldMutate = false;\n    for (const swimlaneName of swimlaneNames) {\n      if (swimlaneVisibility[swimlaneName] === undefined) {\n        swimlaneVisibility[swimlaneName] = true;\n        shouldMutate = true;\n      }\n    }\n\n    if (shouldMutate) {\n      configuration.swimlaneVisibility = swimlaneVisibility;\n      this.#updateConfiguration(configuration);\n    }\n  }\n\n  /**\n   * @param {boolean} isEnabled\n   */\n  setClipActivityNames(isEnabled) {\n    const configuration = this.getConfiguration();\n    configuration.clipActivityNames = isEnabled;\n    this.#updateConfiguration(configuration);\n  }\n\n  configurationChanged(configuration) {\n    if (configuration !== undefined) {\n      this.emit('change', configuration);\n    }\n  }\n\n  destroy() {\n    this.unlistenFromMutation();\n  }\n}\n"
  },
  {
    "path": "src/plugins/plan/PlanViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Plan from './components/PlanView.vue';\n\nexport default function PlanViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: 'plan.view',\n    name: 'Plan',\n    cssClass: 'icon-plan',\n    canView(domainObject) {\n      return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';\n    },\n\n    canEdit(domainObject) {\n      return domainObject.type === 'gantt-chart';\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          let isCompact = isCompactView(objectPath);\n\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Plan\n              },\n              provide: {\n                openmct,\n                domainObject,\n                path: objectPath\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact,\n                    isChildObject: isCompact\n                  }\n                };\n              },\n              template: '<plan :options=\"options\"></plan>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/README.md",
    "content": "# Plan view and domain objects\nPlans provide a view for a list of activities grouped by categories.\n\n## Plan category and activity JSON format\nThe JSON format for a plan consists of categories/groups and a list of activities for each category.\nActivity properties include:\n* name: Name of the activity\n* start: Timestamps in milliseconds\n* end: Timestamps in milliseconds\n* type: Matches the name of the category it is in\n* color: Background color for the activity\n* textColor: Color of the name text for the activity\n* The format of the json file is as follows:\n\n```json\n{\n    \"TEST_GROUP\": [{\n    \"name\": \"Event 1 with a really long name\",\n    \"start\": 1665323197000,\n    \"end\": 1665344921000,\n    \"type\": \"TEST_GROUP\",\n    \"color\": \"orange\",\n    \"textColor\": \"white\"\n    }],\n    \"GROUP_2\": [{\n    \"name\": \"Event 2\",\n    \"start\": 1665409597000,\n    \"end\": 1665456252000,\n    \"type\": \"GROUP_2\",\n    \"color\": \"red\",\n    \"textColor\": \"white\"\n    }]\n}\n```\n\n## Plans using JSON file uploads\nPlan domain objects can be created by uploading a JSON file with the format above to render categories and activities.\n\n## Using Domain Objects directly\nIf uploading a JSON is not desired, it is possible to visualize domain objects of type 'plan'.\nThe standard model is as follows: \n```javascript\n{\n    identifier: {\n        namespace: \"\"\n        key: \"test-plan\"\n    }\n    name:\"A plan object\",\n    type:\"plan\",\n    location:\"ROOT\",\n    selectFile: {\n        body: {\n            SOME_CATEGORY: [{\n                    name: \"An activity\",\n                    start: 1665323197000,\n                    end: 1665323197100,\n                    type: \"SOME_CATEGORY\"\n                }\n            ],\n            ANOTHER_CATEGORY: [{\n                    name: \"An activity\",\n                    start: 1665323197000,\n                    end: 1665323197100,\n                    type: \"ANOTHER_CATEGORY\"\n                }\n            ]\n        }\n    }\n}\n```\n\nIf your data has non-standard keys for `start, end, type and activities` properties, use the `sourceMap` property mapping.\n```javascript\n{\n    identifier: {\n        namespace: \"\"\n        key: \"another-test-plan\"\n    }\n    name:\"Another plan object\",\n    type:\"plan\",\n    location:\"ROOT\",\n    sourceMap: {\n        start: 'start_time',\n        end: 'end_time',\n        activities: 'items',\n        groupId: 'category'\n    },\n    selectFile: {\n        body: {\n            items: [\n                {\n                    name: \"An activity\",\n                    start_time: 1665323197000,\n                    end_time: 1665323197100,\n                    category: \"SOME_CATEGORY\"\n                },\n                {\n                    name: \"Another activity\",\n                    start_time: 1665323198000,\n                    end_time: 1665323198100,\n                    category: \"ANOTHER_CATEGORY\"\n                }\n            ]\n        }\n    }\n}\n```\n\n## Rendering categories and activities:\nThe algorithm to render categories and activities on a canvas is as follows:\n* Each category gets a swimlane.\n* Activities within a category are rendered within it's swimlane.\n* Render each activity on a given row if it's duration+label do not overlap (start/end times) with an existing activity on that row.\n* Move to the next available row within a swimlane if there is overlap\n* Labels for a given activity will be rendered within it's duration slot if it fits in that rectangular space.\n* Labels that do not fit within an activity's duration slot are rendered outside, to the right of the duration slot.\n\n"
  },
  {
    "path": "src/plugins/plan/components/ActivityTimeline.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <SwimLane :is-nested=\"isNested\" :status=\"status\">\n    <template #label>\n      {{ heading }}\n    </template>\n    <template #object>\n      <div class=\"c-plan-av\" :style=\"alignmentStyle\">\n        <svg v-if=\"activities.length > 0\" class=\"c-plan-av__svg\" :height=\"height\">\n          <symbol id=\"activity-bar-bg\" :height=\"rowHeight\" width=\"2\" preserveAspectRatio=\"none\">\n            <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"currentColor\" />\n            <line\n              x1=\"100%\"\n              y1=\"0\"\n              x2=\"100%\"\n              y2=\"100%\"\n              stroke=\"black\"\n              stroke-width=\"1\"\n              opacity=\"0.3\"\n              transform=\"translate(-0.5, 0)\"\n            />\n          </symbol>\n          <template v-for=\"(activity, index) in activities\" :key=\"`g-${activity.clipPathId}`\">\n            <template v-if=\"clipActivityNames === true\">\n              <clipPath :id=\"activity.clipPathId\" :key=\"activity.clipPathId\">\n                <rect\n                  :x=\"activity.rectStart\"\n                  :y=\"activity.row\"\n                  :width=\"activity.rectWidth - 1\"\n                  :height=\"rowHeight\"\n                />\n              </clipPath>\n            </template>\n            <g\n              class=\"c-plan__activity activity-bounds\"\n              @click=\"setSelectionForActivity(activity, $event)\"\n            >\n              <title>{{ activity.name }}</title>\n              <use\n                :key=\"`rect-${index}`\"\n                href=\"#activity-bar-bg\"\n                :x=\"activity.rectStart\"\n                :y=\"activity.row\"\n                :width=\"activity.rectWidth\"\n                :height=\"rowHeight\"\n                :class=\"activity.class\"\n                :color=\"activity.color\"\n              />\n              <text\n                v-for=\"(textLine, textIndex) in activity.textLines\"\n                :key=\"`text-${index}-${textIndex}`\"\n                :class=\"`c-plan__activity-label ${activity.textClass}`\"\n                :x=\"activity.textStart\"\n                :y=\"activity.textY + textIndex * lineHeight\"\n                :fill=\"activity.textColor\"\n                :clip-path=\"clipActivityNames === true ? `url(#${activity.clipPathId})` : ''\"\n              >\n                {{ textLine }}\n              </text>\n            </g>\n          </template>\n        </svg>\n        <div v-else class=\"c-timeline__no-items\">No activities within timeframe</div>\n      </div>\n    </template>\n  </SwimLane>\n</template>\n\n<script>\nconst AXES_PADDING = 20;\n\nimport { inject } from 'vue';\n\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\n\nimport { useAlignment } from '../../../ui/composables/alignmentContext.js';\n\nexport default {\n  components: {\n    SwimLane\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    activities: {\n      type: Array,\n      required: true\n    },\n    clipActivityNames: {\n      type: Boolean,\n      default: false\n    },\n    heading: {\n      type: String,\n      required: true\n    },\n    height: {\n      type: Number,\n      default: 30\n    },\n    width: {\n      type: Number,\n      default: 200\n    },\n    isNested: {\n      type: Boolean,\n      default: false\n    },\n    status: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    rowHeight: {\n      type: Number,\n      default: 22\n    }\n  },\n  emits: ['activity-selected'],\n  setup() {\n    const domainObject = inject('domainObject');\n    const path = inject('path');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData, reset: resetAlignment } = useAlignment(\n      domainObject,\n      path,\n      openmct\n    );\n\n    return { alignmentData, resetAlignment };\n  },\n  data() {\n    return {\n      lineHeight: 10\n    };\n  },\n  computed: {\n    alignmentStyle() {\n      let leftOffset = 0;\n      const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;\n      if (this.alignmentData.leftWidth) {\n        leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;\n      }\n      return {\n        margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`\n      };\n    }\n  },\n  methods: {\n    setSelectionForActivity(activity, event) {\n      event.stopPropagation();\n      this.$emit('activity-selected', {\n        event,\n        selection: activity.selection\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/components/PlanView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"plan\" class=\"c-plan c-timeline-holder\">\n    <template v-if=\"viewBounds && !options.compact\">\n      <SwimLane class=\"c-swimlane__time-axis\">\n        <template #label>{{ timeSystem.name }}</template>\n        <template #object>\n          <TimelineAxis\n            :bounds=\"viewBounds\"\n            :time-system=\"timeSystem\"\n            :content-height=\"height\"\n            :ahead-behind=\"aheadBehind\"\n            :rendering-engine=\"renderingEngine\"\n          />\n        </template>\n      </SwimLane>\n    </template>\n    <div class=\"c-plan__contents u-contents\">\n      <ActivityTimeline\n        v-for=\"(group, index) in visibleActivityGroups\"\n        :key=\"`activityGroup-${group.heading}-${index}`\"\n        :activities=\"group.activities\"\n        :clip-activity-names=\"clipActivityNames\"\n        :heading=\"group.heading\"\n        :height=\"group.height\"\n        :row-height=\"rowHeight\"\n        :width=\"group.width\"\n        :is-nested=\"options.isChildObject\"\n        :status=\"status\"\n        @activity-selected=\"selectActivity\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { scaleLinear, scaleUtc } from 'd3-scale';\n\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\n\nimport TimelineAxis from '../../../ui/components/TimeSystemAxis.vue';\nimport { PLAN_EXECUTION_MONITORING_KEY } from '../../planExecutionMonitoring/planExecutionMonitoringIdentifier.js';\nimport PlanViewConfiguration from '../PlanViewConfiguration.js';\nimport { getContrastingColor, getValidatedData, getValidatedGroups } from '../util.js';\nimport ActivityTimeline from './ActivityTimeline.vue';\n\nconst PADDING = 1;\nconst OUTER_TEXT_PADDING = 12;\nconst INNER_TEXT_PADDING = 15;\nconst TEXT_LEFT_PADDING = 5;\nconst ROW_PADDING = 5;\nconst SWIMLANE_PADDING = 3;\nconst ROW_HEIGHT = 22;\nconst MAX_TEXT_WIDTH = 300;\nconst MIN_ACTIVITY_WIDTH = 2;\nconst DEFAULT_COLOR = '#999';\nconst DEFAULT_AHEAD_BEHIND_STATUS = {\n  duration: 0,\n  status: ''\n};\n\nexport default {\n  components: {\n    TimelineAxis,\n    SwimLane,\n    ActivityTimeline\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    options: {\n      type: Object,\n      default() {\n        return {\n          compact: false,\n          isChildObject: false\n        };\n      }\n    },\n    renderingEngine: {\n      type: String,\n      default() {\n        return 'svg';\n      }\n    }\n  },\n  data() {\n    return {\n      activityGroups: [],\n      viewBounds: null,\n      timeSystem: null,\n      planData: {},\n      swimlaneVisibility: {},\n      clipActivityNames: false,\n      height: 0,\n      rowHeight: ROW_HEIGHT,\n      aheadBehind: DEFAULT_AHEAD_BEHIND_STATUS\n    };\n  },\n  computed: {\n    visibleActivityGroups() {\n      if (this.domainObject.type === 'plan') {\n        return this.activityGroups;\n      } else {\n        return this.activityGroups.filter(\n          (group) => this.swimlaneVisibility[group.heading] === true\n        );\n      }\n    }\n  },\n  watch: {\n    clipActivityNames() {\n      this.setScaleAndGenerateActivities();\n    }\n  },\n  mounted() {\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.planViewConfiguration = new PlanViewConfiguration(this.domainObject, this.openmct);\n    this.configuration = this.planViewConfiguration.getConfiguration();\n    this.isNested = this.options.isChildObject;\n    this.swimlaneVisibility = this.configuration.swimlaneVisibility;\n    this.clipActivityNames = this.configuration.clipActivityNames;\n\n    // This view is used for both gantt-chart and plan domain objects\n    if (this.domainObject.type === 'plan') {\n      this.setupPlan(this.domainObject);\n    }\n\n    const canvas = document.createElement('canvas');\n    this.canvasContext = canvas.getContext('2d');\n    this.setDimensions();\n    this.setTimeContext();\n    this.handleConfigurationChange(this.configuration);\n    this.planViewConfiguration.on('change', this.handleConfigurationChange);\n    this.loadComposition();\n\n    this.resizeObserver = new ResizeObserver(this.resize);\n    this.resizeObserver.observe(this.$refs.plan);\n  },\n  beforeUnmount() {\n    this.resizeObserver.disconnect();\n    this.stopFollowingTimeContext();\n    if (this.unlisten) {\n      this.unlisten();\n    }\n\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n\n    if (this.composition) {\n      this.composition.off('add', this.handleCompositionAdd);\n      this.composition.off('remove', this.handleCompositionRemove);\n    }\n\n    this.planViewConfiguration.off('change', this.handleConfigurationChange);\n    if (this.stopObservingPlanChanges) {\n      this.stopObservingPlanChanges();\n    }\n    this.planViewConfiguration.destroy();\n\n    if (this.stopObservingPlanExecutionMonitoringStatusObject) {\n      this.stopObservingPlanExecutionMonitoringStatusObject();\n    }\n  },\n  methods: {\n    setupPlan(domainObject) {\n      this.planObject = domainObject;\n      // Plan object configuration\n      this.applyChangesForPlanObject(domainObject);\n      this.stopObservingPlanChanges = this.openmct.objects.observe(\n        domainObject,\n        '*',\n        this.applyChangesForPlanObject\n      );\n      this.removeStatusListener = this.openmct.status.observe(\n        domainObject.identifier,\n        this.setPlanStatus\n      );\n      // plan execution monitoring\n      this.getPlanExecutionMonitoringStatus();\n    },\n    async getPlanExecutionMonitoringStatus() {\n      this.planExecutionMonitoringStatusObject = await this.openmct.objects.get(\n        PLAN_EXECUTION_MONITORING_KEY\n      );\n      this.setPlanExecutionMonitoringStatus(this.planExecutionMonitoringStatusObject);\n      this.stopObservingPlanExecutionMonitoringStatusObject = this.openmct.objects.observe(\n        this.planExecutionMonitoringStatusObject,\n        '*',\n        this.setPlanExecutionMonitoringStatus\n      );\n    },\n    setPlanExecutionMonitoringStatus(newStatusObject) {\n      const planIdentifier = this.openmct.objects.makeKeyString(this.planObject.identifier);\n      if (\n        newStatusObject &&\n        newStatusObject.execution_monitoring &&\n        newStatusObject.execution_monitoring[planIdentifier]\n      ) {\n        this.aheadBehind = newStatusObject.execution_monitoring[planIdentifier];\n      } else {\n        this.aheadBehind = DEFAULT_AHEAD_BEHIND_STATUS;\n      }\n    },\n    setPlanData(domainObject) {\n      this.planData = getValidatedData(domainObject);\n    },\n    activityNameFitsRect(activityName, rectWidth) {\n      return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;\n    },\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.path);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.updateViewBounds(this.timeContext.getBounds());\n\n      this.timeContext.on('timeSystem', this.setScaleAndGenerateActivities);\n      this.timeContext.on('boundsChanged', this.updateViewBounds);\n    },\n    loadComposition() {\n      if (this.composition) {\n        this.composition.on('add', this.handleCompositionAdd);\n        this.composition.on('remove', this.handleCompositionRemove);\n        this.composition.load();\n      }\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('timeSystem', this.setScaleAndGenerateActivities);\n        this.timeContext.off('boundsChanged', this.updateViewBounds);\n      }\n    },\n    showReplacePlanDialog(domainObject) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will replace the current Plan. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              this.removeFromComposition(this.planObject);\n              this.setupPlan(domainObject);\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              this.removeFromComposition(domainObject);\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    handleCompositionAdd(domainObject) {\n      if (this.planObject) {\n        this.showReplacePlanDialog(domainObject);\n      } else {\n        this.setupPlan(domainObject);\n        this.swimlaneVisibility = this.configuration.swimlaneVisibility;\n      }\n    },\n    handleConfigurationChange(newConfiguration) {\n      this.configuration = this.planViewConfiguration.getConfiguration();\n      Object.keys(newConfiguration).forEach((key) => {\n        this[key] = newConfiguration[key];\n      });\n    },\n    handleCompositionRemove(identifier) {\n      if (\n        this.planObject &&\n        this.openmct.objects.areIdsEqual(identifier, this.planObject?.identifier)\n      ) {\n        this.planObject = null;\n        this.planData = {};\n        this.planViewConfiguration.resetSwimlaneVisibility();\n      }\n\n      this.setScaleAndGenerateActivities();\n    },\n    applyChangesForPlanObject(domainObject) {\n      const planDomainObject = domainObject || this.domainObject;\n      this.setPlanData(planDomainObject);\n      this.setPlanStatus(this.openmct.status.get(planDomainObject.identifier));\n      this.setScaleAndGenerateActivities();\n    },\n    removeFromComposition(domainObject) {\n      this.composition.remove(domainObject);\n    },\n    resize() {\n      let clientWidth = this.getClientWidth();\n      let clientHeight = this.getClientHeight();\n      if (clientWidth !== this.width) {\n        this.setDimensions();\n        this.updateViewBounds();\n      }\n\n      if (clientHeight !== this.height) {\n        this.setDimensions();\n      }\n    },\n    getClientWidth() {\n      let clientWidth = this.$refs.plan.clientWidth;\n\n      if (!clientWidth) {\n        //this is a hack - need a better way to find the parent of this component\n        let parent = this.getParent();\n        if (parent) {\n          clientWidth = parent.getBoundingClientRect().width;\n        }\n      }\n\n      return clientWidth - 200;\n    },\n    getParent() {\n      //this is a hack - need a better way to find the parent of this component\n      return this.$el.closest('.is-object-type-time-strip');\n    },\n    getClientHeight() {\n      let clientHeight = this.$refs.plan.clientHeight;\n\n      if (!clientHeight) {\n        let parent = this.getParent();\n        if (parent) {\n          clientHeight = parent.getBoundingClientRect().height;\n        }\n      }\n\n      return clientHeight;\n    },\n    updateViewBounds(bounds) {\n      if (bounds) {\n        this.viewBounds = bounds;\n      }\n\n      if (this.timeSystem === null) {\n        this.timeSystem = this.openmct.time.getTimeSystem();\n      }\n\n      this.setScaleAndGenerateActivities();\n    },\n    setScaleAndGenerateActivities(timeSystem) {\n      if (timeSystem) {\n        this.timeSystem = timeSystem;\n      }\n\n      this.setScale(this.timeSystem);\n      if (this.xScale) {\n        this.generateActivities();\n      }\n    },\n    setDimensions() {\n      this.width = this.getClientWidth();\n      this.height = this.getClientHeight();\n    },\n    setScale(timeSystem) {\n      if (!this.width) {\n        return;\n      }\n\n      if (!timeSystem) {\n        timeSystem = this.openmct.time.getTimeSystem();\n      }\n\n      if (timeSystem.isUTCBased) {\n        this.xScale = scaleUtc();\n        this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);\n      } else {\n        this.xScale = scaleLinear();\n        this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);\n      }\n\n      this.xScale.range([PADDING, this.width - PADDING * 2]);\n    },\n    isActivityInBounds(activity) {\n      return activity.start < this.viewBounds.end && activity.end > this.viewBounds.start;\n    },\n    /**\n     * Get the width of the given text in pixels.\n     * @param {string} text\n     * @returns {number} width of the text in pixels (as a double)\n     */\n    getTextWidth(text) {\n      const textMetrics = this.canvasContext.measureText(text);\n\n      return textMetrics.width;\n    },\n    sortIntegerAsc(a, b) {\n      const numA = parseInt(a, 10);\n      const numB = parseInt(b, 10);\n      if (numA > numB) {\n        return 1;\n      }\n\n      if (numA < numB) {\n        return -1;\n      }\n\n      return 0;\n    },\n    /**\n     * Get the row where the next activity will land.\n     * @param {number} rectX the x coordinate of the activity rect\n     * @param {number} width the width of the activity rect\n     * @param {Object.<string, Array.<Object>>} activitiesByRow activity arrays mapped by row value\n     */\n    getRowForActivity(rectX, rectWidth, activitiesByRow) {\n      const sortedActivityRows = Object.keys(activitiesByRow).sort(this.sortIntegerAsc);\n      let currentRow;\n\n      function activitiesHaveOverlap(rects) {\n        return rects.some((rect) => {\n          const { rectStart, rectEnd } = rect;\n          const calculatedEnd = rectX + rectWidth;\n          const hasOverlap =\n            (rectX >= rectStart && rectX <= rectEnd) ||\n            (calculatedEnd >= rectStart && calculatedEnd <= rectEnd) ||\n            (rectX <= rectStart && calculatedEnd >= rectEnd);\n\n          return hasOverlap;\n        });\n      }\n\n      for (let i = 0; i < sortedActivityRows.length; i++) {\n        let row = sortedActivityRows[i];\n        if (!activitiesHaveOverlap(activitiesByRow[row])) {\n          currentRow = row;\n          break;\n        }\n      }\n\n      if (currentRow === undefined && sortedActivityRows.length) {\n        let row = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10);\n        currentRow = row + ROW_HEIGHT + ROW_PADDING;\n      }\n\n      return currentRow || SWIMLANE_PADDING;\n    },\n    generateActivities() {\n      if (!this.planObject) {\n        return;\n      }\n      const groupNames = getValidatedGroups(this.planObject, this.planData);\n\n      if (!groupNames.length) {\n        return;\n      }\n\n      const activityGroups = [];\n      this.planViewConfiguration.initializeSwimlaneVisibility(groupNames);\n\n      groupNames.forEach((groupName) => {\n        let activitiesByRow = {};\n        let currentRow = 0;\n\n        const rawActivities = this.planData[groupName];\n        if (rawActivities === undefined) {\n          return;\n        }\n\n        rawActivities.forEach((rawActivity, index) => {\n          if (!this.isActivityInBounds(rawActivity)) {\n            return;\n          }\n\n          const currentStart = Math.max(this.viewBounds.start, rawActivity.start);\n          const currentEnd = Math.min(this.viewBounds.end, rawActivity.end);\n          const rectX1 = this.xScale(currentStart);\n          const rectX2 = this.xScale(currentEnd);\n          const rectWidth = Math.max(rectX2 - rectX1, MIN_ACTIVITY_WIDTH);\n\n          //TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text\n          const showTextInsideRect =\n            this.clipActivityNames || this.activityNameFitsRect(rawActivity.name, rectWidth);\n          const textStart = (showTextInsideRect ? rectX1 : rectX2) + TEXT_LEFT_PADDING;\n          const color = rawActivity.color || DEFAULT_COLOR;\n          let textColor = '';\n          if (rawActivity.textColor) {\n            textColor = rawActivity.textColor;\n          } else if (showTextInsideRect) {\n            textColor = getContrastingColor(color);\n          }\n\n          const textLines = this.getActivityDisplayText(\n            this.canvasContext,\n            rawActivity.name,\n            showTextInsideRect\n          );\n          const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;\n\n          if (showTextInsideRect) {\n            currentRow = this.getRowForActivity(rectX1, rectWidth, activitiesByRow);\n          } else {\n            currentRow = this.getRowForActivity(rectX1, textWidth, activitiesByRow);\n          }\n\n          let textY =\n            parseInt(currentRow, 10) +\n            (showTextInsideRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);\n\n          if (!activitiesByRow[currentRow]) {\n            activitiesByRow[currentRow] = [];\n          }\n\n          const activity = {\n            color: color,\n            textColor: textColor,\n            exceeds: {\n              start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),\n              end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)\n            },\n            row: currentRow,\n            textLines: textLines,\n            textStart: textStart,\n            textClass: showTextInsideRect ? '' : 'c-plan__activity-label--outside-rect',\n            textY: textY,\n            rectStart: rectX1,\n            rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,\n            rectWidth: rectWidth,\n            clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),\n            selection: {\n              groupName,\n              index\n            }\n          };\n          activitiesByRow[currentRow].push(activity);\n        });\n\n        const { swimlaneHeight, swimlaneWidth } = this.getGroupDimensions(activitiesByRow);\n        const activities = Array.from(Object.values(activitiesByRow)).flat();\n        activityGroups.push({\n          heading: groupName,\n          activities,\n          height: swimlaneHeight,\n          width: swimlaneWidth,\n          status: this.isNested ? '' : this.status\n        });\n      });\n\n      this.activityGroups = activityGroups;\n    },\n    /**\n     * Format the activity name to fit within the activity rect with a max of 2 lines\n     * @param {CanvasRenderingContext2D} canvasContext\n     * @param {string} activityName\n     * @param {boolean} activityNameFitsRect\n     */\n    getActivityDisplayText(canvasContext, activityName, activityNameFitsRect) {\n      // TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)\n      let words = activityName.split(' ');\n      let line = '';\n      let activityLines = [];\n\n      for (let n = 0; n < words.length && activityLines.length <= 2; n++) {\n        let tempLine = line + words[n] + ' ';\n        let textMetrics = canvasContext.measureText(tempLine);\n        const textWidth = textMetrics.width;\n        if (!activityNameFitsRect && textWidth > MAX_TEXT_WIDTH && n > 0) {\n          activityLines.push(line);\n          line = words[n] + ' ';\n          tempLine = line + words[n] + ' ';\n        }\n\n        line = tempLine;\n      }\n\n      return activityLines.length ? activityLines : [line];\n    },\n    getGroupDimensions(activityRows) {\n      let swimlaneHeight = 30;\n      let swimlaneWidth = this.width;\n\n      if (!activityRows) {\n        return {\n          swimlaneHeight,\n          swimlaneWidth\n        };\n      }\n\n      const rows = Object.keys(activityRows);\n\n      if (rows.length) {\n        const lastActivityRow = rows[rows.length - 1];\n        swimlaneHeight = parseInt(lastActivityRow, 10) + ROW_HEIGHT + SWIMLANE_PADDING;\n        swimlaneWidth = this.width;\n      }\n\n      return {\n        swimlaneHeight,\n        swimlaneWidth\n      };\n    },\n    setPlanStatus(status) {\n      this.status = status;\n    },\n    getClipPathId(groupName, activity, row) {\n      groupName = groupName.toLowerCase().replace(/ /g, '-');\n      const activityName = activity.name.toLowerCase().replace(/ /g, '-');\n\n      return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;\n    },\n    selectActivity({ event, selection }) {\n      const element = event.currentTarget;\n      const multiSelect = event.metaKey;\n      const { groupName, index } = selection;\n      const rawActivity = this.planData[groupName][index];\n      this.openmct.selection.select(\n        [\n          {\n            element: element,\n            context: {\n              type: 'activity',\n              activity: rawActivity\n            }\n          },\n          {\n            element: this.openmct.layout.$refs.browseObject.$el,\n            context: {\n              item: this.domainObject,\n              supportsMultiSelect: true\n            }\n          }\n        ],\n        multiSelect\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/ActivityInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport PlanActivities from './components/PlanActivitiesView.vue';\n\nexport default function ActivityInspectorViewProvider(openmct) {\n  return {\n    key: 'activity-inspector',\n    name: 'Activity',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      let context = selection[0][0].context;\n\n      return context && context.type === 'activity';\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlanActivities\n              },\n              provide: {\n                openmct,\n                selection\n              },\n              template: '<PlanActivities />'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.priority.HIGH;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/inspector/GanttChartInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport PlanViewConfiguration from './components/PlanViewConfiguration.vue';\n\nexport default function GanttChartInspectorViewProvider(openmct) {\n  return {\n    key: 'plan-inspector',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      const domainObject = selection[0][0].context.item;\n\n      return domainObject?.type === 'gantt-chart';\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlanViewConfiguration\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item\n              },\n              template: '<plan-view-configuration></plan-view-configuration>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/inspector/PlanInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport PlanExecutionMonitoringView from './components/PlanExecutionMonitoringView.vue';\n\nexport default function PlanInspectorViewProvider(openmct) {\n  return {\n    key: 'plan-status-inspector',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      const domainObject = selection[0][0].context.item;\n\n      return domainObject?.type === 'plan';\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlanExecutionMonitoringView\n              },\n              provide: {\n                openmct\n              },\n              data() {\n                return {\n                  planObject: selection[0][0].context.item\n                };\n              },\n              template:\n                '<plan-execution-monitoring-view :plan-object=\"planObject\"></plan-execution-monitoring-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/ActivityProperty.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <li class=\"c-inspect-properties__row\">\n    <div class=\"c-inspect-properties__label\">\n      {{ label }}\n    </div>\n    <div class=\"c-inspect-properties__value\">\n      {{ value }}\n    </div>\n  </li>\n</template>\n\n<script>\nexport default {\n  props: {\n    label: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    value: {\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanActivitiesView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <PlanActivityTimeView\n    v-for=\"activity in activities\"\n    :key=\"activity.key\"\n    :activity=\"activity\"\n    :heading=\"heading\"\n  />\n  <PlanActivityPropertiesView\n    v-for=\"activity in activities\"\n    :key=\"activity.key\"\n    heading=\"Properties\"\n    :activity=\"activity\"\n  />\n  <PlanActivityStatusView\n    v-if=\"canPersistState\"\n    :key=\"activities[0].key\"\n    :activity=\"activities[0]\"\n    :execution-state=\"activityExecutionState\"\n    heading=\"Activity Status\"\n    @update-activity-state=\"persistActivityState\"\n  />\n</template>\n\n<script>\nimport { getPreciseDuration } from 'utils/duration';\n\nimport { getDisplayProperties } from '../../util.js';\nimport PlanActivityPropertiesView from './PlanActivityPropertiesView.vue';\nimport PlanActivityStatusView from './PlanActivityStatusView.vue';\nimport PlanActivityTimeView from './PlanActivityTimeView.vue';\n\nconst propertyLabels = {\n  start: 'Start DateTime',\n  end: 'End DateTime',\n  duration: 'Duration',\n  earliestStart: 'Earliest Start',\n  latestEnd: 'Latest End',\n  gap: 'Gap',\n  overlap: 'Overlap',\n  totalTime: 'Total Time',\n  description: 'Description'\n};\nexport default {\n  components: {\n    PlanActivityTimeView,\n    PlanActivityPropertiesView,\n    PlanActivityStatusView\n  },\n  inject: ['openmct', 'selection'],\n  data() {\n    return {\n      name: '',\n      activities: [],\n      selectedActivities: [],\n      activityExecutionState: undefined,\n      heading: ''\n    };\n  },\n  computed: {\n    canPersistState() {\n      return this.selectedActivities.length === 1 && this.activities?.[0]?.id;\n    }\n  },\n  mounted() {\n    this.setFormatters();\n    this.getPlanData(this.selection);\n    this.getActivityStates();\n    this.getActivities();\n    this.openmct.selection.on('change', this.updateSelection);\n    this.openmct.time.on('timeSystem', this.setFormatters);\n  },\n  beforeUnmount() {\n    this.openmct.selection.off('change', this.updateSelection);\n    this.openmct.time.off('timeSystem', this.setFormatters);\n    if (this.stopObservingActivityStatesObject) {\n      this.stopObservingActivityStatesObject();\n    }\n  },\n  methods: {\n    async getActivityStates() {\n      this.activityStatesObject = await this.openmct.objects.get('activity-states');\n      this.setActivityStates(this.activityStatesObject);\n      this.stopObservingActivityStatesObject = this.openmct.objects.observe(\n        this.activityStatesObject,\n        '*',\n        this.setActivityStates\n      );\n    },\n    setActivityStates(newActivitiesStateObject) {\n      if (this.activities.length) {\n        const id = this.activities[0].id;\n        this.activityExecutionState = newActivitiesStateObject.activities[id];\n      } else {\n        this.activityExecutionState = undefined;\n      }\n    },\n    setFormatters() {\n      let timeSystem = this.openmct.time.getTimeSystem();\n      this.timeFormatter = this.openmct.telemetry.getValueFormatter({\n        format: timeSystem.timeFormat\n      }).formatter;\n    },\n    updateSelection(newSelection) {\n      this.getPlanData(newSelection);\n      this.getActivities();\n    },\n    getPlanData(selection) {\n      this.selectedActivities = [];\n      selection.forEach((selectionItem) => {\n        if (selectionItem[0].context.type === 'activity') {\n          const activity = { ...selectionItem[0].context.activity };\n          if (activity) {\n            activity.key = activity.id ?? activity.name;\n            this.selectedActivities.push(activity);\n          }\n        }\n      });\n    },\n    getActivities() {\n      if (this.selectedActivities.length <= 1) {\n        this.heading = 'Time';\n        this.setSingleActivityProperties();\n      } else {\n        this.heading = 'Convex Hull';\n        this.setMultipleActivityProperties();\n      }\n    },\n    setSingleActivityProperties() {\n      this.activities.splice(0);\n      this.selectedActivities.forEach((selectedActivity, index) => {\n        const activity = {\n          id: selectedActivity.id,\n          key: selectedActivity.key,\n          timeProperties: {\n            start: {\n              label: propertyLabels.start,\n              value: this.formatTime(selectedActivity.start)\n            },\n            end: {\n              label: propertyLabels.end,\n              value: this.formatTime(selectedActivity.end)\n            },\n            duration: {\n              label: propertyLabels.duration,\n              value: this.formatDuration(selectedActivity.end - selectedActivity.start)\n            }\n          }\n        };\n        activity.metadata = {};\n        if (selectedActivity.description) {\n          activity.metadata.description = {\n            label: propertyLabels.description,\n            value: selectedActivity.description\n          };\n        }\n\n        const displayProperties = getDisplayProperties(selectedActivity);\n        activity.metadata = {\n          ...activity.metadata,\n          ...displayProperties\n        };\n\n        this.activities[index] = activity;\n      });\n    },\n    sortFn(a, b) {\n      const numA = parseInt(a.start, 10);\n      const numB = parseInt(b.start, 10);\n      if (numA > numB) {\n        return 1;\n      }\n\n      if (numA < numB) {\n        return -1;\n      }\n\n      return 0;\n    },\n    setMultipleActivityProperties() {\n      this.activities.splice(0);\n\n      let earliestStart;\n      let latestEnd;\n      let gap;\n      let overlap;\n      let id;\n      let key;\n\n      //Sort by start time\n      let selectedActivities = this.selectedActivities.sort(this.sortFn);\n      selectedActivities.forEach((selectedActivity, index) => {\n        if (selectedActivities.length === 2 && index > 0) {\n          const previous = selectedActivities[index - 1];\n          //they're on different rows so there must be overlap\n          if (previous.end > selectedActivity.start) {\n            overlap = previous.end - selectedActivity.start;\n          } else if (previous.end < selectedActivity.start) {\n            gap = selectedActivity.start - previous.end;\n          }\n        }\n\n        if (index > 0) {\n          earliestStart = Math.min(earliestStart, selectedActivity.start);\n          latestEnd = Math.max(latestEnd, selectedActivity.end);\n        } else {\n          id = selectedActivity.id;\n          key = selectedActivity.id ?? selectedActivity.name;\n          earliestStart = selectedActivity.start;\n          latestEnd = selectedActivity.end;\n        }\n      });\n      let totalTime = latestEnd - earliestStart;\n\n      const activity = {\n        id,\n        key,\n        timeProperties: {\n          earliestStart: {\n            label: propertyLabels.earliestStart,\n            value: this.formatTime(earliestStart)\n          },\n          latestEnd: {\n            label: propertyLabels.latestEnd,\n            value: this.formatTime(latestEnd)\n          }\n        }\n      };\n\n      if (gap) {\n        activity.timeProperties.gap = {\n          label: propertyLabels.gap,\n          value: this.formatDuration(gap)\n        };\n      } else if (overlap) {\n        activity.timeProperties.overlap = {\n          label: propertyLabels.overlap,\n          value: this.formatDuration(overlap)\n        };\n      }\n\n      activity.timeProperties.totalTime = {\n        label: propertyLabels.totalTime,\n        value: this.formatDuration(totalTime)\n      };\n\n      this.activities[0] = activity;\n    },\n    formatDuration(duration) {\n      return getPreciseDuration(duration);\n    },\n    formatTime(time) {\n      return this.timeFormatter.format(time);\n    },\n    persistActivityState(data) {\n      const { key, executionState } = data;\n      const activitiesPath = `activities.${key}`;\n      this.openmct.objects.mutate(this.activityStatesObject, activitiesPath, executionState);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanActivityPropertiesView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__properties c-inspect-properties\">\n    <div v-if=\"properties.length\" class=\"u-contents\">\n      <div class=\"c-inspect-properties__header\">{{ heading }}</div>\n      <ul v-for=\"property in properties\" :key=\"property.id\" class=\"c-inspect-properties__section\">\n        <ActivityProperty :label=\"property.label\" :value=\"property.value\" />\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ActivityProperty from './ActivityProperty.vue';\n\nexport default {\n  components: {\n    ActivityProperty\n  },\n  props: {\n    activity: {\n      type: Object,\n      required: true\n    },\n    heading: {\n      type: String,\n      required: true\n    }\n  },\n  data() {\n    return {\n      properties: []\n    };\n  },\n  mounted() {\n    this.setProperties();\n  },\n  methods: {\n    setProperties() {\n      if (!this.activity.metadata) {\n        return;\n      }\n\n      Object.keys(this.activity.metadata).forEach((key) => {\n        if (this.activity.metadata[key].label) {\n          const label = this.activity.metadata[key].label;\n          const value = String(this.activity.metadata[key].value);\n          const id = this.activity.id;\n\n          this.properties[this.properties.length] = {\n            id,\n            label,\n            value\n          };\n        }\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanActivityStatusView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__properties c-inspect-properties\">\n    <div class=\"u-contents\">\n      <div class=\"c-inspect-properties__header\">{{ heading }}</div>\n      <div class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Set Status\">Set Status</div>\n        <div class=\"c-inspect-properties__value\" aria-label=\"Activity Status Label\">\n          <select\n            v-model=\"currentStatusKey\"\n            name=\"setActivityStatus\"\n            aria-label=\"Activity Status\"\n            @change=\"changeActivityStatus\"\n          >\n            <option\n              v-for=\"status in activityStates\"\n              :key=\"status.key\"\n              :value=\"status.key\"\n              :aria-selected=\"currentStatusKey === status.key\"\n            >\n              {{ status.label }}\n            </option>\n          </select>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nconst activityStates = [\n  {\n    key: 'notStarted',\n    label: 'Not started'\n  },\n  {\n    key: 'in-progress',\n    label: 'In progress'\n  },\n  {\n    key: 'completed',\n    label: 'Completed'\n  },\n  {\n    key: 'aborted',\n    label: 'Aborted'\n  },\n  {\n    key: 'skipped',\n    label: 'Skipped'\n  }\n];\n\nexport default {\n  props: {\n    activity: {\n      type: Object,\n      required: true\n    },\n    executionState: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    heading: {\n      type: String,\n      required: true\n    }\n  },\n  emits: ['updateActivityState'],\n  data() {\n    return {\n      activityStates: activityStates,\n      currentStatusKey: activityStates[0].key\n    };\n  },\n  watch: {\n    executionState() {\n      this.setActivityStatus();\n    }\n  },\n  mounted() {\n    this.setActivityStatus();\n  },\n  methods: {\n    setActivityStatus() {\n      let statusKeyIndex = activityStates.findIndex((state) => state.key === this.executionState);\n      if (statusKeyIndex < 0) {\n        statusKeyIndex = 0;\n      }\n      this.currentStatusKey = this.activityStates[statusKeyIndex].key;\n    },\n    changeActivityStatus() {\n      if (this.currentStatusKey === '') {\n        return;\n      }\n      this.activity.executionState = this.currentStatusKey;\n      this.$emit('updateActivityState', {\n        key: this.activity.id,\n        executionState: this.currentStatusKey\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanActivityTimeView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__properties c-inspect-properties\">\n    <div v-if=\"timeProperties.length\" class=\"u-contents\">\n      <div class=\"c-inspect-properties__header\">\n        {{ heading }}\n      </div>\n      <ul\n        v-for=\"timeProperty in timeProperties\"\n        :key=\"timeProperty.id\"\n        class=\"c-inspect-properties__section\"\n      >\n        <ActivityProperty :label=\"timeProperty.label\" :value=\"timeProperty.value\" />\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ActivityProperty from './ActivityProperty.vue';\n\nexport default {\n  components: {\n    ActivityProperty\n  },\n  props: {\n    activity: {\n      type: Object,\n      required: true\n    },\n    heading: {\n      type: String,\n      required: true\n    }\n  },\n  data() {\n    return {\n      timeProperties: []\n    };\n  },\n  mounted() {\n    this.setProperties();\n  },\n  methods: {\n    setProperties() {\n      Object.keys(this.activity.timeProperties).forEach((key) => {\n        if (this.activity.timeProperties[key].label) {\n          const label = this.activity.timeProperties[key].label;\n          const value = String(this.activity.timeProperties[key].value);\n          const id = this.activity.id;\n\n          this.timeProperties[this.timeProperties.length] = {\n            id,\n            label,\n            value\n          };\n        }\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanExecutionMonitoringView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__properties c-inspect-properties\">\n    <div class=\"u-contents\">\n      <div class=\"c-inspect-properties__header\">Plan Execution Status</div>\n      <div class=\"c-inspect-properties__row\">\n        <div\n          class=\"c-inspect-properties__label\"\n          aria-label=\"Plan Execution Monitoring Status Label\"\n        >\n          <select\n            v-model=\"planExecutionMonitoringStatus\"\n            name=\"executionMonitoringStatus\"\n            aria-label=\"Plan Execution Monitoring Status\"\n            @change=\"changePlanExecutionMonitoring\"\n          >\n            <option\n              v-for=\"status in executionMonitorStates\"\n              :key=\"status.key\"\n              :value=\"status.key\"\n              :aria-selected=\"planExecutionMonitoringStatus === status.key\"\n            >\n              {{ status.label }}\n            </option>\n          </select>\n        </div>\n        <div\n          v-if=\"planExecutionMonitoringStatus !== executionMonitorStates[0].key\"\n          class=\"c-inspect-properties__value\"\n        >\n          <input\n            id=\"plan_execution_monitoring_duration\"\n            v-model=\"duration\"\n            aria-label=\"Plan Execution Monitoring Duration\"\n            class=\"c-input--sm\"\n            type=\"number\"\n            @change=\"toggleDuration\"\n          />\n          <span class=\"hint\">minutes</span>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { PLAN_EXECUTION_MONITORING_KEY } from '../../../planExecutionMonitoring/planExecutionMonitoringIdentifier.js';\n\nconst executionMonitorStates = [\n  {\n    key: 'nominal',\n    label: 'Nominal'\n  },\n  {\n    key: 'behind',\n    label: 'Behind by'\n  },\n  {\n    key: 'ahead',\n    label: 'Ahead by'\n  }\n];\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    planObject: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      executionMonitorStates: executionMonitorStates,\n      planExecutionMonitoringStatus: executionMonitorStates[0].key,\n      duration: 0\n    };\n  },\n  watch: {\n    planObject() {\n      this.getStatus();\n    }\n  },\n  mounted() {\n    this.getStatus();\n  },\n  beforeUnmount() {\n    if (this.stopObservingPlanExecutionMonitoringStatusObject) {\n      this.stopObservingPlanExecutionMonitoringStatusObject();\n    }\n  },\n  methods: {\n    getStatus() {\n      this.planIdentifier = this.openmct.objects.makeKeyString(this.planObject.identifier);\n      this.getPlanExecutionMonitoringStatus();\n    },\n    toggleDuration() {\n      if (this.duration === undefined || this.duration < 0) {\n        return;\n      }\n      if (this.duration === 0) {\n        this.planExecutionMonitoringStatus = executionMonitorStates[0].key;\n      }\n      this.persistExecutionMonitoringStatus();\n    },\n    changePlanExecutionMonitoring() {\n      if (this.planExecutionMonitoringStatus === '') {\n        return;\n      }\n      this.persistExecutionMonitoringStatus();\n    },\n    setPlanExecutionMonitoring(status, duration) {\n      let statusKeyIndex = executionMonitorStates.findIndex((state) => state.key === status);\n      if (statusKeyIndex < 0) {\n        statusKeyIndex = 0;\n      }\n      this.planExecutionMonitoringStatus = this.executionMonitorStates[statusKeyIndex].key;\n      this.duration = duration ?? 0;\n    },\n    async getPlanExecutionMonitoringStatus() {\n      this.planExecutionMonitoringStatusObject = await this.openmct.objects.get(\n        PLAN_EXECUTION_MONITORING_KEY\n      );\n      this.setPlanExecutionMonitoringStatus(this.planExecutionMonitoringStatusObject);\n      this.stopObservingPlanExecutionMonitoringStatusObject = this.openmct.objects.observe(\n        this.planExecutionMonitoringStatusObject,\n        '*',\n        this.setPlanExecutionMonitoringStatus\n      );\n    },\n    setPlanExecutionMonitoringStatus(newStatusObject) {\n      const statusObj = newStatusObject?.execution_monitoring?.[this.planIdentifier];\n      if (!statusObj) {\n        this.setPlanExecutionMonitoring();\n        return;\n      }\n      const { status, duration } = statusObj;\n      this.setPlanExecutionMonitoring(status, duration);\n    },\n    persistExecutionMonitoringStatus() {\n      const executionMonitoringStatus = {\n        duration: this.duration,\n        status: this.planExecutionMonitoringStatus\n      };\n      const executionMonitoringPath = `execution_monitoring.${this.planIdentifier}`;\n      this.openmct.objects.mutate(\n        this.planExecutionMonitoringStatusObject,\n        executionMonitoringPath,\n        executionMonitoringStatus\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/inspector/components/PlanViewConfiguration.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-inspect-properties\">\n    <div class=\"c-inspect-properties__header\">Swimlane Visibility</div>\n    <ul class=\"c-inspect-properties__section\">\n      <li\n        v-for=\"(visible, swimlaneName) in configuration.swimlaneVisibility\"\n        :key=\"swimlaneName\"\n        class=\"c-inspect-properties__row\"\n      >\n        <div class=\"c-inspect-properties__label\" title=\"Show or hide swimlane\">\n          <label :for=\"swimlaneName + 'ColumnControl'\">{{ swimlaneName }}</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            :id=\"swimlaneName + 'ColumnControl'\"\n            type=\"checkbox\"\n            :checked=\"visible === true\"\n            @change=\"toggleHideSwimlane(swimlaneName)\"\n          />\n          <div v-else class=\"value\">\n            {{ visible === true ? 'Visible' : 'Hidden' }}\n          </div>\n        </div>\n      </li>\n    </ul>\n    <div class=\"c-inspect-properties__header\">Display settings</div>\n    <ul class=\"c-inspect-properties__section\">\n      <li class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Clip Activity Names\">\n          <label for=\"clipActivityNames\">Clip Activity Names</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            id=\"clipActivityNames\"\n            type=\"checkbox\"\n            :checked=\"configuration.clipActivityNames === true\"\n            @change=\"toggleClipActivityNames\"\n          />\n          <div v-else class=\"value\">\n            {{ configuration.clipActivityNames === true ? 'On' : 'Off' }}\n          </div>\n        </div>\n      </li>\n    </ul>\n  </div>\n  <PlanExecutionMonitoringView\n    v-if=\"planObject !== null\"\n    :plan-object=\"planObject\"\n  ></PlanExecutionMonitoringView>\n</template>\n<script>\nimport { markRaw } from 'vue';\n\nimport PlanViewConfiguration from '../../PlanViewConfiguration.js';\nimport PlanExecutionMonitoringView from './PlanExecutionMonitoringView.vue';\n\nexport default {\n  components: { PlanExecutionMonitoringView },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    const planViewConfiguration = markRaw(\n      new PlanViewConfiguration(this.domainObject, this.openmct)\n    );\n\n    return {\n      planViewConfiguration,\n      isEditing: this.openmct.editor.isEditing(),\n      configuration: planViewConfiguration.getConfiguration(),\n      planObject: null\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setIsEditing);\n    this.planViewConfiguration.on('change', this.handleConfigurationChange);\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.loadComposition();\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setIsEditing);\n    this.planViewConfiguration.off('change', this.handleConfigurationChange);\n  },\n  methods: {\n    loadComposition() {\n      if (this.composition) {\n        this.composition.on('add', this.handleCompositionAdd);\n        this.composition.on('remove', this.handleCompositionRemove);\n        this.composition.load();\n      }\n    },\n    handleCompositionAdd(domainObject) {\n      this.planObject = domainObject;\n    },\n    handleCompositionRemove(identifier) {\n      if (\n        this.planObject &&\n        this.openmct.objects.areIdsEqual(identifier, this.planObject?.identifier)\n      ) {\n        this.planObject = null;\n      }\n    },\n    /**\n     * @param {Object.<string, any>} newConfiguration\n     */\n    handleConfigurationChange(newConfiguration) {\n      this.configuration = newConfiguration;\n    },\n    /**\n     * @param {boolean} isEditing\n     */\n    setIsEditing(isEditing) {\n      this.isEditing = isEditing;\n    },\n    toggleClipActivityNames() {\n      this.planViewConfiguration.setClipActivityNames(!this.configuration.clipActivityNames);\n    },\n    /**\n     * @param {string} swimlaneName\n     */\n    toggleHideSwimlane(swimlaneName) {\n      this.planViewConfiguration.setSwimlaneVisibility(\n        swimlaneName,\n        !this.configuration.swimlaneVisibility[swimlaneName]\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plan/plan.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n.c-plan {\n  svg {\n    text-rendering: geometricPrecision;\n\n    text {\n      stroke: none;\n    }\n\n    .c-swimlane {\n      flex: 1 0 auto;\n    }\n\n    .c-swimlane__lane-object {\n      display: flex;\n    }\n  }\n\n  &__activity {\n    cursor: pointer;\n\n    &[s-selected] {\n      rect,\n      use {\n        outline-style: dotted;\n        outline-width: 2px;\n        stroke: $colorGanttSelectedBorder;\n        stroke-width: 2px;\n      }\n    }\n  }\n\n  &__activity-label {\n    &--outside-rect {\n      fill: $colorBodyFg !important;\n    }\n  }\n\n  canvas {\n    display: none;\n  }\n}\n\n.c-plan-av {\n  // Activities view\n  background-color: $colorPlotBg;\n  flex: 1 1 auto;\n  height: 100%;\n\n  &__svg {\n    width: 100%;\n  }\n}\n\n// When in a Time Strip view\n.c-timeline__objects {\n  .is-object-type-plan {\n    overflow-x: hidden;\n    overflow-y: scroll !important; // `scroll` ensures that right edges align in time\n  }\n}\n"
  },
  {
    "path": "src/plugins/plan/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport activityStatesInterceptor from '../activityStates/activityStatesInterceptor.js';\nimport { createActivityStatesIdentifier } from '../activityStates/createActivityStatesIdentifier.js';\nimport { createPlanExecutionMonitoringIdentifier } from '../planExecutionMonitoring/planExecutionMonitoringIdentifier.js';\nimport planExecutionMonitoringInterceptor from '../planExecutionMonitoring/planExecutionMonitoringInterceptor';\nimport ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';\nimport ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';\nimport GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';\nimport PlanInspectorViewProvider from './inspector/PlanInspectorViewProvider';\nimport { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';\nimport PlanViewProvider from './PlanViewProvider.js';\n\nconst PLAN_EXECUTION_MONITORING_DEFAULT_NAME = 'Plan Execution Monitoring';\nconst ACTIVITY_STATES_DEFAULT_NAME = 'Activity States';\n/**\n * @typedef {Object} PlanOptions\n * @property {boolean} creatable true/false to allow creation of a plan via the Create menu.\n * @property {string} name The name of the activity states model.\n * @property {string} namespace the namespace to use for the activity states object.\n * @property {number} priority the priority of the interceptor. By default, it is low.\n */\n\n/**\n *\n * @param {PlanOptions} options\n * @returns {*} (any)\n */\nexport default function (options = {}) {\n  return function install(openmct) {\n    openmct.types.addType('plan', {\n      name: 'Plan',\n      key: 'plan',\n      description: 'A non-configurable timeline-like view for a compatible plan file.',\n      creatable: options.creatable ?? false,\n      cssClass: 'icon-plan',\n      form: [\n        {\n          name: 'Upload Plan (JSON File)',\n          key: 'selectFile',\n          control: 'file-input',\n          required: true,\n          text: 'Select File...',\n          type: 'application/json',\n          property: ['selectFile']\n        }\n      ],\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames,\n          aheadBehind: {\n            duration: 0,\n            isBehind: false\n          }\n        };\n      }\n    });\n    // Name TBD and subject to change\n    openmct.types.addType('gantt-chart', {\n      name: 'Gantt Chart',\n      key: 'gantt-chart',\n      description: 'A configurable timeline-like view for a compatible plan file.',\n      creatable: true,\n      cssClass: 'icon-plan',\n      form: [],\n      initialize(domainObject) {\n        domainObject.configuration = {\n          clipActivityNames: true\n        };\n        domainObject.composition = [];\n      }\n    });\n    openmct.objectViews.addProvider(new PlanViewProvider(openmct));\n\n    openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));\n\n    openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));\n\n    //add activity states get interceptor\n    const { name = ACTIVITY_STATES_DEFAULT_NAME, namespace = '', priority } = options;\n    const identifier = createActivityStatesIdentifier(namespace);\n\n    openmct.objects.addGetInterceptor(\n      activityStatesInterceptor(openmct, { identifier, name, priority })\n    );\n\n    //add plan execution monitoring get interceptor\n    const {\n      plan_exec_monitoring_obj_name = PLAN_EXECUTION_MONITORING_DEFAULT_NAME,\n      plan_exec_monitoring_obj_namespace = '',\n      plan_exec_monitoring_obj_priority = openmct.priority.LOW\n    } = options;\n    const plan_exec_monitoring_obj_identifier = createPlanExecutionMonitoringIdentifier(\n      plan_exec_monitoring_obj_namespace\n    );\n\n    openmct.objects.addGetInterceptor(\n      planExecutionMonitoringInterceptor(openmct, {\n        identifier: plan_exec_monitoring_obj_identifier,\n        name: plan_exec_monitoring_obj_name,\n        priority: plan_exec_monitoring_obj_priority\n      })\n    );\n  };\n}\n"
  },
  {
    "path": "src/plugins/plan/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport Properties from '../inspectorViews/properties/PropertiesComponent.vue';\nimport PlanPlugin from '../plan/plugin.js';\n\ndescribe('the plugin', function () {\n  let planDefinition;\n  let ganttDefinition;\n  let element;\n  let child;\n  let openmct;\n  let appHolder;\n  let originalRouterPath;\n\n  beforeEach((done) => {\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n\n    const timeSystemOptions = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 1597160002854,\n        end: 1597181232854\n      }\n    };\n\n    openmct = createOpenMct(timeSystemOptions);\n    openmct.install(new PlanPlugin());\n\n    planDefinition = openmct.types.get('plan').definition;\n    ganttDefinition = openmct.types.get('gantt-chart').definition;\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    originalRouterPath = openmct.router.path;\n\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    openmct.router.path = originalRouterPath;\n\n    return resetApplicationState(openmct);\n  });\n\n  let mockPlanObject = {\n    name: 'Plan',\n    key: 'plan',\n    creatable: false\n  };\n\n  let mockGanttObject = {\n    name: 'Gantt',\n    key: 'gantt-chart',\n    creatable: true\n  };\n\n  describe('the plan type', () => {\n    it('defines a plan object type with the correct key', () => {\n      expect(planDefinition.key).toEqual(mockPlanObject.key);\n    });\n    it('is not creatable', () => {\n      expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);\n    });\n  });\n  describe('the gantt-chart type', () => {\n    it('defines a gantt-chart object type with the correct key', () => {\n      expect(ganttDefinition.key).toEqual(mockGanttObject.key);\n    });\n    it('is creatable', () => {\n      expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable);\n    });\n  });\n\n  describe('the plan view', () => {\n    it('provides a plan view', () => {\n      const testViewObject = {\n        id: 'test-object',\n        type: 'plan'\n      };\n      openmct.router.path = [testViewObject];\n\n      const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);\n      let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');\n      expect(planView).toBeDefined();\n    });\n\n    it('is not an editable view', () => {\n      const testViewObject = {\n        id: 'test-object',\n        type: 'plan'\n      };\n      openmct.router.path = [testViewObject];\n\n      const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);\n      let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');\n      expect(planView.canEdit(testViewObject)).toBeFalse();\n    });\n  });\n\n  describe('the plan view displays activities', () => {\n    let planDomainObject;\n    let mockObjectPath = [\n      {\n        identifier: {\n          key: 'test',\n          namespace: ''\n        },\n        type: 'time-strip',\n        name: 'Test Parent Object'\n      }\n    ];\n    let planView;\n\n    beforeEach(() => {\n      openmct.time.timeSystem('utc', {\n        start: 1597160002854,\n        end: 1597181232854\n      });\n\n      planDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: 'plan',\n        id: 'test-object',\n        selectFile: {\n          body: JSON.stringify({\n            'TEST-GROUP': [\n              {\n                name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n                start: 1597170002854,\n                end: 1597171032854,\n                type: 'TEST-GROUP',\n                color: 'fuchsia',\n                textColor: 'black'\n              },\n              {\n                name: 'Sed ut perspiciatis',\n                start: 1597171132854,\n                end: 1597171232854,\n                type: 'TEST-GROUP',\n                color: 'fuchsia',\n                textColor: 'black'\n              }\n            ]\n          })\n        }\n      };\n\n      openmct.router.path = [planDomainObject];\n\n      const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]);\n      planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');\n      let view = planView.view(planDomainObject, mockObjectPath);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('loads activities into the view', () => {\n      const svgEls = element.querySelectorAll('.c-plan__contents svg');\n      expect(svgEls.length).toEqual(1);\n    });\n\n    it('displays the group label', () => {\n      const labelEl = element.querySelector(\n        '.c-plan__contents .c-object-label .c-object-label__name'\n      );\n      expect(labelEl.innerHTML).toMatch(/TEST-GROUP/);\n    });\n\n    it('displays the activities and their labels', async () => {\n      const bounds = {\n        start: 1597160002854,\n        end: 1597181232854\n      };\n\n      openmct.time.bounds(bounds);\n\n      await nextTick();\n      const rectEls = element.querySelectorAll('.c-plan__contents use');\n      expect(rectEls.length).toEqual(2);\n      const textEls = element.querySelectorAll('.c-plan__contents text');\n      expect(textEls.length).toEqual(3);\n    });\n\n    it('shows the status indicator when available', async () => {\n      openmct.status.set(\n        {\n          key: 'test-object',\n          namespace: ''\n        },\n        'draft'\n      );\n\n      await nextTick();\n      const statusEl = element.querySelector('.c-plan__contents .is-status--draft');\n      expect(statusEl).toBeDefined();\n    });\n  });\n\n  describe('the plan version', () => {\n    let component;\n    let componentObject;\n    let _destroy;\n    let testPlanObject = {\n      name: 'Plan',\n      type: 'plan',\n      identifier: {\n        key: 'test-plan',\n        namespace: ''\n      },\n      created: 123456789,\n      modified: 123456790,\n      version: 'v1'\n    };\n\n    beforeEach(async () => {\n      openmct.selection.select(\n        [\n          {\n            element: element,\n            context: {\n              item: testPlanObject\n            }\n          },\n          {\n            element: openmct.layout.$refs.browseObject.$el,\n            context: {\n              item: testPlanObject,\n              supportsMultiSelect: false\n            }\n          }\n        ],\n        false\n      );\n\n      await nextTick();\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount({\n        components: {\n          Properties\n        },\n        provide: {\n          openmct: openmct\n        },\n        template: '<properties ref=\"root\"/>'\n      });\n      _destroy = destroy;\n      component = vNode.componentInstance;\n    });\n\n    afterEach(() => {\n      _destroy();\n    });\n\n    it('provides an inspector view with the version information if available', () => {\n      componentObject = component.$refs.root;\n      const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');\n      const found = Array.from(propertiesEls).some((propertyEl) => {\n        return (\n          propertyEl.children[0].innerHTML.trim() === 'Version' &&\n          propertyEl.children[1].innerHTML.trim() === 'v1'\n        );\n      });\n      expect(found).toBeTrue();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/plan/util.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * The SourceMap allows mapping specific implementations of plan domain objects to those expected by Open MCT.\n * @typedef {Object} SourceMapOption\n * @property {string} orderedGroups the property of the plan that lists groups/swim lanes specifying what order they will be displayed in Open MCT.\n * @property {string} activities the property of the plan that has the list of activities to be displayed.\n * @property {string} groupId the property of the activity that maps to the group/swim lane it should be displayed in.\n * @property {string} start The start time property of the activity\n * @property {string} end The end time property of the activity\n * @property {string} id The unique id of the activity. This is required to allow setting activity states\n * @property {Object} displayProperties a list of key: value pairs that specifies which properties of the activity should be displayed when it is selected. Ex. {'location': 'Location', 'metadata.length_in_meters', 'Length (meters)'}\n * @property {Object} filterMetadata a list of strings that specifies which properties of the activity be included for filtering. Ex. {'description','properties.length_in_meters'}\n */\n\nimport _ from 'lodash';\nexport function getValidatedData(domainObject) {\n  const sourceMap = domainObject.sourceMap;\n  const json = getObjectJson(domainObject);\n\n  if (\n    sourceMap !== undefined &&\n    sourceMap.activities !== undefined &&\n    sourceMap.groupId !== undefined\n  ) {\n    let mappedJson = {};\n    json[sourceMap.activities].forEach((activity) => {\n      if (activity[sourceMap.groupId]) {\n        const groupIdKey = activity[sourceMap.groupId];\n        let groupActivity = {\n          ...activity\n        };\n\n        if (sourceMap.start) {\n          groupActivity.start = activity[sourceMap.start];\n        }\n\n        if (sourceMap.end) {\n          groupActivity.end = activity[sourceMap.end];\n        }\n\n        if (Array.isArray(sourceMap.filterMetadata)) {\n          groupActivity.filterMetadataValues = [];\n          sourceMap.filterMetadata.forEach((property) => {\n            const value = _.get(activity, property);\n            if (value !== undefined && value !== null) {\n              groupActivity.filterMetadataValues.push(value);\n            }\n          });\n        }\n\n        if (sourceMap.id) {\n          groupActivity.id = activity[sourceMap.id];\n        }\n\n        if (sourceMap.displayProperties) {\n          groupActivity.displayProperties = sourceMap.displayProperties;\n        }\n\n        if (!mappedJson[groupIdKey]) {\n          mappedJson[groupIdKey] = [];\n        }\n\n        mappedJson[groupIdKey].push(groupActivity);\n      }\n    });\n\n    return mappedJson;\n  } else {\n    return json;\n  }\n}\n\nfunction getObjectJson(domainObject) {\n  const body = domainObject.selectFile?.body;\n  let json = {};\n  if (typeof body === 'string') {\n    try {\n      json = JSON.parse(body);\n    } catch (e) {\n      return json;\n    }\n  } else if (body !== undefined) {\n    json = body;\n  }\n\n  return json;\n}\n\nexport function getValidatedGroups(domainObject, planData) {\n  let orderedGroupNames;\n  const sourceMap = domainObject.sourceMap;\n  const json = getObjectJson(domainObject);\n  if (sourceMap?.orderedGroups) {\n    const groups = json[sourceMap.orderedGroups];\n    if (groups.length && typeof groups[0] === 'object') {\n      //if groups is a list of objects, then get the name property from each group object.\n      const groupsWithNames = groups.filter(\n        (groupObj) => groupObj.name !== undefined && groupObj.name !== ''\n      );\n      orderedGroupNames = groupsWithNames.map((groupObj) => groupObj.name);\n    } else {\n      // Otherwise, groups is likely a list of names, so use that.\n      orderedGroupNames = groups;\n    }\n  }\n  if (orderedGroupNames === undefined) {\n    orderedGroupNames = Object.keys(planData);\n  }\n\n  return orderedGroupNames;\n}\n\nexport function getDisplayProperties(activity) {\n  let displayProperties = {};\n  function extractProperties(properties, useKeyAsLabel = false) {\n    Object.keys(properties).forEach((key) => {\n      const label = useKeyAsLabel ? key : properties[key];\n      const value = _.get(activity, key);\n      if (value) {\n        displayProperties[key] = { label, value };\n      }\n    });\n  }\n\n  if (activity?.displayProperties) {\n    extractProperties(activity.displayProperties);\n  } else if (activity?.properties) {\n    extractProperties(activity.properties, true);\n  }\n  return displayProperties;\n}\n\nexport function getFilteredValues(activity) {\n  let values = [];\n  if (Array.isArray(activity.filterMetadataValues)) {\n    values = activity.filterMetadataValues;\n  } else if (activity?.properties) {\n    values = Object.values(activity.properties);\n  }\n\n  return values;\n}\n\nexport function getContrastingColor(hexColor) {\n  function cutHex(h, start, end) {\n    const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;\n\n    return parseInt(hStr.substring(start, end), 16);\n  }\n\n  // https://codepen.io/davidhalford/pen/ywEva/\n  const cThreshold = 130;\n\n  if (hexColor.indexOf('#') === -1) {\n    // We weren't given a hex color\n    return '#ff0000';\n  }\n\n  const hR = cutHex(hexColor, 0, 2);\n  const hG = cutHex(hexColor, 2, 4);\n  const hB = cutHex(hexColor, 4, 6);\n\n  const cBrightness = (hR * 299 + hG * 587 + hB * 114) / 1000;\n\n  return cBrightness > cThreshold ? '#000000' : '#ffffff';\n}\n"
  },
  {
    "path": "src/plugins/planExecutionMonitoring/planExecutionMonitoringIdentifier.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const PLAN_EXECUTION_MONITORING_KEY = 'plan-execution-monitoring';\n\nexport function createPlanExecutionMonitoringIdentifier(namespace = '') {\n  return {\n    key: PLAN_EXECUTION_MONITORING_KEY,\n    namespace\n  };\n}\n"
  },
  {
    "path": "src/plugins/planExecutionMonitoring/planExecutionMonitoringInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { PLAN_EXECUTION_MONITORING_KEY } from './planExecutionMonitoringIdentifier.js';\n\n/**\n * @typedef {Object} PlanExecutionMonitoringOptions\n * @property {import('openmct').Identifier} identifier the {namespace, key} to use for the plan execution monitoring object.\n * @property {string} name The name of the plan execution monitoring model.\n * @property {number} priority the priority of the interceptor. By default, it is low.\n */\n\n/**\n * Creates an plan execution monitoring object in the persistence store. This is used to save plan plan execution monitoring.\n * This will only get invoked when an attempt is made to save the execution monitoring information for a plan and no plan execution monitoring object exists in the store.\n * @param {import('../../../openmct').OpenMCT} openmct\n * @param {PlanExecutionMonitoringOptions} options\n * @returns {Object}\n */\n\nfunction planExecutionMonitoringInterceptor(openmct, options) {\n  const { identifier, name, priority } = options;\n  const planExecutionMonitoringModel = {\n    identifier,\n    name,\n    type: PLAN_EXECUTION_MONITORING_KEY,\n    execution_monitoring: {},\n    location: null\n  };\n\n  return {\n    appliesTo: (identifierObject) => {\n      return identifierObject.key === PLAN_EXECUTION_MONITORING_KEY;\n    },\n    invoke: (identifierObject, object) => {\n      if (!object || openmct.objects.isMissing(object)) {\n        openmct.objects.save(planExecutionMonitoringModel);\n\n        return planExecutionMonitoringModel;\n      }\n\n      return object;\n    },\n    priority\n  };\n}\n\nexport default planExecutionMonitoringInterceptor;\n"
  },
  {
    "path": "src/plugins/planExecutionMonitoring/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport {\n  createPlanExecutionMonitoringIdentifier,\n  PLAN_EXECUTION_MONITORING_KEY\n} from './planExecutionMonitoringIdentifier.js';\n\nconst MISSING_NAME = `Missing: ${PLAN_EXECUTION_MONITORING_KEY}`;\nconst DEFAULT_NAME = 'Plan Execution Monitoring';\nconst planExecutionMonitoringIdentifier = createPlanExecutionMonitoringIdentifier();\n\ndescribe('the plugin', () => {\n  let openmct;\n  let missingObj = {\n    identifier: planExecutionMonitoringIdentifier,\n    type: 'unknown',\n    name: MISSING_NAME\n  };\n\n  describe('with no arguments passed in', () => {\n    beforeEach((done) => {\n      openmct = createOpenMct();\n      openmct.install(openmct.plugins.PlanLayout());\n\n      openmct.on('start', done);\n      openmct.startHeadless();\n    });\n\n    afterEach(() => {\n      return resetApplicationState(openmct);\n    });\n\n    it('when installed, adds \"Plan Execution Monitoring\"', async () => {\n      const planExecutionMonitoringObject = await openmct.objects.get(\n        planExecutionMonitoringIdentifier\n      );\n      expect(planExecutionMonitoringObject.name).toBe(DEFAULT_NAME);\n      expect(planExecutionMonitoringObject).toBeDefined();\n    });\n\n    describe('adds an interceptor that returns a \"Plan Execution Monitoring\" model for', () => {\n      let planExecutionMonitoringObject;\n      let mockNotFoundProvider;\n      let activeProvider;\n\n      beforeEach(async () => {\n        mockNotFoundProvider = {\n          get: () => Promise.reject(new Error('Not found')),\n          create: () => Promise.resolve(missingObj),\n          update: () => Promise.resolve(missingObj)\n        };\n\n        activeProvider = mockNotFoundProvider;\n        spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);\n        planExecutionMonitoringObject = await openmct.objects.get(\n          planExecutionMonitoringIdentifier\n        );\n      });\n\n      it('missing objects', () => {\n        let idsMatch = openmct.objects.areIdsEqual(\n          planExecutionMonitoringObject.identifier,\n          planExecutionMonitoringIdentifier\n        );\n\n        expect(planExecutionMonitoringObject).toBeDefined();\n        expect(idsMatch).toBeTrue();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/plot/LinearScale.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*jscs:disable disallowDanglingUnderscores */\n/**\n * A scale has an input domain and an output range.  It provides functions\n * `scale` return the range value associated with a domain value.\n * `invert` return the domain value associated with range value.\n */\n\nclass LinearScale {\n  constructor(domain) {\n    this.domain(domain);\n  }\n\n  domain(newDomain) {\n    if (newDomain) {\n      this._domain = newDomain;\n      this._domainDenominator = newDomain.max - newDomain.min;\n    }\n\n    return this._domain;\n  }\n\n  range(newRange) {\n    if (newRange) {\n      this._range = newRange;\n      this._rangeDenominator = newRange.max - newRange.min;\n    }\n\n    return this._range;\n  }\n\n  scale(domainValue) {\n    if (!this._domain || !this._range) {\n      return;\n    }\n\n    const domainOffset = domainValue - this._domain.min;\n    const rangeFraction = domainOffset - this._domainDenominator;\n    const rangeOffset = rangeFraction * this._rangeDenominator;\n    const rangeValue = rangeOffset + this._range.min;\n\n    return rangeValue;\n  }\n\n  invert(rangeValue) {\n    if (!this._domain || !this._range) {\n      return;\n    }\n\n    const rangeOffset = rangeValue - this._range.min;\n    const domainFraction = rangeOffset / this._rangeDenominator;\n    const domainOffset = domainFraction * this._domainDenominator;\n    const domainValue = domainOffset + this._domain.min;\n\n    return domainValue;\n  }\n}\n\nexport default LinearScale;\n\n/**\n *\n */\n"
  },
  {
    "path": "src/plugins/plot/MctPlot.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"loaded\"\n    ref=\"plot\"\n    class=\"gl-plot\"\n    :class=\"{ 'js-series-data-loaded': seriesDataLoaded }\"\n  >\n    <slot></slot>\n    <div class=\"plot-wrapper-axis-and-display-area flex-elem grows\">\n      <div v-if=\"seriesModels.length\" class=\"u-contents\">\n        <YAxis\n          v-for=\"(yAxis, index) in yAxesIds\"\n          :id=\"yAxis.id\"\n          :key=\"`yAxis-${yAxis.id}-${index}`\"\n          :position=\"yAxis.id > 2 ? 'right' : 'left'\"\n          :class=\"{ 'plot-yaxis-right': yAxis.id > 2 }\"\n          @y-key-changed=\"setYAxisKey\"\n          @toggle-axis-visibility=\"toggleSeriesForYAxis\"\n        />\n      </div>\n      <div class=\"gl-plot-wrapper-display-area-and-x-axis\" :style=\"xAxisStyle\">\n        <div class=\"gl-plot-display-area has-local-controls has-cursor-guides\">\n          <div class=\"l-state-indicators\">\n            <span\n              class=\"l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle\"\n              title=\"This plot is not currently displaying the latest data. Reset pan/zoom to view latest data.\"\n            ></span>\n          </div>\n\n          <MctTicks\n            v-show=\"gridLines && !options.compact\"\n            :axis-type=\"'xAxis'\"\n            :position=\"'right'\"\n            :is-utc=\"isUtc\"\n          />\n\n          <MctTicks\n            v-for=\"(yAxis, index) in yAxesIds\"\n            v-show=\"gridLines\"\n            :key=\"`yAxis-gridlines-${index}`\"\n            :axis-type=\"'yAxis'\"\n            :position=\"'bottom'\"\n            :axis-id=\"yAxis.id\"\n          />\n\n          <div\n            ref=\"chartContainer\"\n            class=\"gl-plot-chart-wrapper\"\n            :class=\"[{ 'alt-pressed': altPressed }]\"\n          >\n            <MctChart\n              :rectangles=\"rectangles\"\n              :highlights=\"highlights\"\n              :show-limit-line-labels=\"limitLineLabels\"\n              :annotated-points-by-series=\"annotatedPointsBySeries\"\n              :annotation-selections-by-series=\"annotationSelectionsBySeries\"\n              :hidden-y-axis-ids=\"hiddenYAxisIds\"\n              :annotation-viewing-and-editing-allowed=\"annotationViewingAndEditingAllowed\"\n              @plot-reinitialize-canvas=\"initCanvas\"\n              @chart-loaded=\"initialize\"\n            />\n          </div>\n\n          <div\n            class=\"gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover\"\n          >\n            <div v-if=\"!options.compact\" class=\"c-button-set c-button-set--strip-h js-zoom\">\n              <button\n                class=\"c-button icon-minus\"\n                title=\"Zoom out\"\n                aria-label=\"Zoom out\"\n                @click=\"zoom('out', 0.2)\"\n              ></button>\n              <button\n                class=\"c-button icon-plus\"\n                title=\"Zoom in\"\n                aria-label=\"Zoom in\"\n                @click=\"zoom('in', 0.2)\"\n              ></button>\n            </div>\n            <div\n              v-if=\"plotHistory.length && !options.compact\"\n              class=\"c-button-set c-button-set--strip-h js-pan\"\n            >\n              <button\n                class=\"c-button icon-arrow-left\"\n                title=\"Restore previous pan and zoom\"\n                aria-label=\"Restore previous pan and zoom\"\n                @click=\"back()\"\n              ></button>\n              <button\n                class=\"c-button icon-reset\"\n                title=\"Reset pan and zoom\"\n                aria-label=\"Reset pan and zoom\"\n                @click=\"resumeRealtimeData()\"\n              ></button>\n            </div>\n            <div\n              v-if=\"isRealTime && !options.compact\"\n              class=\"c-button-set c-button-set--strip-h js-pause\"\n            >\n              <button\n                v-if=\"!isFrozen\"\n                class=\"c-button icon-pause\"\n                title=\"Pause incoming real-time data\"\n                aria-label=\"Pause incoming real-time data\"\n                @click=\"pause()\"\n              ></button>\n              <button\n                v-if=\"isFrozen\"\n                class=\"c-button icon-arrow-right pause-play is-paused\"\n                title=\"Resume displaying real-time data\"\n                aria-label=\"Resume displaying real-time data\"\n                @click=\"resumeRealtimeData()\"\n              ></button>\n            </div>\n            <div v-if=\"isTimeOutOfSync || isFrozen\" class=\"c-button-set c-button-set--strip-h\">\n              <button\n                class=\"c-button icon-clock\"\n                title=\"Synchronize Time Conductor\"\n                aria-label=\"Synchronize Time Conductor\"\n                @click=\"showSynchronizeDialog()\"\n              ></button>\n            </div>\n            <div class=\"c-button-set c-button-set--strip-h\">\n              <button\n                class=\"c-button icon-crosshair\"\n                :class=\"{ 'is-active': cursorGuide }\"\n                title=\"Toggle cursor guides\"\n                aria-label=\"Toggle cursor guides\"\n                @click=\"toggleCursorGuide\"\n              ></button>\n              <button\n                class=\"c-button\"\n                :class=\"{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }\"\n                title=\"Toggle grid lines\"\n                aria-label=\"Toggle grid lines\"\n                @click=\"toggleGridLines\"\n              ></button>\n            </div>\n          </div>\n\n          <!--Cursor guides-->\n          <div\n            v-show=\"cursorGuide\"\n            ref=\"cursorGuideVertical\"\n            aria-label=\"Vertical cursor guide\"\n            class=\"c-cursor-guide--v js-cursor-guide--v\"\n          ></div>\n          <div\n            v-show=\"cursorGuide\"\n            ref=\"cursorGuideHorizontal\"\n            aria-label=\"Horizontal cursor guide\"\n            class=\"c-cursor-guide--h js-cursor-guide--h\"\n          ></div>\n        </div>\n        <XAxis v-if=\"seriesModels.length > 0 && !options.compact\" :series-model=\"seriesModels[0]\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Flatbush from 'flatbush';\nimport _ from 'lodash';\nimport { useEventBus } from 'utils/useEventBus';\nimport { inject, toRaw } from 'vue';\n\nimport { MODES } from '../../api/time/constants';\nimport { useAlignment } from '../../ui/composables/alignmentContext.js';\nimport TagEditorClassNames from '../inspectorViews/annotations/tags/TagEditorClassNames.js';\nimport XAxis from './axis/XAxis.vue';\nimport YAxis from './axis/YAxis.vue';\nimport MctChart from './chart/MctChart.vue';\nimport configStore from './configuration/ConfigStore.js';\nimport PlotConfigurationModel from './configuration/PlotConfigurationModel.js';\nimport eventHelpers from './lib/eventHelpers.js';\nimport LinearScale from './LinearScale.js';\nimport MctTicks from './MctTicks.vue';\n\nconst OFFSET_THRESHOLD = 10;\nconst AXES_PADDING = 20;\n\nexport default {\n  components: {\n    XAxis,\n    YAxis,\n    MctTicks,\n    MctChart\n  },\n  inject: ['openmct', 'domainObject', 'objectPath', 'renderWhenVisible'],\n  props: {\n    options: {\n      type: Object,\n      default() {\n        return {\n          compact: false\n        };\n      }\n    },\n    initGridLines: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    initCursorGuide: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    limitLineLabels: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    colorPalette: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  emits: [\n    'config-loaded',\n    'cursor-guide',\n    'grid-lines',\n    'loading-complete',\n    'loading-updated',\n    'highlights',\n    'lock-highlight-point',\n    'status-updated'\n  ],\n  setup() {\n    const { EventBus } = useEventBus();\n\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData, reset: resetAlignment } = useAlignment(\n      domainObject,\n      objectPath,\n      openmct\n    );\n\n    return {\n      EventBus,\n      alignmentData,\n      resetAlignment\n    };\n  },\n  data() {\n    return {\n      altPressed: false,\n      annotatedPointsBySeries: {},\n      highlights: [],\n      annotationSelectionsBySeries: {},\n      annotationsEverLoaded: false,\n      lockHighlightPoint: false,\n      yKeyOptions: [],\n      yAxisLabel: '',\n      rectangles: [],\n      plotHistory: [],\n      selectedXKeyOption: {},\n      xKeyOptions: [],\n      pending: 0,\n      isRealTime: this.openmct.time.isRealTime(),\n      loaded: false,\n      isTimeOutOfSync: false,\n      isFrozenOnMouseDown: false,\n      cursorGuide: this.initCursorGuide,\n      gridLines: this.initGridLines,\n      yAxes: [],\n      hiddenYAxisIds: [],\n      yAxisListWithRange: [],\n      config: {},\n      isUtc: this.openmct.time.getTimeSystem().isUTCBased\n    };\n  },\n  computed: {\n    xAxisStyle() {\n      let leftOffset = 0;\n      if (this.alignmentData.leftWidth) {\n        leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;\n      }\n      let style = {\n        left: `${this.alignmentData.leftWidth + leftOffset}px`\n      };\n\n      if (this.alignmentData.rightWidth) {\n        style.right = `${this.alignmentData.rightWidth + AXES_PADDING}px`;\n      }\n\n      return style;\n    },\n    yAxesIds() {\n      return this.yAxes.filter((yAxis) => yAxis.seriesCount > 0);\n    },\n    isNestedWithinAStackedPlot() {\n      const isNavigatedObject = this.openmct.router.isNavigatedObject(\n        [this.domainObject].concat(this.objectPath)\n      );\n\n      return (\n        !isNavigatedObject &&\n        this.objectPath.find(\n          (pathObject, pathObjIndex) => pathObject.type === 'telemetry.plot.stacked'\n        )\n      );\n    },\n    isFrozen() {\n      return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;\n    },\n    annotationViewingAndEditingAllowed() {\n      // only allow annotations viewing/editing if plot is paused or in fixed time mode\n      return this.isFrozen || !this.isRealTime;\n    },\n    seriesDataLoaded() {\n      return this.pending === 0 && this.loaded;\n    }\n  },\n  watch: {\n    initGridLines(newGridLines) {\n      this.gridLines = newGridLines;\n    },\n    initCursorGuide(newCursorGuide) {\n      this.cursorGuide = newCursorGuide;\n    }\n  },\n  created() {\n    this.abortController = new AbortController();\n  },\n  mounted() {\n    this.seriesModels = [];\n    this.config = {};\n    this.yAxisIdVisibility = {};\n    this.offsetWidth = 0;\n\n    document.addEventListener('keydown', this.handleKeyDown);\n    document.addEventListener('keyup', this.handleKeyUp);\n    eventHelpers.extend(this);\n    this.updateMode = this.updateMode.bind(this);\n    this.updateDisplayBounds = this.updateDisplayBounds.bind(this);\n    this.setTimeContext = this.setTimeContext.bind(this);\n\n    this.config = this.getConfig();\n    this.yAxes = [\n      {\n        id: this.config.yAxis.id,\n        seriesCount: 0\n      }\n    ];\n    if (this.config.additionalYAxes) {\n      this.yAxes = this.yAxes.concat(\n        this.config.additionalYAxes.map((yAxis) => {\n          return {\n            id: yAxis.id,\n            seriesCount: 0\n          };\n        })\n      );\n    }\n\n    this.$emit('config-loaded', true);\n\n    this.listenTo(this.config.series, 'add', this.addSeries, this);\n    this.listenTo(this.config.series, 'remove', this.removeSeries, this);\n\n    this.config.series.models.forEach(this.addSeries, this);\n\n    this.filterObserver = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration.filters',\n      this.updateFiltersAndResubscribe\n    );\n    this.removeStatusListener = this.openmct.status.observe(\n      this.domainObject.identifier,\n      this.updateStatus\n    );\n\n    this.openmct.objectViews.on('clearData', this.clearData);\n    this.EventBus.$on('loading-complete', this.loadAnnotationsIfAllowed);\n    this.openmct.selection.on('change', this.updateSelection);\n    this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];\n\n    this.$nextTick(() => {\n      this.setTimeContext();\n      this.loaded = true;\n    });\n  },\n  beforeUnmount() {\n    this.resetAlignment();\n    this.abortController.abort();\n    this.openmct.selection.off('change', this.updateSelection);\n    document.removeEventListener('keydown', this.handleKeyDown);\n    document.removeEventListener('keyup', this.handleKeyUp);\n    document.body.removeEventListener('click', this.cancelSelection);\n    this.EventBus.$off('loading-complete', this.loadAnnotationsIfAllowed);\n    this.destroy();\n  },\n  methods: {\n    async updateSelection(selection) {\n      const selectionContext = selection?.[0]?.[0]?.context?.item;\n      // on clicking on a search result we highlight the annotation and zoom - we know it's an annotation result when isAnnotationSearchResult === true\n      // We shouldn't zoom when we're selecting existing annotations to view them or creating new annotations.\n      const selectionType = selection?.[0]?.[0]?.context?.type;\n      const validSelectionTypes = ['clicked-on-plot-selection', 'annotation-search-result'];\n      const isAnnotationSearchResult = selectionType === 'annotation-search-result';\n\n      if (!validSelectionTypes.includes(selectionType)) {\n        // wrong type of selection\n        return;\n      }\n\n      if (\n        selectionContext &&\n        !isAnnotationSearchResult &&\n        this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)\n      ) {\n        return;\n      }\n\n      await this.waitForAxesToLoad();\n      const selectedAnnotations = selection?.[0]?.[0]?.context?.annotations;\n      //This section is only for the annotations search results entry to displaying annotations\n      if (isAnnotationSearchResult) {\n        this.showAnnotationsFromSearchResults(selectedAnnotations);\n      }\n\n      //This section is common to all entry points for annotation display\n      this.prepareExistingAnnotationSelection(selectedAnnotations);\n    },\n    cancelSelection(event) {\n      if (this.$refs?.plot) {\n        const clickedInsidePlot = this.$refs.plot.contains(event.target);\n        // unfortunate side effect from possibly being detached from the DOM when\n        // adding/deleting tags, so closest() won't work\n        const clickedTagEditor = Object.values(TagEditorClassNames).some((className) => {\n          return event.target.classList.contains(className);\n        });\n        const clickedInsideInspector = event.target.closest('.js-inspector') !== null;\n        const clickedOption = event.target.closest('.js-autocomplete-options') !== null;\n        if (!clickedInsidePlot && !clickedInsideInspector && !clickedOption && !clickedTagEditor) {\n          this.rectangles = [];\n          this.annotationSelectionsBySeries = {};\n          this.selectPlot();\n          document.body.removeEventListener('click', this.cancelSelection);\n        }\n      }\n    },\n    waitForAxesToLoad() {\n      return new Promise((resolve) => {\n        // When there is no plot data, the ranges can be undefined\n        // in which case we should not perform selection.\n        const currentXaxis = this.config.xAxis.get('displayRange');\n        const currentYaxis = this.config.yAxis.get('displayRange');\n        if (!currentXaxis || !currentYaxis) {\n          this.EventBus.$once('loading-complete', resolve);\n        } else {\n          resolve();\n        }\n      });\n    },\n    showAnnotationsFromSearchResults(selectedAnnotations) {\n      if (selectedAnnotations?.length) {\n        // pause the plot if we haven't already so we can actually display\n        // the annotations\n        this.freeze();\n        // just use first annotation\n        const boundingBoxes = selectedAnnotations[0].targets;\n        let minX = Number.MAX_SAFE_INTEGER;\n        let minY = Number.MAX_SAFE_INTEGER;\n        let maxX = Number.MIN_SAFE_INTEGER;\n        let maxY = Number.MIN_SAFE_INTEGER;\n        boundingBoxes.forEach((boundingBox) => {\n          if (boundingBox.minX < minX) {\n            minX = boundingBox.minX;\n          }\n\n          if (boundingBox.maxX > maxX) {\n            maxX = boundingBox.maxX;\n          }\n\n          if (boundingBox.maxY > maxY) {\n            maxY = boundingBox.maxY;\n          }\n\n          if (boundingBox.minY < minY) {\n            minY = boundingBox.minY;\n          }\n        });\n\n        this.config.xAxis.set('displayRange', {\n          min: minX,\n          max: maxX\n        });\n        this.config.yAxis.set('displayRange', {\n          min: minY,\n          max: maxY\n        });\n        //Zoom out just a touch so that the highlighted section for annotations doesn't take over the whole view - which is not a nice look.\n        this.zoom('out', 0.2);\n      }\n    },\n    handleKeyDown(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = true;\n      }\n    },\n    handleKeyUp(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = false;\n      }\n    },\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.objectPath);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.updateMode();\n      this.updateDisplayBounds(this.timeContext.getBounds());\n      this.timeContext.on('modeChanged', this.updateMode);\n      this.timeContext.on('timeSystemChanged', this.setUtc);\n      this.timeContext.on('boundsChanged', this.updateDisplayBounds);\n      this.synchronized(true);\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off('modeChanged', this.updateMode);\n        this.timeContext.off('timeSystemChanged', this.setUtc);\n        this.timeContext.off('boundsChanged', this.updateDisplayBounds);\n      }\n    },\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      let config = configStore.get(configId);\n      if (!config) {\n        config = new PlotConfigurationModel({\n          id: configId,\n          domainObject: this.domainObject,\n          openmct: this.openmct,\n          palette: this.colorPalette,\n          callback: (data) => {\n            this.data = data;\n          }\n        });\n        configStore.add(configId, config);\n      }\n\n      return config;\n    },\n    addSeries(series, index) {\n      const yAxisId = series.get('yAxisId');\n      this.updateAxisUsageCount(yAxisId, 1);\n      this.seriesModels[index] = series;\n      this.listenTo(series, 'change:xKey', this.setDisplayRange.bind(this, series), this);\n      this.listenTo(series, 'change:yKey', this.loadSeriesData.bind(this, series), this);\n\n      this.listenTo(series, 'change:interpolate', this.loadSeriesData.bind(this, series), this);\n      this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);\n\n      this.loadSeriesData(series);\n    },\n\n    removeSeries(plotSeries, index) {\n      const yAxisId = plotSeries.get('yAxisId');\n      this.updateAxisUsageCount(yAxisId, -1);\n      this.seriesModels.splice(index, 1);\n      this.stopListening(plotSeries);\n    },\n\n    updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {\n      this.updateAxisUsageCount(oldAxisId, -1);\n      this.updateAxisUsageCount(newAxisId, 1);\n    },\n\n    updateAxisUsageCount(yAxisId, updateCountBy) {\n      const foundYAxis = this.yAxes.find((yAxis) => yAxis.id === yAxisId);\n      if (foundYAxis) {\n        foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;\n      }\n    },\n    loadAnnotationsIfAllowed() {\n      if (this.annotationViewingAndEditingAllowed) {\n        this.loadAnnotations();\n      }\n    },\n    async loadAnnotations() {\n      if (!this.openmct.annotation.getAvailableTags().length) {\n        // don't bother loading annotations if there are no tags\n        return;\n      }\n\n      const rawAnnotationsForPlot = [];\n      await Promise.all(\n        this.seriesModels.map(async (seriesModel) => {\n          const seriesAnnotations = await this.openmct.annotation.getAnnotations(\n            seriesModel.model.identifier,\n            this.abortController.signal\n          );\n          rawAnnotationsForPlot.push(...seriesAnnotations);\n        })\n      );\n      if (rawAnnotationsForPlot) {\n        this.annotatedPointsBySeries = this.findAnnotationPoints(rawAnnotationsForPlot);\n      }\n      this.annotationsEverLoaded = true;\n    },\n    loadSeriesData(series) {\n      //this check ensures that duplicate requests don't happen on load\n      if (!this.timeContext) {\n        return;\n      }\n\n      if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {\n        this.scheduleLoad(series);\n\n        return;\n      }\n\n      this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;\n\n      this.startLoading();\n      const bounds = this.timeContext.getBounds();\n      const options = {\n        size: this.$parent.$refs.plotWrapper.offsetWidth,\n        domain: this.config.xAxis.get('key'),\n        start: bounds.start,\n        end: bounds.end\n      };\n\n      series.load(options).then(this.stopLoading.bind(this));\n    },\n\n    loadMoreData(range, purge) {\n      this.config.series.forEach((plotSeries) => {\n        this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;\n        this.startLoading();\n        plotSeries\n          .load({\n            size: this.offsetWidth,\n            start: range.min,\n            end: range.max,\n            domain: this.config.xAxis.get('key')\n          })\n          .then(this.stopLoading.bind(this));\n        if (purge) {\n          plotSeries.purgeRecordsOutsideRange(range);\n        }\n      });\n    },\n\n    scheduleLoad(series) {\n      if (!this.scheduledLoads) {\n        this.startLoading();\n        this.scheduledLoads = [];\n        this.checkForSize = setInterval(\n          function () {\n            if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {\n              return;\n            }\n\n            this.stopLoading();\n            this.scheduledLoads.forEach(this.loadSeriesData, this);\n            delete this.scheduledLoads;\n            clearInterval(this.checkForSize);\n            delete this.checkForSize;\n          }.bind(this)\n        );\n      }\n\n      if (this.scheduledLoads.indexOf(series) === -1) {\n        this.scheduledLoads.push(series);\n      }\n    },\n\n    startLoading() {\n      this.pending += 1;\n      this.updateLoading();\n    },\n\n    stopLoading() {\n      this.pending -= 1;\n      this.updateLoading();\n      if (this.pending === 0) {\n        this.EventBus.$emit('loading-complete');\n      }\n    },\n\n    updateLoading() {\n      this.$emit('loading-updated', this.pending > 0);\n    },\n\n    updateFiltersAndResubscribe(updatedFilters) {\n      this.config.series.forEach(function (series) {\n        series.updateFiltersAndRefresh(updatedFilters[series.keyString]);\n      });\n    },\n\n    clearSeries() {\n      this.config.series.forEach(function (series) {\n        series.reset();\n      });\n    },\n    shareCommonParent(domainObjectToFind) {\n      return false;\n    },\n    compositionPathContainsId(domainObjectToFind) {\n      if (!domainObjectToFind.composition) {\n        return false;\n      }\n\n      return domainObjectToFind.composition.some((compositionIdentifier) => {\n        return this.openmct.objects.areIdsEqual(\n          compositionIdentifier,\n          this.domainObject.identifier\n        );\n      });\n    },\n    plotCompositionContainsId(domainObjectToFind) {\n      if (!this.domainObject.composition) {\n        return false;\n      }\n      if (!domainObjectToFind.identifier) {\n        return false;\n      }\n\n      return this.domainObject.composition.some((compositionIdentifier) => {\n        return this.openmct.objects.areIdsEqual(\n          compositionIdentifier,\n          domainObjectToFind.identifier\n        );\n      });\n    },\n\n    clearData(domainObjectToClear) {\n      // If we don't have an object to clear (global), or the IDs are equal, just clear the data.\n      // If we have an object to clear, but the IDs don't match, we need to check the composition\n      // of the object we've been asked to clear to see if it contains the id we're looking for.\n      // This happens with stacked plots for example.\n      // If we find the ID, clear the plot.\n      if (\n        !domainObjectToClear ||\n        this.openmct.objects.areIdsEqual(\n          domainObjectToClear.identifier,\n          this.domainObject.identifier\n        ) ||\n        this.compositionPathContainsId(domainObjectToClear) ||\n        this.plotCompositionContainsId(domainObjectToClear)\n      ) {\n        this.clearSeries();\n      }\n    },\n\n    setDisplayRange(series, xKey) {\n      if (this.config.series.models.length !== 1) {\n        return;\n      }\n\n      const displayRange = series.getDisplayRange(xKey);\n      this.config.xAxis.set('range', displayRange);\n    },\n    updateMode() {\n      this.isRealTime = this.timeContext.isRealTime();\n    },\n\n    setUtc(timeSystem) {\n      this.isUtc = timeSystem.isUTCBased;\n    },\n\n    /**\n     * Track latest display bounds.  Forces update when not receiving ticks.\n     */\n    updateDisplayBounds(bounds, isTick) {\n      const newRange = {\n        min: bounds.start,\n        max: bounds.end\n      };\n      this.config.xAxis.set('range', newRange);\n      if (!isTick) {\n        this.annotatedPointsBySeries = {};\n        this.clearPanZoomHistory();\n        this.synchronizeIfBoundsMatch();\n        this.loadMoreData(newRange, true);\n      } else {\n        // If we're not paused, panning or zooming (time conductor and plot x-axis times are not out of sync)\n        // Drop any data that is more than 1x (max-min) before min.\n        // Limit these purges to once a second.\n        const isPanningOrZooming = this.isTimeOutOfSync;\n        const purgeRecords =\n          !this.isFrozen && !isPanningOrZooming && (!this.nextPurge || this.nextPurge < Date.now());\n        if (purgeRecords) {\n          const keepRange = {\n            min: newRange.min - (newRange.max - newRange.min),\n            max: newRange.max\n          };\n          this.config.series.forEach(function (series) {\n            series.purgeRecordsOutsideRange(keepRange);\n          });\n          this.nextPurge = Date.now() + 1000;\n        }\n      }\n    },\n\n    /**\n     * Handle end of user viewport change: load more data for current display\n     * bounds, and mark view as synchronized if necessary.\n     */\n    userViewportChangeEnd() {\n      this.synchronizeIfBoundsMatch();\n      const xDisplayRange = this.config.xAxis.get('displayRange');\n      this.loadMoreData(xDisplayRange);\n    },\n\n    /**\n     * mark view as synchronized if bounds match configured bounds.\n     */\n    synchronizeIfBoundsMatch() {\n      const xDisplayRange = this.config.xAxis.get('displayRange');\n      const xRange = this.config.xAxis.get('range');\n      this.synchronized(xRange.min === xDisplayRange.min && xRange.max === xDisplayRange.max);\n    },\n\n    /**\n     * Getter/setter for \"synchronized\" value.  If not synchronized and\n     * time conductor is in clock mode, will mark objects as unsynced so that\n     * displays can update accordingly.\n     */\n    synchronized(value) {\n      const isRealTime = this.timeContext.isRealTime();\n\n      if (typeof value !== 'undefined') {\n        this._synchronized = value;\n        this.isTimeOutOfSync = value !== true;\n\n        const isUnsynced = isRealTime && !value;\n        this.setStatus(isUnsynced);\n      }\n\n      return this._synchronized;\n    },\n\n    setStatus(isNotInSync) {\n      const outOfSync =\n        isNotInSync === true || this.isTimeOutOfSync === true || this.isFrozen === true;\n      if (outOfSync === true) {\n        this.openmct.status.set(this.domainObject.identifier, 'timeconductor-unsynced');\n      } else {\n        this.openmct.status.set(this.domainObject.identifier, '');\n      }\n    },\n\n    initCanvas() {\n      if (this.canvas) {\n        this.stopListening(this.canvas);\n      }\n\n      this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];\n\n      this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);\n      this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);\n      this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);\n      this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);\n      this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);\n    },\n\n    marqueeAnnotations(annotationsToSelect) {\n      annotationsToSelect.forEach((annotationToSelect) => {\n        annotationToSelect.targets.forEach((target) => {\n          const targetKeyString = target.keyString;\n          const series = this.seriesModels.find(\n            (seriesModel) => seriesModel.keyString === targetKeyString\n          );\n          if (!series) {\n            return;\n          }\n\n          const yAxisId = series.get('yAxisId');\n          const rectangle = {\n            start: {\n              x: target.minX,\n              y: [target.minY],\n              yAxisIds: [yAxisId]\n            },\n            end: {\n              x: target.maxX,\n              y: [target.maxY],\n              yAxisIds: [yAxisId]\n            },\n            color: [1, 1, 1, 0.1]\n          };\n          this.rectangles.push(rectangle);\n        });\n      });\n    },\n    gatherNearbyAnnotations() {\n      const nearbyAnnotations = [];\n      this.config.series.models.forEach((series) => {\n        if (series?.closest?.annotationsById) {\n          Object.values(series.closest.annotationsById).forEach((closeAnnotation) => {\n            const addedAnnotationAlready = nearbyAnnotations.some((annotation) => {\n              return (\n                _.isEqual(annotation.targets, closeAnnotation.targets) &&\n                _.isEqual(annotation.tags, closeAnnotation.tags)\n              );\n            });\n            if (!addedAnnotationAlready) {\n              nearbyAnnotations.push(closeAnnotation);\n            }\n          });\n        }\n      });\n\n      return nearbyAnnotations;\n    },\n\n    prepareExistingAnnotationSelection(annotations) {\n      const targetDomainObjects = this.config.series.models.map((series) => {\n        return series.domainObject;\n      });\n\n      const targetDetails = [];\n      const uniqueBoundsAnnotations = [];\n      annotations.forEach((annotation) => {\n        // for each target, push toRaw\n        annotation.targets.forEach((target) => {\n          targetDetails.push(toRaw(target));\n        });\n\n        const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some((existingAnnotation) => {\n          const existingBoundingBox = Object.values(existingAnnotation.targets)[0];\n          const newBoundingBox = Object.values(annotation.targets)[0];\n\n          return (\n            existingBoundingBox.minX === newBoundingBox.minX &&\n            existingBoundingBox.minY === newBoundingBox.minY &&\n            existingBoundingBox.maxX === newBoundingBox.maxX &&\n            existingBoundingBox.maxY === newBoundingBox.maxY\n          );\n        });\n        if (!boundingBoxAlreadyAdded) {\n          uniqueBoundsAnnotations.push(annotation);\n        }\n      });\n      this.marqueeAnnotations(uniqueBoundsAnnotations);\n\n      return {\n        targetDomainObjects,\n        targetDetails\n      };\n    },\n    initialize() {\n      this.handleWindowResize = _.debounce(this.handleWindowResize, 500);\n      this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);\n      this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);\n\n      // Setup canvas etc.\n      this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));\n      this.yScale = [];\n      this.yAxisListWithRange.forEach((yAxis) => {\n        this.yScale.push({\n          id: yAxis.id,\n          scale: new LinearScale(yAxis.get('displayRange'))\n        });\n      });\n\n      this.pan = undefined;\n      this.marquee = undefined;\n\n      this.chartElementBounds = undefined;\n      this.tickUpdate = false;\n\n      this.initCanvas();\n\n      this.config.yAxisLabel = this.config.yAxis.get('label');\n\n      this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);\n      this.yAxisListWithRange.forEach((yAxis) => {\n        this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);\n      });\n    },\n\n    onXAxisChange(displayBounds) {\n      if (displayBounds) {\n        this.xScale.domain(displayBounds);\n      }\n    },\n\n    onYAxisChange(yAxisId, displayBounds) {\n      if (displayBounds) {\n        this.yScale\n          .filter((yAxis) => yAxis.id === yAxisId)\n          .forEach((yAxis) => {\n            yAxis.scale.domain(displayBounds);\n          });\n      }\n    },\n\n    toggleSeriesForYAxis({ id, visible }) {\n      //if toggling to visible, re-fetch the data for the series that are part of this y Axis\n      if (visible === true) {\n        this.config.series.models\n          .filter((model) => model.get('yAxisId') === id)\n          .forEach(this.loadSeriesData, this);\n      }\n\n      this.yAxisIdVisibility[id] = visible;\n      this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility)\n        .map(Number)\n        .filter((key) => {\n          return this.yAxisIdVisibility[key] === false;\n        });\n    },\n\n    trackMousePosition(event) {\n      this.trackChartElementBounds(event);\n      this.xScale.range({\n        min: 0,\n        max: this.chartElementBounds.width\n      });\n      this.yScale.forEach((yAxis) => {\n        yAxis.scale.range({\n          min: 0,\n          max: this.chartElementBounds.height\n        });\n      });\n\n      this.positionOverElement = {\n        x: event.clientX - this.chartElementBounds.left,\n        y: this.chartElementBounds.height - (event.clientY - this.chartElementBounds.top)\n      };\n\n      const yLocationForPositionOverPlot = this.yScale.map((yAxis) =>\n        yAxis.scale.invert(this.positionOverElement.y)\n      );\n      const yAxisIds = this.yScale.map((yAxis) => yAxis.id);\n      // Also store the order of yAxisIds so that we can associate the y location to the yAxis\n      this.positionOverPlot = {\n        x: this.xScale.invert(this.positionOverElement.x),\n        y: yLocationForPositionOverPlot,\n        yAxisIds\n      };\n\n      if (this.cursorGuide) {\n        this.updateCrosshairs(event);\n      }\n\n      this.highlightValues(this.positionOverPlot.x);\n      this.updateMarquee();\n      this.updatePan();\n      event.preventDefault();\n    },\n\n    getYPositionForYAxis(object, yAxis) {\n      const index = object.yAxisIds.findIndex((yAxisId) => yAxisId === yAxis.get('id'));\n\n      return object.y[index];\n    },\n\n    updateCrosshairs(event) {\n      this.$refs.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';\n      this.$refs.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';\n    },\n\n    trackChartElementBounds(event) {\n      if (event.target === this.canvas) {\n        this.chartElementBounds = event.target.getBoundingClientRect();\n      }\n    },\n\n    onPlotHighlightSet($e, point) {\n      if (point === this.highlightPoint) {\n        return;\n      }\n\n      this.highlightValues(point);\n    },\n\n    highlightValues(point) {\n      this.highlightPoint = point;\n      if (this.lockHighlightPoint) {\n        return;\n      }\n\n      if (!point) {\n        this.highlights = [];\n        this.config.series.models.forEach((series) => delete series.closest);\n      } else {\n        this.highlights = this.config.series.models\n          .filter((series) => series.getSeriesData().length > 0)\n          .map((series) => {\n            series.closest = series.nearestPoint(point);\n\n            return {\n              seriesKeyString: series.keyString,\n              point: series.closest\n            };\n          });\n      }\n\n      this.$emit('highlights', this.highlights);\n    },\n\n    untrackMousePosition() {\n      this.positionOverElement = undefined;\n      this.positionOverPlot = undefined;\n      this.highlightValues();\n    },\n\n    onMouseDown(event) {\n      // do not monitor drag events on browser context click\n      if (event.ctrlKey) {\n        return;\n      }\n\n      this.listenTo(window, 'mouseup', this.onMouseUp, this);\n      this.listenTo(window, 'mousemove', this.trackMousePosition, this);\n\n      if (!this.options.compact) {\n        // track frozen state on mouseDown to be read on mouseUp\n        const isFrozen =\n          this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;\n        this.isFrozenOnMouseDown = isFrozen;\n\n        if (event.altKey && !event.shiftKey) {\n          return this.startPan(event);\n        } else if (event.altKey && event.shiftKey) {\n          this.freeze();\n\n          return this.startMarquee(event, true);\n        } else {\n          return this.startMarquee(event, false);\n        }\n      }\n    },\n\n    onMouseUp(event) {\n      this.stopListening(window, 'mouseup', this.onMouseUp, this);\n      this.stopListening(window, 'mousemove', this.trackMousePosition, this);\n\n      if (this.isMouseClick() && event.shiftKey) {\n        this.lockHighlightPoint = !this.lockHighlightPoint;\n        this.$emit('lock-highlight-point', this.lockHighlightPoint);\n      }\n\n      if (this.pan) {\n        return this.endPan(event);\n      }\n\n      if (this.marquee) {\n        this.endMarquee(event);\n      }\n\n      // resume the plot if no pan, zoom, or drag action is taken\n      // needs to follow endMarquee so that plotHistory is pruned\n      const isAction = Boolean(this.plotHistory.length);\n      if (!isAction && !this.isFrozenOnMouseDown) {\n        this.clearPanZoomHistory();\n        this.synchronizeIfBoundsMatch();\n      }\n    },\n\n    isMouseClick() {\n      // We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.\n      if (!this.marquee && !this.positionOverPlot) {\n        return false;\n      }\n\n      const { start, end } = this.marquee ?? {\n        start: this.positionOverPlot,\n        end: this.positionOverPlot\n      };\n      const someYPositionOverPlot = start.y.some((y) => y);\n\n      return start.x === end.x && someYPositionOverPlot;\n    },\n\n    updateMarquee() {\n      if (!this.marquee) {\n        return;\n      }\n\n      this.marquee.end = this.positionOverPlot;\n      this.marquee.endPixels = this.positionOverElement;\n    },\n\n    startMarquee(event, annotationEvent) {\n      this.rectangles = [];\n      this.annotationSelectionsBySeries = {};\n      this.canvas.classList.remove('plot-drag');\n      this.canvas.classList.add('plot-marquee');\n\n      this.trackMousePosition(event);\n      if (this.positionOverPlot) {\n        this.freeze();\n        this.marquee = {\n          startPixels: this.positionOverElement,\n          endPixels: this.positionOverElement,\n          start: this.positionOverPlot,\n          end: this.positionOverPlot,\n          color: [1, 1, 1, 0.25]\n        };\n        if (annotationEvent) {\n          this.marquee.annotationEvent = true;\n        }\n\n        this.rectangles.push(this.marquee);\n        this.trackHistory();\n      }\n    },\n    selectNearbyAnnotations(event) {\n      // need to stop propagation right away to prevent selecting the plot itself\n      event.stopPropagation();\n\n      const nearbyAnnotations = this.gatherNearbyAnnotations();\n\n      if (\n        this.annotationViewingAndEditingAllowed &&\n        Object.keys(this.annotationSelectionsBySeries).length\n      ) {\n        //no annotations were found, but we are adding some now\n        return;\n      }\n\n      if (this.annotationViewingAndEditingAllowed && nearbyAnnotations.length) {\n        //show annotations if some were found\n        const { targetDomainObjects, targetDetails } =\n          this.prepareExistingAnnotationSelection(nearbyAnnotations);\n        this.selectPlotAnnotations({\n          targetDetails,\n          targetDomainObjects,\n          annotations: nearbyAnnotations\n        });\n\n        return;\n      }\n\n      //Fall through to here if either there is no new selection add tags or no existing annotations were retrieved\n      this.selectPlot();\n    },\n    selectPlot() {\n      // should show plot itself if we didn't find any annotations\n      const selection = this.createPathSelection();\n      this.openmct.selection.select(selection, true);\n    },\n    createPathSelection() {\n      let selection = [];\n      selection.unshift({\n        element: this.$el,\n        context: {\n          item: this.domainObject\n        }\n      });\n      this.objectPath.forEach((pathObject, index) => {\n        selection.push({\n          element: this.openmct.layout.$refs.browseObject.$el,\n          context: {\n            item: pathObject\n          }\n        });\n      });\n\n      return selection;\n    },\n    selectPlotAnnotations({ targetDetails, targetDomainObjects, annotations }) {\n      const annotationContext = {\n        type: 'clicked-on-plot-selection',\n        targetDetails,\n        targetDomainObjects,\n        annotations,\n        annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,\n        onAnnotationChange: this.onAnnotationChange\n      };\n      const selection = this.createPathSelection();\n      if (\n        selection.length &&\n        this.openmct.objects.areIdsEqual(\n          selection[0].context.item.identifier,\n          this.domainObject.identifier\n        )\n      ) {\n        selection[0].context = {\n          ...selection[0].context,\n          ...annotationContext\n        };\n      } else {\n        selection.unshift({\n          element: this.$el,\n          context: {\n            item: this.domainObject,\n            ...annotationContext\n          }\n        });\n      }\n\n      this.openmct.selection.select(selection, true);\n\n      document.body.addEventListener('click', this.cancelSelection);\n    },\n    selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBoxBySeries, event) {\n      let targetDomainObjects = [];\n      let targetDetails = [];\n      let annotations = [];\n      Object.keys(pointsInBoxBySeries).forEach((seriesKey) => {\n        const seriesModel = this.getSeries(seriesKey);\n        const boundingBoxWithId = boundingBoxPerYAxis.find(\n          (box) => box.id === seriesModel.get('yAxisId')\n        );\n        targetDetails.push({ ...boundingBoxWithId?.boundingBox, keyString: seriesKey });\n\n        targetDomainObjects.push(seriesModel.domainObject);\n      });\n      this.selectPlotAnnotations({\n        targetDetails,\n        targetDomainObjects,\n        annotations\n      });\n    },\n    findAnnotationPoints(rawAnnotations) {\n      const annotationsBySeries = {};\n      rawAnnotations.forEach((rawAnnotation) => {\n        if (rawAnnotation.targets) {\n          const targetValues = rawAnnotation.targets;\n          const targetKeys = rawAnnotation.targets.map((target) => target.keyString);\n          if (targetValues && targetValues.length) {\n            let boundingBoxPerYAxis = [];\n            targetValues.forEach((boundingBox, index) => {\n              const seriesId = targetKeys[index];\n              const series = this.seriesModels.find(\n                (seriesModel) => seriesModel.keyString === seriesId\n              );\n              if (!series) {\n                return;\n              }\n              if (!annotationsBySeries[seriesId]) {\n                annotationsBySeries[seriesId] = [];\n              }\n\n              boundingBoxPerYAxis.push({\n                id: series.get('yAxisId'),\n                boundingBox\n              });\n            });\n\n            const pointsInBoxBySeries = this.getPointsInBoxBySeries(\n              boundingBoxPerYAxis,\n              rawAnnotation\n            );\n            if (pointsInBoxBySeries && Object.values(pointsInBoxBySeries).length) {\n              Object.keys(pointsInBoxBySeries).forEach((seriesKeyString) => {\n                const pointsInBox = pointsInBoxBySeries[seriesKeyString];\n                if (pointsInBox && pointsInBox.length) {\n                  if (!annotationsBySeries[seriesKeyString]) {\n                    annotationsBySeries[seriesKeyString] = [];\n                  }\n                  annotationsBySeries[seriesKeyString].push(...pointsInBox);\n                }\n              });\n            }\n          }\n        }\n      });\n\n      return annotationsBySeries;\n    },\n    searchWithFlatbush(seriesData, seriesModel, boundingBox) {\n      const flatbush = new Flatbush(seriesData.length);\n      seriesData.forEach((point) => {\n        const x = seriesModel.getXVal(point);\n        const y = seriesModel.getYVal(point);\n        flatbush.add(x, y, x, y);\n      });\n      flatbush.finish();\n\n      const rangeResults = flatbush.search(\n        boundingBox.minX,\n        boundingBox.minY,\n        boundingBox.maxX,\n        boundingBox.maxY\n      );\n\n      return rangeResults;\n    },\n    getSeries(keyStringToFind) {\n      const foundSeries = this.seriesModels.find((series) => {\n        return series.keyString === keyStringToFind;\n      });\n      return foundSeries;\n    },\n    getPointsInBoxBySeries(boundingBoxPerYAxis, rawAnnotation) {\n      // load series models in KD-Trees\n      const searchResultsBySeries = {};\n      this.seriesModels.forEach((seriesModel) => {\n        const boundingBoxWithId = boundingBoxPerYAxis.find(\n          (box) => box.id === seriesModel.get('yAxisId')\n        );\n        const boundingBox = boundingBoxWithId?.boundingBox;\n        //Series was probably added after the last annotations were saved\n        if (!boundingBox) {\n          return;\n        }\n\n        const seriesData = seriesModel.getSeriesData();\n        if (seriesData && seriesData.length) {\n          searchResultsBySeries[seriesModel.keyString] = [];\n          const rangeResults = this.searchWithFlatbush(seriesData, seriesModel, boundingBox);\n          rangeResults.forEach((id) => {\n            const seriesDatum = seriesData[id];\n            if (seriesDatum) {\n              const result = {\n                point: seriesDatum\n              };\n              searchResultsBySeries[seriesModel.keyString].push(result);\n            }\n\n            if (rawAnnotation) {\n              if (!seriesDatum.annotationsById) {\n                seriesDatum.annotationsById = {};\n              }\n\n              const annotationKeyString = this.openmct.objects.makeKeyString(\n                rawAnnotation.identifier\n              );\n              seriesDatum.annotationsById[annotationKeyString] = rawAnnotation;\n            }\n          });\n        }\n      });\n\n      return searchResultsBySeries;\n    },\n    endAnnotationMarquee(event) {\n      const boundingBoxPerYAxis = [];\n      this.yAxisListWithRange.forEach((yAxis, yIndex) => {\n        const minX = Math.min(this.marquee.start.x, this.marquee.end.x);\n        const minY = Math.min(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);\n        const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);\n        const maxY = Math.max(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);\n        const boundingBox = {\n          minX,\n          minY,\n          maxX,\n          maxY\n        };\n        boundingBoxPerYAxis.push({\n          id: yAxis.get('id'),\n          boundingBox\n        });\n      });\n\n      const pointsInBoxBySeries = this.getPointsInBoxBySeries(boundingBoxPerYAxis);\n      if (!pointsInBoxBySeries || Object.values(pointsInBoxBySeries).length === 0) {\n        return;\n      }\n\n      this.annotationSelectionsBySeries = pointsInBoxBySeries;\n      this.selectNewPlotAnnotations(boundingBoxPerYAxis, this.annotationSelectionsBySeries, event);\n    },\n    endZoomMarquee() {\n      const startPixels = this.marquee.startPixels;\n      const endPixels = this.marquee.endPixels;\n      const marqueeDistance = Math.sqrt(\n        Math.pow(startPixels.x - endPixels.x, 2) + Math.pow(startPixels.y - endPixels.y, 2)\n      );\n      // Don't zoom if mouse moved less than 7.5 pixels.\n      if (marqueeDistance > 7.5) {\n        this.config.xAxis.set('displayRange', {\n          min: Math.min(this.marquee.start.x, this.marquee.end.x),\n          max: Math.max(this.marquee.start.x, this.marquee.end.x)\n        });\n        this.yAxisListWithRange.forEach((yAxis) => {\n          const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis);\n          const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis);\n          yAxis.set('displayRange', {\n            min: Math.min(yStartPosition, yEndPosition),\n            max: Math.max(yStartPosition, yEndPosition)\n          });\n        });\n        this.userViewportChangeEnd();\n      } else {\n        // A history entry is created by startMarquee, need to remove\n        // if marquee zoom doesn't occur.\n        this.plotHistory.pop();\n      }\n    },\n    endMarquee(event) {\n      if (this.marquee.annotationEvent) {\n        this.endAnnotationMarquee(event);\n      } else {\n        this.endZoomMarquee();\n        this.rectangles = [];\n      }\n\n      this.marquee = null;\n    },\n\n    onAnnotationChange(annotations) {\n      if (this.marquee) {\n        this.marquee.annotationEvent = false;\n        this.endMarquee();\n      }\n\n      this.loadAnnotations().catch((err) => {\n        if (err.name !== 'AbortError') {\n          throw err;\n        }\n      });\n    },\n\n    zoom(zoomDirection, zoomFactor) {\n      const currentXaxis = this.config.xAxis.get('displayRange');\n\n      let doesYAxisHaveRange = false;\n      this.yAxisListWithRange.forEach((yAxisModel) => {\n        if (yAxisModel.get('displayRange')) {\n          doesYAxisHaveRange = true;\n        }\n      });\n\n      // when there is no plot data, the ranges can be undefined\n      // in which case we should not perform zoom\n      if (!currentXaxis || !doesYAxisHaveRange) {\n        return;\n      }\n\n      this.freeze();\n      this.trackHistory();\n\n      const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;\n\n      if (zoomDirection === 'in') {\n        this.config.xAxis.set('displayRange', {\n          min: currentXaxis.min + xAxisDist,\n          max: currentXaxis.max - xAxisDist\n        });\n\n        this.yAxisListWithRange.forEach((yAxisModel) => {\n          const currentYaxis = yAxisModel.get('displayRange');\n          if (!currentYaxis) {\n            return;\n          }\n\n          const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;\n          yAxisModel.set('displayRange', {\n            min: currentYaxis.min + yAxisDist,\n            max: currentYaxis.max - yAxisDist\n          });\n        });\n      } else if (zoomDirection === 'out') {\n        this.config.xAxis.set('displayRange', {\n          min: currentXaxis.min - xAxisDist,\n          max: currentXaxis.max + xAxisDist\n        });\n\n        this.yAxisListWithRange.forEach((yAxisModel) => {\n          const currentYaxis = yAxisModel.get('displayRange');\n          if (!currentYaxis) {\n            return;\n          }\n\n          const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;\n          yAxisModel.set('displayRange', {\n            min: currentYaxis.min - yAxisDist,\n            max: currentYaxis.max + yAxisDist\n          });\n        });\n      }\n\n      this.userViewportChangeEnd();\n    },\n\n    wheelZoom(event) {\n      const ZOOM_AMT = 0.1;\n      event.preventDefault();\n\n      if (event.wheelDelta === undefined || !this.positionOverPlot) {\n        return;\n      }\n\n      let xDisplayRange = this.config.xAxis.get('displayRange');\n\n      let doesYAxisHaveRange = false;\n      this.yAxisListWithRange.forEach((yAxisModel) => {\n        if (yAxisModel.get('displayRange')) {\n          doesYAxisHaveRange = true;\n        }\n      });\n\n      // when there is no plot data, the ranges can be undefined\n      // in which case we should not perform zoom\n      if (!xDisplayRange || !doesYAxisHaveRange) {\n        return;\n      }\n\n      this.freeze();\n      window.clearTimeout(this.stillZooming);\n\n      let xAxisDist = xDisplayRange.max - xDisplayRange.min;\n      let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;\n      let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;\n      let xAxisMaxDist = xDistMouseToMax / xAxisDist;\n      let xAxisMinDist = xDistMouseToMin / xAxisDist;\n\n      let plotHistoryStep;\n\n      if (!plotHistoryStep) {\n        const yRangeList = [];\n        this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange')));\n        plotHistoryStep = {\n          x: this.config.xAxis.get('displayRange'),\n          y: yRangeList\n        };\n      }\n\n      if (event.wheelDelta < 0) {\n        this.config.xAxis.set('displayRange', {\n          min: xDisplayRange.min + xAxisDist * ZOOM_AMT * xAxisMinDist,\n          max: xDisplayRange.max - xAxisDist * ZOOM_AMT * xAxisMaxDist\n        });\n\n        this.yAxisListWithRange.forEach((yAxisModel) => {\n          const yDisplayRange = yAxisModel.get('displayRange');\n          if (!yDisplayRange) {\n            return;\n          }\n\n          const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);\n          let yAxisDist = yDisplayRange.max - yDisplayRange.min;\n          let yDistMouseToMax = yDisplayRange.max - yPosition;\n          let yDistMouseToMin = yPosition - yDisplayRange.min;\n          let yAxisMaxDist = yDistMouseToMax / yAxisDist;\n          let yAxisMinDist = yDistMouseToMin / yAxisDist;\n\n          yAxisModel.set('displayRange', {\n            min: yDisplayRange.min + yAxisDist * ZOOM_AMT * yAxisMinDist,\n            max: yDisplayRange.max - yAxisDist * ZOOM_AMT * yAxisMaxDist\n          });\n        });\n      } else if (event.wheelDelta >= 0) {\n        this.config.xAxis.set('displayRange', {\n          min: xDisplayRange.min - xAxisDist * ZOOM_AMT * xAxisMinDist,\n          max: xDisplayRange.max + xAxisDist * ZOOM_AMT * xAxisMaxDist\n        });\n\n        this.yAxisListWithRange.forEach((yAxisModel) => {\n          const yDisplayRange = yAxisModel.get('displayRange');\n          if (!yDisplayRange) {\n            return;\n          }\n\n          const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);\n          let yAxisDist = yDisplayRange.max - yDisplayRange.min;\n          let yDistMouseToMax = yDisplayRange.max - yPosition;\n          let yDistMouseToMin = yPosition - yDisplayRange.min;\n          let yAxisMaxDist = yDistMouseToMax / yAxisDist;\n          let yAxisMinDist = yDistMouseToMin / yAxisDist;\n\n          yAxisModel.set('displayRange', {\n            min: yDisplayRange.min - yAxisDist * ZOOM_AMT * yAxisMinDist,\n            max: yDisplayRange.max + yAxisDist * ZOOM_AMT * yAxisMaxDist\n          });\n        });\n      }\n\n      this.stillZooming = window.setTimeout(\n        function () {\n          this.plotHistory.push(plotHistoryStep);\n          plotHistoryStep = undefined;\n          this.userViewportChangeEnd();\n        }.bind(this),\n        250\n      );\n    },\n\n    startPan(event) {\n      this.canvas.classList.add('plot-drag');\n      this.canvas.classList.remove('plot-marquee');\n\n      this.trackMousePosition(event);\n      this.freeze();\n      this.pan = {\n        start: this.positionOverPlot\n      };\n      event.preventDefault();\n      this.trackHistory();\n\n      return false;\n    },\n\n    updatePan() {\n      // calculate offset between points.  Apply that offset to viewport.\n      if (!this.pan) {\n        return;\n      }\n\n      const dX = this.pan.start.x - this.positionOverPlot.x;\n      const xRange = this.config.xAxis.get('displayRange');\n\n      this.config.xAxis.set('displayRange', {\n        min: xRange.min + dX,\n        max: xRange.max + dX\n      });\n\n      const dY = [];\n      this.positionOverPlot.y.forEach((yAxisPosition, index) => {\n        const yAxisId = this.positionOverPlot.yAxisIds[index];\n        dY.push({\n          yAxisId: yAxisId,\n          y: this.pan.start.y[index] - yAxisPosition\n        });\n      });\n\n      this.yAxisListWithRange.forEach((yAxis) => {\n        const yRange = yAxis.get('displayRange');\n        if (!yRange) {\n          return;\n        }\n\n        const yIndex = dY.findIndex((y) => y.yAxisId === yAxis.get('id'));\n\n        yAxis.set('displayRange', {\n          min: yRange.min + dY[yIndex].y,\n          max: yRange.max + dY[yIndex].y\n        });\n      });\n    },\n\n    trackHistory() {\n      const yRangeList = [];\n      const yAxisIds = [];\n      this.yAxisListWithRange.forEach((yAxis) => {\n        yRangeList.push(yAxis.get('displayRange'));\n        yAxisIds.push(yAxis.get('id'));\n      });\n      this.plotHistory.push({\n        x: this.config.xAxis.get('displayRange'),\n        y: yRangeList,\n        yAxisIds\n      });\n    },\n\n    endPan() {\n      this.pan = undefined;\n      this.userViewportChangeEnd();\n    },\n\n    freeze() {\n      this.yAxisListWithRange.forEach((yAxis) => {\n        yAxis.set('frozen', true);\n      });\n      this.config.xAxis.set('frozen', true);\n      this.setStatus();\n      if (!this.annotationsEverLoaded) {\n        this.loadAnnotations();\n      }\n    },\n\n    resumeRealtimeData() {\n      // remove annotation selections\n      this.rectangles = [];\n\n      this.clearPanZoomHistory();\n      this.userViewportChangeEnd();\n    },\n\n    clearPanZoomHistory() {\n      this.yAxisListWithRange.forEach((yAxis) => {\n        yAxis.set('frozen', false);\n      });\n      this.config.xAxis.set('frozen', false);\n      this.setStatus();\n      this.plotHistory = [];\n    },\n\n    back() {\n      const previousAxisRanges = this.plotHistory.pop();\n      if (this.plotHistory.length === 0) {\n        this.resumeRealtimeData();\n\n        return;\n      }\n\n      this.config.xAxis.set('displayRange', previousAxisRanges.x);\n      this.yAxisListWithRange.forEach((yAxis) => {\n        const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis);\n        yAxis.set('displayRange', yPosition);\n      });\n\n      this.userViewportChangeEnd();\n    },\n\n    setYAxisKey(yKey, yAxisId) {\n      const seriesForYAxis = this.config.series.models.filter(\n        (model) => model.get('yAxisId') === yAxisId\n      );\n      seriesForYAxis.forEach((model) => model.set('yKey', yKey));\n    },\n\n    pause() {\n      this.freeze();\n    },\n\n    showSynchronizeDialog() {\n      const isFixedTimespanMode = this.timeContext.isFixed();\n      if (!isFixedTimespanMode) {\n        const message = `\n                This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.\n                Do you want to continue?\n            `;\n\n        let dialog = this.openmct.overlays.dialog({\n          title: 'Synchronize Time Conductor',\n          iconClass: 'alert',\n          size: 'fit',\n          message: message,\n          buttons: [\n            {\n              label: 'Ok',\n              callback: () => {\n                dialog.dismiss();\n                this.synchronizeTimeConductor();\n              }\n            },\n            {\n              label: 'Cancel',\n              callback: () => {\n                dialog.dismiss();\n              }\n            }\n          ]\n        });\n      } else {\n        this.openmct.notifications.alert('Time conductor bounds have changed.');\n        this.synchronizeTimeConductor();\n      }\n    },\n\n    synchronizeTimeConductor() {\n      const range = this.config.xAxis.get('displayRange');\n      this.timeContext.setMode(MODES.fixed, {\n        start: range.min,\n        end: range.max\n      });\n      this.isTimeOutOfSync = false;\n    },\n\n    destroy() {\n      if (this.config) {\n        configStore.deleteStore(this.config.id);\n      }\n\n      this.config = {};\n      this.canvas = undefined;\n      this.abortController = undefined;\n\n      this.stopListening();\n\n      if (this.checkForSize) {\n        clearInterval(this.checkForSize);\n        delete this.checkForSize;\n      }\n\n      if (this.filterObserver) {\n        this.filterObserver();\n      }\n\n      if (this.removeStatusListener) {\n        this.removeStatusListener();\n      }\n\n      if (this.plotContainerResizeObserver) {\n        this.plotContainerResizeObserver.disconnect();\n      }\n\n      this.stopFollowingTimeContext();\n      this.openmct.objectViews.off('clearData', this.clearData);\n    },\n    updateStatus(status) {\n      this.$emit('status-updated', status);\n    },\n    handleWindowResize() {\n      const { plotWrapper } = this.$parent.$refs;\n      if (!plotWrapper) {\n        return;\n      }\n\n      const newOffsetWidth = plotWrapper.offsetWidth;\n      //we ignore when width gets smaller\n      const offsetChange = newOffsetWidth - this.offsetWidth;\n      if (offsetChange > OFFSET_THRESHOLD) {\n        this.offsetWidth = newOffsetWidth;\n        this.config.series.models.forEach(this.loadSeriesData, this);\n      }\n    },\n    toggleCursorGuide() {\n      this.cursorGuide = !this.cursorGuide;\n      this.$emit('cursor-guide', this.cursorGuide);\n    },\n    toggleGridLines() {\n      this.gridLines = !this.gridLines;\n      this.$emit('grid-lines', this.gridLines);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/MctTicks.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"tickContainer\" class=\"u-contents js-ticks\">\n    <div v-if=\"position === 'left'\" class=\"gl-plot-tick-wrapper\">\n      <div\n        v-for=\"(tick, i) in ticks\"\n        :key=\"'tick-left' + i\"\n        class=\"gl-plot-tick gl-plot-x-tick-label\"\n        :style=\"{\n          left: (100 * (tick.value - min)) / interval + '%'\n        }\"\n        :aria-label=\"tick.fullText || tick.text\"\n        :title=\"tick.fullText || tick.text\"\n      >\n        {{ tick.text }}\n      </div>\n    </div>\n    <div v-if=\"position === 'top'\" class=\"gl-plot-tick-wrapper\">\n      <div\n        v-for=\"(tick, i) in ticks\"\n        :key=\"'tick-top' + i\"\n        class=\"gl-plot-tick gl-plot-y-tick-label\"\n        :style=\"{ top: (100 * (max - tick.value)) / interval + '%' }\"\n        :aria-label=\"tick.fullText || tick.text\"\n        :title=\"tick.fullText || tick.text\"\n        style=\"margin-top: -0.5em; direction: ltr\"\n      >\n        <span>{{ tick.text }}</span>\n      </div>\n    </div>\n    <!-- grid lines follow -->\n    <template v-if=\"position === 'right'\">\n      <div\n        v-for=\"(tick, i) in ticks\"\n        :key=\"'tick-right' + i\"\n        class=\"gl-plot-hash hash-v\"\n        :style=\"{\n          right: (100 * (max - tick.value)) / interval + '%',\n          height: '100%'\n        }\"\n      ></div>\n    </template>\n    <template v-if=\"position === 'bottom'\">\n      <div\n        v-for=\"(tick, i) in ticks\"\n        :key=\"'tick-bottom' + i\"\n        class=\"gl-plot-hash hash-h\"\n        :style=\"{ bottom: (100 * (tick.value - min)) / interval + '%', width: '100%' }\"\n      ></div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport { inject } from 'vue';\n\nimport { useAlignment } from '../../ui/composables/alignmentContext.js';\nimport configStore from './configuration/ConfigStore.js';\nimport eventHelpers from './lib/eventHelpers.js';\nimport {\n  getFormattedTicks,\n  getLogTicks,\n  getTimeTicks,\n  measureTextWidth,\n  ticks\n} from './tickUtils.js';\n\nconst SECONDARY_TICK_NUMBER = 2;\n\nexport default {\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  props: {\n    axisType: {\n      type: String,\n      default() {\n        return '';\n      },\n      required: true\n    },\n    // Make it a prop, then later we can allow user to change it via UI input\n    tickCount: {\n      type: Number,\n      default() {\n        return 6;\n      }\n    },\n    axisId: {\n      type: Number,\n      default() {\n        return null;\n      }\n    },\n    isUtc: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    position: {\n      required: true,\n      type: String,\n      default() {\n        return '';\n      }\n    }\n  },\n  emits: ['plot-tick-width'],\n  setup() {\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { update: updateAlignment, remove: removeAlignment } = useAlignment(\n      domainObject,\n      objectPath,\n      openmct\n    );\n\n    return { updateAlignment, removeAlignment };\n  },\n  data() {\n    return {\n      ticks: [],\n      interval: undefined,\n      min: undefined\n    };\n  },\n  mounted() {\n    eventHelpers.extend(this);\n\n    if (!this.axisType) {\n      throw new Error('axis-type prop expected');\n    }\n\n    this.axis = this.getAxisFromConfig();\n\n    this.tickUpdate = false;\n    this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);\n    this.listenTo(this.axis, 'change:format', this.updateTicks, this);\n    this.listenTo(this.axis, 'change:key', this.updateTicksForceRegeneration, this);\n    this.updateTicks();\n\n    this.resizeObserver = new ResizeObserver(() => {\n      this.updateTicks(true);\n    });\n\n    if (this.$refs.tickContainer) {\n      this.resizeObserver.observe(this.$refs.tickContainer.parentElement);\n    }\n  },\n  beforeUnmount() {\n    this.removeAlignment({\n      yAxisId: this.axisId,\n      updateObjectPath: this.objectPath\n    });\n    this.stopListening();\n\n    if (this.resizeObserver) {\n      this.resizeObserver.disconnect();\n    }\n  },\n  methods: {\n    getAxisFromConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      /** @type {import('./configuration/PlotConfigurationModel').default} */\n      let config = configStore.get(configId);\n\n      if (!config) {\n        throw new Error('config is missing');\n      }\n\n      if (this.axisType === 'yAxis') {\n        if (this.axisId && this.axisId !== config.yAxis.id) {\n          return config.additionalYAxes.find((axis) => axis.id === this.axisId);\n        } else {\n          return config.yAxis;\n        }\n      } else {\n        return config[this.axisType];\n      }\n    },\n    /**\n     * Determine whether ticks should be regenerated for a given range.\n     * Ticks are updated\n     * a) if they don't exist,\n     * b) if existing ticks are outside of given range,\n     * c) if range exceeds size of tick range by more than one tick step,\n     * d) if forced to regenerate (ex. changing x-axis metadata).\n     *\n     */\n    shouldRegenerateTicks(range, forceRegeneration) {\n      if (forceRegeneration) {\n        return true;\n      }\n\n      if (!this.tickRange || !this.ticks || !this.ticks.length) {\n        return true;\n      }\n\n      if (this.tickRange.max > range.max || this.tickRange.min < range.min) {\n        return true;\n      }\n\n      if (Math.abs(range.max - this.tickRange.max) > this.tickRange.step) {\n        return true;\n      }\n\n      if (Math.abs(this.tickRange.min - range.min) > this.tickRange.step) {\n        return true;\n      }\n\n      return false;\n    },\n\n    getTicks() {\n      const number = this.tickCount;\n      const clampRange = this.axis.get('values');\n      const range = this.axis.get('displayRange');\n      if (clampRange) {\n        return clampRange.filter(function (value) {\n          return value <= range.max && value >= range.min;\n        }, this);\n      }\n\n      let tickCount = number;\n      // Use dynamic ticks for xAxis to avoid overcrowding and improve readability.\n      if (this.axisType === 'xAxis') {\n        tickCount = this.getDynamicTickCount(range);\n      }\n\n      if (this.axisType === 'yAxis' && this.axis.get('logMode')) {\n        return getLogTicks(range.min, range.max, number, SECONDARY_TICK_NUMBER);\n      } else if (this.isUtc) {\n        return getTimeTicks(range.min, range.max, tickCount);\n      } else {\n        return ticks(range.min, range.max, tickCount);\n      }\n    },\n\n    updateTicksForceRegeneration() {\n      this.updateTicks(true);\n    },\n\n    updateTicks(forceRegeneration = false) {\n      const range = this.axis.get('displayRange');\n\n      if (!range) {\n        delete this.min;\n        delete this.max;\n        delete this.interval;\n        delete this.tickRange;\n        this.ticks = [];\n        delete this.shouldCheckWidth;\n\n        return;\n      }\n\n      const format = this.axis.get('format');\n      if (!format) {\n        return;\n      }\n\n      this.min = range.min;\n      this.max = range.max;\n      this.interval = Math.abs(range.min - range.max);\n      if (this.shouldRegenerateTicks(range, forceRegeneration)) {\n        let newTicks = this.getTicks();\n        this.tickRange = {\n          min: Math.min(...newTicks),\n          max: Math.max(...newTicks),\n          step: newTicks[1] - newTicks[0]\n        };\n\n        newTicks = getFormattedTicks(newTicks, format);\n\n        this.ticks = newTicks;\n        this.shouldCheckWidth = true;\n      }\n\n      this.scheduleTickUpdate();\n    },\n\n    scheduleTickUpdate() {\n      if (this.tickUpdate) {\n        return;\n      }\n\n      this.tickUpdate = true;\n      setTimeout(this.doTickUpdate.bind(this), 0);\n    },\n\n    doTickUpdate() {\n      if (this.shouldCheckWidth) {\n        const tickElements =\n          this.$refs.tickContainer &&\n          this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');\n        if (tickElements) {\n          const tickWidth = Number(\n            [].reduce.call(\n              tickElements,\n              function (memo, first) {\n                return Math.max(memo, first.offsetWidth);\n              },\n              0\n            )\n          );\n\n          if (this.tickWidth !== tickWidth) {\n            this.tickWidth = tickWidth;\n            this.$emit('plot-tick-width', {\n              width: tickWidth,\n              yAxisId: this.axisType === 'yAxis' ? this.axisId : ''\n            });\n            if (this.axisType === 'yAxis') {\n              this.updateAlignment({\n                width: tickWidth,\n                yAxisId: this.axisId,\n                updateObjectPath: this.objectPath\n              });\n            }\n\n            this.shouldCheckWidth = false;\n          }\n        }\n      }\n\n      this.tickUpdate = false;\n    },\n\n    getDynamicTickCount(range) {\n      const container = this.$refs.tickContainer.parentElement;\n      if (!container) {\n        return this.tickCount;\n      }\n\n      const availableWidth = container.offsetWidth;\n\n      // Get current range\n      const { min, max } = range;\n      const format = this.axis.get('format');\n      const isNumeric = typeof min === 'number' && typeof max === 'number';\n\n      // Average start, mid and end labels to get a good estimate of label length.\n      // This handles non UTC systems too now\n      let midValue;\n      if (isNumeric) {\n        midValue = min + (max - min) / 2;\n      } else {\n        midValue = max;\n      }\n      const formattedLabels = getFormattedTicks([min, midValue, max], format).map(\n        (tick) => tick.text\n      );\n\n      // Use the one with more characters\n      const maxLabelLength = formattedLabels.reduce(\n        (maxLen, str) => Math.max(maxLen, str?.length || 0),\n        0\n      );\n\n      // Use a character width to get estimated length of a tick\n      const font = window.getComputedStyle(container).font || '12px \"Open Sans\", sans-serif';\n      const charWidth = measureTextWidth('M', font);\n\n      const estimatedLabelWidth = charWidth * maxLabelLength;\n\n      const padding = 20;\n      const tickCount = Math.floor(availableWidth / (estimatedLabelWidth + padding));\n\n      // Return at least 1 ticks, and at most 12 ticks to avoid overcrowding\n      return Math.max(1, Math.min(tickCount, 12));\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/PlotView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"plotWrapper\"\n    class=\"c-plot holder holder-plot has-control-bar\"\n    :class=\"isStale && 'is-stale'\"\n  >\n    <div\n      ref=\"plotContainer\"\n      class=\"l-view-section u-style-receiver js-style-receiver\"\n      aria-label=\"Plot Container Style Target\"\n      :class=\"{\n        's-status-timeconductor-unsynced': status === 'timeconductor-unsynced'\n      }\"\n    >\n      <ProgressBar\n        v-show=\"!!loading\"\n        class=\"c-telemetry-table__progress-bar\"\n        :model=\"{ progressPerc: null }\"\n      />\n      <MctPlot\n        ref=\"mctPlot\"\n        :class=\"[plotLegendExpandedStateClass, plotLegendPositionClass]\"\n        :init-grid-lines=\"gridLinesProp\"\n        :init-cursor-guide=\"cursorGuide\"\n        :options=\"options\"\n        :limit-line-labels=\"limitLineLabelsProp\"\n        :color-palette=\"colorPalette\"\n        @loading-updated=\"loadingUpdated\"\n        @status-updated=\"setStatus\"\n        @config-loaded=\"updateReady\"\n        @lock-highlight-point=\"lockHighlightPointUpdated\"\n        @highlights=\"highlightsUpdated\"\n        @cursor-guide=\"onCursorGuideChange\"\n        @grid-lines=\"onGridLinesChange\"\n      >\n        <PlotLegend\n          v-if=\"configReady && hideLegend === false\"\n          :cursor-locked=\"lockHighlightPoint\"\n          :highlights=\"highlights\"\n          @legend-hover-changed=\"legendHoverChanged\"\n          @expanded=\"updateExpanded\"\n          @position=\"updatePosition\"\n        />\n      </MctPlot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport ImageExporter from '../../exporters/ImageExporter.js';\nimport ProgressBar from '../../ui/components/ProgressBar.vue';\nimport PlotLegend from './legend/PlotLegend.vue';\nimport eventHelpers from './lib/eventHelpers.js';\nimport MctPlot from './MctPlot.vue';\n\nexport default {\n  components: {\n    MctPlot,\n    ProgressBar,\n    PlotLegend\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  props: {\n    options: {\n      type: Object,\n      default() {\n        return {\n          compact: false\n        };\n      }\n    },\n    gridLines: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    cursorGuide: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    parentLimitLineLabels: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    colorPalette: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    hideLegend: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: [\n    'loading-updated',\n    'lock-highlight-point',\n    'grid-lines',\n    'highlights',\n    'config-loaded',\n    'cursor-guide'\n  ],\n  data() {\n    return {\n      loading: false,\n      status: '',\n      limitLineLabels: undefined,\n      lockHighlightPoint: false,\n      highlights: [],\n      expanded: false,\n      position: undefined,\n      configReady: false\n    };\n  },\n  computed: {\n    limitLineLabelsProp() {\n      return this.parentLimitLineLabels ?? this.limitLineLabels;\n    },\n    gridLinesProp() {\n      return this.gridLines ?? !this.options.compact;\n    },\n    plotLegendPositionClass() {\n      return this.position ? `plot-legend-${this.position}` : '';\n    },\n    plotLegendExpandedStateClass() {\n      if (this.expanded) {\n        return 'plot-legend-expanded';\n      } else {\n        return 'plot-legend-collapsed';\n      }\n    }\n  },\n  created() {\n    eventHelpers.extend(this);\n    this.imageExporter = new ImageExporter(this.openmct);\n    this.loadComposition();\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  unmounted() {\n    this.destroy();\n  },\n  methods: {\n    loadComposition() {\n      this.compositionCollection = this.openmct.composition.get(this.domainObject);\n\n      if (this.compositionCollection) {\n        this.compositionCollection.on('add', this.subscribeToStaleness);\n        this.compositionCollection.on('remove', this.removeSubscription);\n        this.compositionCollection.load();\n      }\n    },\n    removeSubscription(identifier) {\n      this.triggerUnsubscribeFromStaleness({\n        identifier\n      });\n    },\n    loadingUpdated(loading) {\n      this.loading = loading;\n      this.$emit('loading-updated', ...arguments);\n    },\n    destroy() {\n      if (this.compositionCollection) {\n        this.compositionCollection.off('add', this.subscribeToStaleness);\n        this.compositionCollection.off('remove', this.removeSubscription);\n      }\n\n      this.imageExporter = null;\n      this.stopListening();\n    },\n    exportJPG(filename) {\n      const plotElement = this.$refs.plotContainer;\n      filename = filename ?? `${this.domainObject.name} - plot`;\n\n      this.imageExporter.exportJPG(plotElement, filename, 'export-plot');\n    },\n    exportPNG(filename) {\n      const plotElement = this.$refs.plotContainer;\n      filename = filename ?? `${this.domainObject.name} - plot`;\n\n      this.imageExporter.exportPNG(plotElement, filename, 'export-plot');\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    getViewContext() {\n      return {\n        exportPNG: this.exportPNG,\n        exportJPG: this.exportJPG\n      };\n    },\n    lockHighlightPointUpdated(data) {\n      this.lockHighlightPoint = data;\n      this.$emit('lock-highlight-point', ...arguments);\n    },\n    highlightsUpdated(data) {\n      this.highlights = data;\n      this.$emit('highlights', ...arguments);\n    },\n    legendHoverChanged(data) {\n      this.limitLineLabels = data;\n    },\n    updateExpanded(expanded) {\n      this.expanded = expanded;\n    },\n    updatePosition(position) {\n      this.position = position;\n    },\n    updateReady(ready) {\n      this.configReady = ready;\n      this.$emit('config-loaded', ...arguments);\n    },\n    onCursorGuideChange() {\n      this.$emit('cursor-guide', ...arguments);\n    },\n    onGridLinesChange() {\n      this.$emit('grid-lines', ...arguments);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/PlotViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Plot from './PlotView.vue';\n\nexport default function PlotViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: 'plot-single',\n    name: 'Plot',\n    cssClass: 'icon-telemetry',\n    canView(domainObject, objectPath) {\n      return openmct.telemetry.hasNumericTelemetry(domainObject);\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show: function (element, isEditing, { renderWhenVisible }) {\n          let isCompact = isCompactView(objectPath);\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                Plot\n              },\n              provide: {\n                openmct,\n                domainObject,\n                objectPath,\n                renderWhenVisible\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact\n                  }\n                };\n              },\n              template: '<plot ref=\"plotComponent\" :options=\"options\"></plot>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n        getViewContext() {\n          if (!component) {\n            return {};\n          }\n\n          return component.$refs.plotComponent.getViewContext();\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        },\n        getComponent() {\n          return component;\n        }\n      };\n    },\n    priority() {\n      return openmct.priority.LOW;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/README.md",
    "content": "# Plot Plugin\n\nEnables plot visualization of telemetry data. This plugin adds a plot view that is available from the view switcher for \nall telemetry objects. Two user createable objects are also added by this plugin, for Overlay and Stacked Plots. \nTelemetry objects can be added to Overlay and Stacked Plots via drag and drop.\n\n## Installation\n``` js\nopenmct.install(openmct.plugins.Plot());\n```"
  },
  {
    "path": "src/plugins/plot/actions/ViewActions.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { isPlotView } from '@/plugins/plot/actions/utils';\n\nconst exportPNG = {\n  name: 'Export as PNG',\n  key: 'export-as-png',\n  description: \"Export This View's Data as PNG\",\n  cssClass: 'icon-download',\n  group: 'view',\n  invoke(objectPath, view, filename) {\n    view.getViewContext().exportPNG(filename);\n  }\n};\n\nconst exportJPG = {\n  name: 'Export as JPG',\n  key: 'export-as-jpg',\n  description: \"Export This View's Data as JPG\",\n  cssClass: 'icon-download',\n  group: 'view',\n  invoke(objectPath, view, filename) {\n    view.getViewContext().exportJPG(filename);\n  }\n};\n\nconst viewActions = [exportPNG, exportJPG];\n\nviewActions.forEach((action) => {\n  action.appliesTo = (objectPath, view = {}) => {\n    return isPlotView(view);\n  };\n});\n\nexport default viewActions;\n"
  },
  {
    "path": "src/plugins/plot/actions/utils.js",
    "content": "export function isPlotView(view) {\n  return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked';\n}\n"
  },
  {
    "path": "src/plugins/plot/axis/XAxis.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div v-if=\"loaded\" class=\"gl-plot-axis-area gl-plot-x has-local-controls\">\n    <MctTicks :axis-type=\"'xAxis'\" :position=\"'left'\" :is-utc=\"isUtc\" />\n\n    <div class=\"gl-plot-label gl-plot-x-label\" :class=\"{ 'icon-gear': isEnabledXKeyToggle() }\">\n      {{ xAxisLabel }}\n    </div>\n\n    <select\n      v-show=\"isEnabledXKeyToggle()\"\n      v-model=\"selectedXKeyOptionKey\"\n      class=\"gl-plot-x-label__select local-controls--hidden\"\n      @change=\"toggleXKeyOption()\"\n    >\n      <option v-for=\"option in xKeyOptions\" :key=\"option.key\" :value=\"option.key\">\n        {{ option.name }}\n      </option>\n    </select>\n  </div>\n</template>\n\n<script>\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport MctTicks from '../MctTicks.vue';\n\nexport default {\n  components: {\n    MctTicks\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    seriesModel: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  data() {\n    return {\n      selectedXKeyOptionKey: '',\n      xKeyOptions: [],\n      xAxis: {},\n      loaded: false,\n      xAxisLabel: '',\n      isUtc: this.openmct.time.getTimeSystem().isUTCBased\n    };\n  },\n  mounted() {\n    eventHelpers.extend(this);\n    this.xAxis = this.getXAxisFromConfig();\n    this.loaded = true;\n    this.setUpXAxisOptions();\n    this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);\n    this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);\n  },\n  beforeUnmount() {\n    this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);\n  },\n  methods: {\n    isEnabledXKeyToggle() {\n      const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;\n      const isFrozen = this.xAxis.get('frozen');\n      const inRealTimeMode = this.openmct.time.isRealTime();\n\n      return isSinglePlot && !isFrozen && !inRealTimeMode;\n    },\n    getXAxisFromConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      let config = configStore.get(configId);\n      if (config) {\n        return config.xAxis;\n      }\n    },\n    toggleXKeyOption() {\n      const selectedXKey = this.selectedXKeyOptionKey;\n      const seriesData = this.seriesModel.getSeriesData();\n      const dataForSelectedXKey = seriesData ? seriesData[0][selectedXKey] : undefined;\n\n      if (dataForSelectedXKey !== undefined) {\n        this.xAxis.set('key', selectedXKey);\n      } else {\n        this.openmct.notifications.error(\n          'Cannot change x-axis view as no data exists for this view type.'\n        );\n        const xAxisKey = this.xAxis.get('key');\n        this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;\n      }\n    },\n    getXKeyOption(key) {\n      return this.xKeyOptions.find((option) => option.key === key);\n    },\n    syncXAxisToTimeSystem(timeSystem) {\n      const xAxisKey = this.xAxis.get('key');\n      if (xAxisKey !== timeSystem.key) {\n        this.xAxis.set('key', timeSystem.key);\n        this.xAxis.resetSeries();\n        this.setUpXAxisOptions();\n      }\n      this.isUtc = timeSystem.isUTCBased;\n    },\n    setUpXAxisOptions() {\n      const xAxisKey = this.xAxis.get('key');\n      this.xKeyOptions = [];\n\n      if (this.seriesModel.metadata) {\n        this.xKeyOptions = this.seriesModel.metadata.valuesForHints(['domain']).map(function (o) {\n          return {\n            name: o.name,\n            key: o.key\n          };\n        });\n      }\n\n      this.xAxisLabel = this.xAxis.get('label');\n      this.selectedXKeyOptionKey =\n        this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/axis/YAxis.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"loaded\"\n    class=\"gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis\"\n    :style=\"yAxisStyle\"\n  >\n    <div class=\"gl-plot-label gl-plot-y-label\">\n      <div class=\"gl-plot-y-label-swatch-container\">\n        <span\n          v-for=\"(colorAsHexString, index) in seriesColors\"\n          :key=\"`${colorAsHexString}-${index}`\"\n          class=\"plot-series-color-swatch\"\n          :style=\"{ 'background-color': colorAsHexString }\"\n        >\n        </span>\n      </div>\n      <span :class=\"{ 'icon-gear-after': yKeyOptions.length > 1 && singleSeries }\">{{\n        canShowYAxisLabel ? yAxisLabel : `Y Axis ${id}`\n      }}</span>\n      <span\n        v-if=\"showVisibilityToggle\"\n        :class=\"{ 'icon-eye-open': visible, 'icon-eye-disabled': !visible }\"\n        @click=\"toggleSeriesVisibility\"\n      ></span>\n    </div>\n    <select\n      v-if=\"yKeyOptions.length > 1 && singleSeries\"\n      v-model=\"yAxisLabel\"\n      class=\"gl-plot-y-label__select local-controls--hidden\"\n      @change=\"toggleYAxisLabel\"\n    >\n      <option\n        v-for=\"(option, index) in yKeyOptions\"\n        :key=\"index\"\n        :value=\"option.name\"\n        :selected=\"option.name === yAxisLabel\"\n      >\n        {{ option.name }}\n      </option>\n    </select>\n\n    <MctTicks\n      :axis-id=\"id\"\n      :axis-type=\"'yAxis'\"\n      class=\"gl-plot-ticks\"\n      :position=\"'top'\"\n      @plot-tick-width=\"onTickWidthChange\"\n    />\n  </div>\n</template>\n\n<script>\nimport { inject } from 'vue';\n\nimport { useAlignment } from '../../../ui/composables/alignmentContext.js';\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport MctTicks from '../MctTicks.vue';\n\nconst AXIS_PADDING = 20;\n\nexport default {\n  components: {\n    MctTicks\n  },\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  props: {\n    id: {\n      type: Number,\n      default() {\n        return 1;\n      }\n    },\n    position: {\n      type: String,\n      default() {\n        return 'left';\n      }\n    }\n  },\n  emits: ['toggle-axis-visibility', 'y-key-changed'],\n  setup() {\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);\n\n    return { alignmentData };\n  },\n  data() {\n    return {\n      yAxisLabel: 'none',\n      loaded: false,\n      yKeyOptions: [],\n      hasSameRangeValue: true,\n      singleSeries: true,\n      mainYAxisId: null,\n      hasAdditionalYAxes: false,\n      seriesColors: [],\n      visible: true,\n      selfTickWidth: 0\n    };\n  },\n  computed: {\n    showVisibilityToggle() {\n      return this.domainObject.type === 'telemetry.plot.overlay';\n    },\n    canShowYAxisLabel() {\n      return this.singleSeries === true || this.hasSameRangeValue === true;\n    },\n    yAxisStyle() {\n      let style = {\n        width: `${this.selfTickWidth + AXIS_PADDING}px`\n      };\n      const multipleAxesPadding = this.alignmentData.multiple ? AXIS_PADDING : 0;\n\n      if (this.position === 'right') {\n        style.left = `-${this.selfTickWidth + AXIS_PADDING}px`;\n      } else {\n        const thisIsTheSecondLeftAxis = this.id - 1 > 0;\n        if (this.alignmentData.multiple && thisIsTheSecondLeftAxis) {\n          const otherAxisWidth = this.alignmentData.leftWidth - this.selfTickWidth;\n          style.left = `${this.alignmentData.leftWidth - otherAxisWidth - this.selfTickWidth}px`;\n          style['border-right'] = `1px solid`;\n        } else {\n          style.left = `${this.alignmentData.leftWidth - this.selfTickWidth + multipleAxesPadding}px`;\n        }\n      }\n\n      return style;\n    }\n  },\n  mounted() {\n    this.seriesModels = [];\n    eventHelpers.extend(this);\n    this.initAxisAndSeriesConfig();\n    this.loaded = true;\n    this.setUpYAxisOptions();\n  },\n  methods: {\n    initAxisAndSeriesConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      let config = configStore.get(configId);\n      if (config) {\n        this.mainYAxisId = config.yAxis.id;\n        this.hasAdditionalYAxes = config?.additionalYAxes.length;\n        if (this.id && this.id !== this.mainYAxisId) {\n          this.yAxis = config.additionalYAxes.find((yAxis) => yAxis.id === this.id);\n        } else {\n          this.yAxis = config.yAxis;\n        }\n\n        this.config = config;\n        this.listenTo(this.config.series, 'add', this.addSeries, this);\n        this.listenTo(this.config.series, 'remove', this.removeSeries, this);\n        this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this);\n\n        this.config.series.models.forEach(this.addSeries, this);\n      }\n    },\n    addOrRemoveSeries(series) {\n      const yAxisId = series.get('yAxisId');\n      if (yAxisId === this.id) {\n        this.addSeries(series);\n      } else {\n        this.removeSeries(series);\n      }\n    },\n    addSeries(series, index) {\n      const yAxisId = series.get('yAxisId');\n      const seriesIndex = this.seriesModels.findIndex((model) =>\n        this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier'))\n      );\n\n      if (yAxisId === this.id && seriesIndex < 0) {\n        this.seriesModels.push(series);\n        this.processSeries();\n        this.setUpYAxisOptions();\n      }\n\n      this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);\n      this.listenTo(series, 'change:color', this.updateSeriesColors.bind(this, series), this);\n    },\n    removeSeries(plotSeries) {\n      const seriesIndex = this.seriesModels.findIndex((model) =>\n        this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier'))\n      );\n      if (seriesIndex > -1) {\n        this.seriesModels.splice(seriesIndex, 1);\n        this.processSeries();\n        this.setUpYAxisOptions();\n      }\n    },\n    processSeries() {\n      this.hasSameRangeValue = this.seriesModels.every((model) => {\n        return model.get('yKey') === this.seriesModels[0].get('yKey');\n      });\n      this.singleSeries = this.seriesModels.length === 1;\n      this.updateSeriesColors();\n    },\n    updateSeriesColors() {\n      this.seriesColors = this.seriesModels.map((model) => {\n        return model.get('color').asHexString();\n      });\n    },\n    setUpYAxisOptions() {\n      this.yKeyOptions = [];\n      if (!this.seriesModels.length) {\n        return;\n      }\n\n      const seriesModel = this.seriesModels[0];\n      if (seriesModel.metadata) {\n        this.yKeyOptions = seriesModel.metadata.valuesForHints(['range']).map(function (o) {\n          return {\n            name: o.name,\n            key: o.key\n          };\n        });\n      }\n\n      //  set yAxisLabel if none is set yet\n      if (this.yAxisLabel === 'none') {\n        this.yAxisLabel = this.yAxis.get('label');\n      }\n    },\n    toggleYAxisLabel() {\n      let yAxisObject = this.yKeyOptions.filter((o) => o.name === this.yAxisLabel)[0];\n\n      if (yAxisObject) {\n        this.$emit('y-key-changed', yAxisObject.key, this.id);\n        this.yAxis.set('label', this.yAxisLabel);\n      }\n    },\n    onTickWidthChange(data) {\n      this.selfTickWidth = data.width;\n    },\n    toggleSeriesVisibility() {\n      this.visible = !this.visible;\n      this.$emit('toggle-axis-visibility', {\n        id: this.id,\n        visible: this.visible\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/chart/LimitLabel.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-plot-limit\" :style=\"styleObj\" :class=\"limitClass\">\n    <div class=\"c-plot-limit__label\">\n      <span class=\"c-plot-limit__direction-icon\"></span>\n      <span class=\"c-plot-limit__severity-icon\"></span>\n      <span class=\"c-plot-limit__limit-value\">{{ limit.value }}</span>\n      <span\n        class=\"c-plot-limit__series-color-swatch\"\n        :style=\"{ 'background-color': limit.seriesColor }\"\n      ></span>\n      <span class=\"c-plot-limit__series-name\">{{ limit.name }}</span>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { getLimitClass } from './limitUtil.js';\n\nexport default {\n  props: {\n    limit: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    },\n    point: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    }\n  },\n  computed: {\n    styleObj() {\n      const top = `${this.point.top}px`;\n\n      return {\n        top: top\n      };\n    },\n    limitClass() {\n      return getLimitClass(this.limit, 'c-plot-limit--');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/chart/LimitLine.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div :style=\"styleObj\" class=\"c-plot-limit-line js-limit-line\" :class=\"limitClass\"></div>\n</template>\n\n<script>\nimport { getLimitClass } from './limitUtil.js';\n\nexport default {\n  props: {\n    point: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    },\n    limit: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  computed: {\n    styleObj() {\n      const top = `${this.point.top}px`;\n\n      return {\n        top: top\n      };\n    },\n    limitClass() {\n      return getLimitClass(this.limit, 'c-plot-limit-line--');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartAlarmLineSet.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport eventHelpers from '../lib/eventHelpers.js';\n\nexport default class MCTChartAlarmLineSet {\n  /**\n   * @param {Bounds} bounds\n   */\n  constructor(series, chart, offset, bounds) {\n    this.series = series;\n    this.chart = chart;\n    this.offset = offset;\n    this.bounds = bounds;\n    this.limits = [];\n\n    eventHelpers.extend(this);\n    this.listenTo(series, 'limitBounds', this.updateBounds, this);\n    this.listenTo(series, 'limits', this.getLimitPoints, this);\n    this.listenTo(series, 'change:xKey', this.getLimitPoints, this);\n\n    if (series.limits) {\n      this.getLimitPoints(series);\n    }\n  }\n\n  /**\n   * @param {Bounds} bounds\n   */\n  updateBounds(bounds) {\n    this.bounds = bounds;\n    this.getLimitPoints(this.series);\n  }\n\n  color() {\n    return this.series.get('color');\n  }\n\n  name() {\n    return this.series.get('name');\n  }\n\n  makePoint(point, series) {\n    if (!this.offset.xVal) {\n      this.chart.setOffset(point, undefined, series);\n    }\n\n    return {\n      x: this.offset.xVal(point, series),\n      y: this.offset.yVal(point, series)\n    };\n  }\n\n  getLimitPoints(series) {\n    this.limits = [];\n    let xKey = series.get('xKey');\n    Object.keys(series.limits).forEach((key) => {\n      const limitForLevel = series.limits[key];\n      if (limitForLevel.high) {\n        this.limits.push({\n          seriesKey: series.keyString,\n          level: key.toLowerCase(),\n          name: this.name(),\n          seriesColor: series.get('color').asHexString(),\n          point: this.makePoint(\n            Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high),\n            series\n          ),\n          value: series.getYVal(limitForLevel.high),\n          color: limitForLevel.high.color,\n          isUpper: true\n        });\n      }\n\n      if (limitForLevel.low) {\n        this.limits.push({\n          seriesKey: series.keyString,\n          level: key.toLowerCase(),\n          name: this.name(),\n          seriesColor: series.get('color').asHexString(),\n          point: this.makePoint(\n            Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low),\n            series\n          ),\n          value: series.getYVal(limitForLevel.low),\n          color: limitForLevel.low.color,\n          isUpper: false\n        });\n      }\n    }, this);\n  }\n\n  reset() {\n    this.limits = [];\n    if (this.series.limits) {\n      this.getLimitPoints(this.series);\n    }\n  }\n\n  destroy() {\n    this.stopListening();\n  }\n}\n\n/**\n@typedef {import('@/api/time/TimeContext').Bounds} Bounds\n*/\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartAlarmPointSet.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport eventHelpers from '../lib/eventHelpers.js';\n\nexport default class MCTChartAlarmPointSet {\n  constructor(series, chart, offset) {\n    this.series = series;\n    this.chart = chart;\n    this.offset = offset;\n    this.points = [];\n\n    eventHelpers.extend(this);\n\n    this.listenTo(series, 'add', this.append, this);\n    this.listenTo(series, 'remove', this.remove, this);\n    this.listenTo(series, 'reset', this.reset, this);\n    this.listenTo(series, 'destroy', this.destroy, this);\n\n    this.series.getSeriesData().forEach(function (point, index) {\n      this.append(point, index, series);\n    }, this);\n  }\n\n  append(datum) {\n    if (datum.mctLimitState) {\n      this.points.push({\n        x: this.offset.xVal(datum, this.series),\n        y: this.offset.yVal(datum, this.series),\n        datum: datum\n      });\n    }\n  }\n\n  remove(datum) {\n    this.points = this.points.filter(function (p) {\n      return p.datum !== datum;\n    });\n  }\n\n  reset() {\n    this.points = [];\n  }\n\n  destroy() {\n    this.stopListening();\n  }\n}\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartLineLinear.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport MCTChartSeriesElement from './MCTChartSeriesElement.js';\n\nexport default class MCTChartLineLinear extends MCTChartSeriesElement {\n  addPoint(point, start) {\n    this.buffer[start] = point.x;\n    this.buffer[start + 1] = point.y;\n  }\n}\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartLineStepAfter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport MCTChartSeriesElement from './MCTChartSeriesElement.js';\n\nexport default class MCTChartLineStepAfter extends MCTChartSeriesElement {\n  removePoint(index) {\n    if (index > 0 && index / 2 < this.count) {\n      this.buffer[index + 1] = this.buffer[index - 1];\n    }\n  }\n\n  vertexCountForPointAtIndex(index) {\n    if (index === 0 && this.count === 0) {\n      return 2;\n    }\n\n    return 4;\n  }\n\n  startIndexForPointAtIndex(index) {\n    if (index === 0) {\n      return 0;\n    }\n\n    return 2 + (index - 1) * 4;\n  }\n\n  addPoint(point, start) {\n    if (start === 0 && this.count === 0) {\n      // First point is easy.\n      this.buffer[start] = point.x;\n      this.buffer[start + 1] = point.y; // one point\n    } else if (start === 0 && this.count > 0) {\n      // Unshifting requires adding an extra point.\n      this.buffer[start] = point.x;\n      this.buffer[start + 1] = point.y;\n      this.buffer[start + 2] = this.buffer[start + 4];\n      this.buffer[start + 3] = point.y;\n    } else {\n      // Appending anywhere in line, insert standard two points.\n      this.buffer[start] = point.x;\n      this.buffer[start + 1] = this.buffer[start - 1];\n      this.buffer[start + 2] = point.x;\n      this.buffer[start + 3] = point.y;\n\n      if (start < this.count * 2) {\n        // Insert into the middle, need to update the following\n        // point.\n        this.buffer[start + 5] = point.y;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartPointSet.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport MCTChartSeriesElement from './MCTChartSeriesElement.js';\n\n// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?\nexport default class MCTChartPointSet extends MCTChartSeriesElement {\n  addPoint(point, start) {\n    this.buffer[start] = point.x;\n    this.buffer[start + 1] = point.y;\n  }\n}\n"
  },
  {
    "path": "src/plugins/plot/chart/MCTChartSeriesElement.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport eventHelpers from '../lib/eventHelpers.js';\n\n/** @abstract */\nexport default class MCTChartSeriesElement {\n  constructor(series, chart, offset) {\n    this.series = series;\n    this.chart = chart;\n    this.offset = offset;\n    this.buffer = new Float32Array(20000);\n    this.count = 0;\n\n    eventHelpers.extend(this);\n\n    this.listenTo(series, 'add', this.append, this);\n    this.listenTo(series, 'remove', this.remove, this);\n    this.listenTo(series, 'reset', this.reset, this);\n    this.listenTo(series, 'destroy', this.destroy, this);\n    this.series.getSeriesData().forEach(function (point, index) {\n      this.append(point, index, series);\n    }, this);\n  }\n\n  getBuffer() {\n    if (this.isTempBuffer) {\n      this.buffer = new Float32Array(this.buffer);\n      this.isTempBuffer = false;\n    }\n\n    return this.buffer;\n  }\n\n  color() {\n    return this.series.get('color');\n  }\n\n  vertexCountForPointAtIndex(index) {\n    return 2;\n  }\n\n  startIndexForPointAtIndex(index) {\n    return 2 * index;\n  }\n\n  removeSegments(index, count) {\n    const target = index;\n    const start = index + count;\n    const end = this.count * 2;\n    this.buffer.copyWithin(target, start, end);\n    for (let zero = end - count; zero < end; zero++) {\n      this.buffer[zero] = 0;\n    }\n  }\n\n  /** @abstract */\n  removePoint(index) {}\n\n  /** @abstract */\n  addPoint(point, index) {}\n\n  remove(point, index, series) {\n    const vertexCount = this.vertexCountForPointAtIndex(index);\n    const removalPoint = this.startIndexForPointAtIndex(index);\n\n    this.removeSegments(removalPoint, vertexCount);\n\n    // TODO useless makePoint call?\n    this.makePoint(point, series);\n    this.removePoint(removalPoint);\n\n    this.count -= vertexCount / 2;\n  }\n\n  makePoint(point, series) {\n    if (!this.offset.xVal) {\n      this.chart.setOffset(point, undefined, series);\n    }\n\n    // Here x,y are the offsets of the current point from the first data point.\n    return {\n      x: this.offset.xVal(point, series),\n      y: this.offset.yVal(point, series)\n    };\n  }\n\n  append(point, index, series) {\n    const pointsRequired = this.vertexCountForPointAtIndex(index);\n    const insertionPoint = this.startIndexForPointAtIndex(index);\n    this.growIfNeeded(pointsRequired);\n    this.makeInsertionPoint(insertionPoint, pointsRequired);\n    this.addPoint(this.makePoint(point, series), insertionPoint);\n    this.count += pointsRequired / 2;\n  }\n\n  makeInsertionPoint(insertionPoint, pointsRequired) {\n    if (this.count * 2 > insertionPoint) {\n      if (!this.isTempBuffer) {\n        this.buffer = Array.prototype.slice.apply(this.buffer);\n        this.isTempBuffer = true;\n      }\n\n      const target = insertionPoint + pointsRequired;\n      let start = insertionPoint;\n      for (; start < target; start++) {\n        this.buffer.splice(start, 0, 0);\n      }\n    }\n  }\n\n  reset() {\n    this.buffer = new Float32Array(20000);\n    this.count = 0;\n    //TODO: Should we call resetYOffsetAndSeriesDataForYAxis here?\n    if (this.offset.x) {\n      this.series.getSeriesData().forEach(function (point, index) {\n        this.append(point, index, this.series);\n      }, this);\n    }\n  }\n\n  growIfNeeded(pointsRequired) {\n    const remainingPoints = this.buffer.length - this.count * 2;\n    let temp;\n\n    if (remainingPoints <= pointsRequired) {\n      temp = new Float32Array(this.buffer.length + 20000);\n      temp.set(this.buffer);\n      this.buffer = temp;\n    }\n  }\n\n  destroy() {\n    this.stopListening();\n  }\n}\n\n/** @typedef {any} TODO */\n\n/** @typedef {import('../configuration/PlotSeries').default} PlotSeries */\n\n/**\n@typedef {{\n    x: (x: number) => number\n    y: (y: number) => number\n    xVal: (point: Point, pSeries: PlotSeries) => number\n    yVal: (point: Point, pSeries: PlotSeries) => number\n    xKey: TODO\n    yKey: TODO\n}} Offset\n*/\n"
  },
  {
    "path": "src/plugins/plot/chart/MctChart.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"chart\" class=\"gl-plot-chart-area\">\n    <canvas\n      id=\"2dContext\"\n      :style=\"canvasStyle\"\n      class=\"js-overlay-canvas\"\n      role=\"img\"\n      aria-label=\"Overlay Canvas\"\n    ></canvas>\n    <canvas\n      id=\"webglContext\"\n      :style=\"canvasStyle\"\n      class=\"js-main-canvas\"\n      role=\"img\"\n      aria-label=\"Plot Canvas\"\n    ></canvas>\n    <div ref=\"limitArea\" class=\"js-limit-area\" aria-hidden=\"true\">\n      <LimitLabel\n        v-for=\"(limitLabel, index) in visibleLimitLabels\"\n        :key=\"`limitLabel-${limitLabel.limit.seriesKey}-${index}`\"\n        :point=\"limitLabel.point\"\n        :limit=\"limitLabel.limit\"\n      ></LimitLabel>\n      <LimitLine\n        v-for=\"(limitLine, index) in visibleLimitLines\"\n        :key=\"`limitLine-${limitLine.limit.seriesKey}${index}`\"\n        :point=\"limitLine.point\"\n        :limit=\"limitLine.limit\"\n      ></LimitLine>\n    </div>\n  </div>\n</template>\n\n<script>\nimport mount from 'utils/mount';\nimport { toRaw } from 'vue';\n\nimport configStore from '../configuration/ConfigStore.js';\nimport PlotConfigurationModel from '../configuration/PlotConfigurationModel.js';\nimport { DrawLoader } from '../draw/DrawLoader.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport LimitLabel from './LimitLabel.vue';\nimport LimitLine from './LimitLine.vue';\nimport MCTChartAlarmLineSet from './MCTChartAlarmLineSet.js';\nimport MCTChartAlarmPointSet from './MCTChartAlarmPointSet.js';\nimport MCTChartLineLinear from './MCTChartLineLinear.js';\nimport MCTChartLineStepAfter from './MCTChartLineStepAfter.js';\nimport MCTChartPointSet from './MCTChartPointSet.js';\n\nconst MARKER_SIZE = 6.0;\nconst HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;\nconst ANNOTATION_SIZE = MARKER_SIZE * 3.0;\nconst CLEARANCE = 15;\n// These attributes are changed in the plot model, but we don't need to react to the changes.\nconst NO_HANDLING_NEEDED_ATTRIBUTES = {\n  label: 'label',\n  values: 'values',\n  format: 'format',\n  color: 'color',\n  name: 'name',\n  unit: 'unit'\n};\n// These attributes in turn set one of HANDLED_ATTRIBUTES, so we don't need specific listeners for them - this prevents excessive redraws.\nconst IMPLICIT_HANDLED_ATTRIBUTES = {\n  range: 'range',\n  //series stats update y axis stats\n  stats: 'stats',\n  frozen: 'frozen',\n  autoscale: 'autoscale',\n  autoscalePadding: 'autoscalePadding',\n  logMode: 'logMode',\n  yKey: 'yKey'\n};\n// Attribute changes that we are specifically handling with listeners\nconst HANDLED_ATTRIBUTES = {\n  //X and Y Axis attributes\n  key: 'key',\n  displayRange: 'displayRange',\n  //series attributes\n  xKey: 'xKey',\n  interpolate: 'interpolate',\n  markers: 'markers',\n  markerShape: 'markerShape',\n  markerSize: 'markerSize',\n  alarmMarkers: 'alarmMarkers',\n  limitLines: 'limitLines',\n  yAxisId: 'yAxisId'\n};\n\nexport default {\n  components: { LimitLine, LimitLabel },\n  inject: ['openmct', 'domainObject', 'objectPath', 'renderWhenVisible'],\n  props: {\n    rectangles: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    highlights: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    annotatedPointsBySeries: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    annotationSelectionsBySeries: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    showLimitLineLabels: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    hiddenYAxisIds: {\n      type: Array,\n      default() {\n        return [];\n      }\n    },\n    annotationViewingAndEditingAllowed: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['chart-loaded', 'plot-reinitialize-canvas'],\n  data() {\n    return {\n      visibleLimitLabels: [],\n      visibleLimitLines: []\n    };\n  },\n  computed: {\n    canvasStyle() {\n      return {\n        position: 'absolute',\n        background: 'none',\n        width: '100%',\n        height: '100%'\n      };\n    }\n  },\n  watch: {\n    highlights: {\n      handler() {\n        this.scheduleDraw();\n      },\n      deep: true\n    },\n    annotatedPointsBySeries: {\n      handler() {\n        this.scheduleDraw();\n      }\n    },\n    annotationSelectionsBySeries: {\n      handler() {\n        this.scheduleDraw();\n      }\n    },\n    rectangles: {\n      handler() {\n        this.scheduleDraw();\n      },\n      deep: true\n    },\n    showLimitLineLabels() {\n      this.updateLimitLines();\n    },\n    hiddenYAxisIds: {\n      handler() {\n        this.hiddenYAxisIds.forEach((id) => {\n          this.resetYOffsetAndSeriesDataForYAxis(id);\n        });\n        this.scheduleDraw(true);\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.chartVisible = true;\n    this.chartContainer = this.$refs.chart;\n    this.drawnOnce = false;\n    const rootContainer = this.openmct.element;\n    const options = {\n      root: rootContainer\n    };\n    this.visibilityObserver = new IntersectionObserver(this.visibilityChanged, options);\n    eventHelpers.extend(this);\n    this.seriesModels = [];\n    this.config = this.getConfig();\n    this.isDestroyed = false;\n    this.lines = [];\n    this.limitLines = [];\n    this.pointSets = [];\n    this.alarmSets = [];\n    const yAxisId = this.config.yAxis.get('id');\n    this.offset = {\n      [yAxisId]: {}\n    };\n    this.listenTo(\n      this.config.yAxis,\n      `change:${HANDLED_ATTRIBUTES.displayRange}`,\n      this.scheduleDraw\n    );\n    this.listenTo(\n      this.config.yAxis,\n      `change:${HANDLED_ATTRIBUTES.key}`,\n      this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId),\n      this\n    );\n    this.listenTo(this.config.yAxis, 'change', this.redrawIfNotAlreadyHandled);\n    if (this.config.additionalYAxes.length) {\n      this.config.additionalYAxes.forEach((yAxis) => {\n        const id = yAxis.get('id');\n        this.offset[id] = {};\n        this.listenTo(yAxis, `change:${HANDLED_ATTRIBUTES.displayRange}`, this.scheduleDraw);\n        this.listenTo(\n          yAxis,\n          `change:${HANDLED_ATTRIBUTES.key}`,\n          this.resetYOffsetAndSeriesDataForYAxis.bind(this, id),\n          this\n        );\n        this.listenTo(yAxis, 'change', this.redrawIfNotAlreadyHandled);\n      });\n    }\n\n    this.seriesElements = new WeakMap();\n    this.seriesLimits = new WeakMap();\n\n    const canvasReadyForDrawing = this.readyCanvasForDrawing();\n    if (canvasReadyForDrawing) {\n      this.draw();\n    }\n\n    this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);\n    this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);\n\n    this.listenTo(this.config.xAxis, 'change:displayRange', this.scheduleDraw);\n    this.listenTo(this.config.xAxis, 'change', this.redrawIfNotAlreadyHandled);\n    this.config.series.forEach(this.onSeriesAdd, this);\n    this.$emit('chart-loaded');\n\n    this.handleWindowResize = _.debounce(this.handleWindowResize, 250);\n    this.chartResizeObserver = new ResizeObserver(this.handleWindowResize);\n    this.chartResizeObserver.observe(this.$parent.$refs.chartContainer);\n  },\n  beforeUnmount() {\n    this.destroy();\n    this.visibilityObserver.unobserve(this.chartContainer);\n  },\n  methods: {\n    handleWindowResize() {\n      this.scheduleDraw(true);\n    },\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      let config = configStore.get(configId);\n      if (!config) {\n        config = new PlotConfigurationModel({\n          id: configId,\n          domainObject: this.domainObject,\n          openmct: this.openmct\n        });\n        configStore.add(configId, config);\n      }\n\n      return config;\n    },\n    visibilityChanged([entry]) {\n      // Per https://github.com/nasa/openmct/issues/7405, we only want to draw when the chart is visible.\n      // and we need to use the Open MCT root element as the root of the intersection observer.\n      if (entry.target === this.chartContainer) {\n        const wasVisible = this.chartVisible;\n        const isNowVisible = entry.isIntersecting;\n        const chartInOverlayWindow = this.chartContainer?.closest('.js-overlay') !== null;\n\n        if (!isNowVisible && !chartInOverlayWindow) {\n          this.chartVisible = false;\n          this.destroyCanvas();\n        } else if (!isNowVisible && chartInOverlayWindow) {\n          this.chartVisible = true;\n        } else if (!wasVisible && isNowVisible) {\n          this.chartVisible = true;\n          // rebuild the chart\n          this.buildCanvasElements();\n          const canvasInitialized = this.readyCanvasForDrawing();\n          if (canvasInitialized) {\n            this.draw();\n          }\n          this.$emit('plot-reinitialize-canvas');\n        } else {\n          this.chartVisible = isNowVisible;\n        }\n      }\n    },\n    reDraw(newXKey, oldXKey, series) {\n      this.changeInterpolate(newXKey, oldXKey, series);\n      this.changeMarkers(newXKey, oldXKey, series);\n      this.changeAlarmMarkers(newXKey, oldXKey, series);\n      this.changeLimitLines(newXKey, oldXKey, series);\n    },\n    onSeriesAdd(series, index) {\n      this.seriesModels[index] = series;\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.xKey}`, this.reDraw, this);\n      this.listenTo(\n        series,\n        `change:${HANDLED_ATTRIBUTES.interpolate}`,\n        this.changeInterpolate,\n        this\n      );\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markers}`, this.changeMarkers, this);\n      this.listenTo(\n        series,\n        `change:${HANDLED_ATTRIBUTES.alarmMarkers}`,\n        this.changeAlarmMarkers,\n        this\n      );\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.limitLines}`, this.changeLimitLines, this);\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.yAxisId}`, this.resetAxisAndRedraw, this);\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markerShape}`, this.scheduleDraw, this);\n      this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markerSize}`, this.scheduleDraw, this);\n      this.listenTo(series, 'change', this.redrawIfNotAlreadyHandled);\n      this.listenTo(series, 'add', this.onAddPoint);\n      this.makeChartElement(series);\n      this.makeLimitLines(series);\n    },\n    onSeriesRemove(seriesToRemove) {\n      this.stopListening(seriesToRemove);\n      this.removeChartElement(seriesToRemove);\n      this.scheduleDraw();\n\n      const seriesIndexToRemove = this.seriesModels.findIndex(\n        (series) => series.keyString === seriesToRemove.keyString\n      );\n      this.seriesModels.splice(seriesIndexToRemove, 1);\n    },\n    onAddPoint(point, insertIndex, series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const seriesYAxisId = series.get('yAxisId');\n      const xRange = this.config.xAxis.get('displayRange');\n\n      let yRange;\n      if (seriesYAxisId === mainYAxisId) {\n        yRange = this.config.yAxis.get('displayRange');\n      } else {\n        yRange = this.config.additionalYAxes\n          .find((yAxis) => yAxis.get('id') === seriesYAxisId)\n          .get('displayRange');\n      }\n\n      const xValue = series.getXVal(point);\n      const yValue = series.getYVal(point);\n\n      // if user is not looking at data within the current bounds, don't draw the point\n      if (\n        xValue > xRange.min &&\n        xValue < xRange.max &&\n        yValue > yRange.min &&\n        yValue < yRange.max\n      ) {\n        this.scheduleDraw();\n      }\n    },\n    changeInterpolate(mode, o, series) {\n      if (mode === o) {\n        return;\n      }\n\n      const elements = this.seriesElements.get(toRaw(series));\n      elements.lines.forEach(function (line) {\n        this.lines.splice(this.lines.indexOf(line), 1);\n        line.destroy();\n      }, this);\n      elements.lines = [];\n\n      const newLine = this.lineForSeries(series);\n      if (newLine) {\n        elements.lines.push(newLine);\n        this.lines.push(newLine);\n      }\n    },\n    changeAlarmMarkers(mode, o, series) {\n      if (mode === o) {\n        return;\n      }\n\n      const elements = this.seriesElements.get(toRaw(series));\n      if (elements.alarmSet) {\n        elements.alarmSet.destroy();\n        this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);\n      }\n\n      elements.alarmSet = this.alarmPointSetForSeries(series);\n      if (elements.alarmSet) {\n        this.alarmSets.push(elements.alarmSet);\n      }\n    },\n    changeMarkers(mode, o, series) {\n      if (mode === o) {\n        return;\n      }\n\n      const elements = this.seriesElements.get(toRaw(series));\n      elements.pointSets.forEach(function (pointSet) {\n        this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);\n        pointSet.destroy();\n      }, this);\n      elements.pointSets = [];\n\n      const pointSet = this.pointSetForSeries(series);\n      if (pointSet) {\n        elements.pointSets.push(pointSet);\n        this.pointSets.push(pointSet);\n      }\n    },\n    changeLimitLines(showLimitLines, oldShowLimitLines, series) {\n      if (showLimitLines === oldShowLimitLines) {\n        return;\n      }\n\n      this.makeLimitLines(series);\n      this.updateLimitLines();\n    },\n    resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {\n      if (!oldYAxisId) {\n        return;\n      }\n\n      //Remove the old chart elements for the series since their offsets are pointing to the old y axis\n      this.removeChartElement(series);\n      this.resetYOffsetAndSeriesDataForYAxis(oldYAxisId);\n\n      //Make the chart elements again for the new y-axis and offset\n      this.makeChartElement(series);\n      this.makeLimitLines(series);\n      this.scheduleDraw(true);\n    },\n    destroy() {\n      this.destroyCanvas();\n      this.isDestroyed = true;\n      this.lines.forEach((line) => line.destroy());\n      this.limitLines.forEach((line) => line.destroy());\n      this.pointSets.forEach((pointSet) => pointSet.destroy());\n      this.alarmSets.forEach((alarmSet) => alarmSet.destroy());\n      DrawLoader.releaseDrawAPI(this.drawAPI);\n      if (this.chartResizeObserver) {\n        this.chartResizeObserver.disconnect();\n      }\n    },\n    resetYOffsetAndSeriesDataForYAxis(yAxisId) {\n      delete this.offset[yAxisId].y;\n      delete this.offset[yAxisId].xVal;\n      delete this.offset[yAxisId].yVal;\n      delete this.offset[yAxisId].xKey;\n      delete this.offset[yAxisId].yKey;\n\n      this.resetResetChartElements(yAxisId);\n    },\n    resetResetChartElements(yAxisId) {\n      const lines = this.lines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));\n      lines.forEach(function (line) {\n        line.reset();\n      });\n      const limitLines = this.limitLines.filter(\n        this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId)\n      );\n      limitLines.forEach(function (line) {\n        line.reset();\n      });\n      const pointSets = this.pointSets.filter(\n        this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId)\n      );\n      pointSets.forEach(function (pointSet) {\n        pointSet.reset();\n      });\n    },\n    setOffset(offsetPoint, index, series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const yAxisId = series.get('yAxisId') || mainYAxisId;\n      if (this.offset[yAxisId].x && this.offset[yAxisId].y) {\n        return;\n      }\n\n      const offsets = {\n        x: series.getXVal(offsetPoint),\n        y: series.getYVal(offsetPoint)\n      };\n\n      this.offset[yAxisId].x = function (x) {\n        return x - offsets.x;\n      }.bind(this);\n      this.offset[yAxisId].y = function (y) {\n        return y - offsets.y;\n      }.bind(this);\n      this.offset[yAxisId].xVal = function (point, pSeries) {\n        return this.offset[yAxisId].x(pSeries.getXVal(point));\n      }.bind(this);\n      this.offset[yAxisId].yVal = function (point, pSeries) {\n        return this.offset[yAxisId].y(pSeries.getYVal(point));\n      }.bind(this);\n    },\n    destroyCanvas() {\n      if (this.isDestroyed) {\n        return;\n      }\n      this.stopListening(this.drawAPI);\n      DrawLoader.releaseDrawAPI(this.drawAPI);\n      if (this.chartContainer) {\n        const canvasElements = this.chartContainer.querySelectorAll('canvas');\n        canvasElements.forEach((canvas) => {\n          canvas.parentNode.removeChild(canvas);\n        });\n      }\n    },\n    readyCanvasForDrawing() {\n      const canvasEls = this.chartContainer.querySelectorAll('canvas');\n      const mainCanvas = canvasEls[1];\n      const overlayCanvas = canvasEls[0];\n      this.canvas = mainCanvas;\n      this.overlay = overlayCanvas;\n      this.drawAPI = DrawLoader.getDrawAPI(mainCanvas, overlayCanvas);\n      if (this.drawAPI?.on) {\n        this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);\n      }\n\n      return Boolean(this.drawAPI);\n    },\n    buildCanvasElements() {\n      const div = document.createElement('div');\n      div.innerHTML = `\n      <canvas style=\"position: absolute; background: none; width: 100%; height: 100%;\" class=\"js-overlay-canvas\"></canvas>\n      <canvas style=\"position: absolute; background: none; width: 100%; height: 100%;\" class=\"js-main-canvas\"></canvas>\n      `;\n      const mainCanvas = div.querySelectorAll('canvas')[1];\n      const overlayCanvas = div.querySelectorAll('canvas')[0];\n      this.chartContainer.appendChild(mainCanvas, this.canvas);\n      this.canvas = mainCanvas;\n      this.chartContainer.appendChild(overlayCanvas, this.overlay);\n      this.overlay = overlayCanvas;\n    },\n    fallbackToCanvas() {\n      console.warn(`📈 fallback to 2D canvas`);\n      this.destroyCanvas();\n      this.buildCanvasElements();\n      this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);\n      this.$emit('plot-reinitialize-canvas');\n    },\n    removeChartElement(series) {\n      const elements = this.seriesElements.get(toRaw(series));\n\n      elements.lines.forEach(function (line) {\n        this.lines.splice(this.lines.indexOf(line), 1);\n        line.destroy();\n      }, this);\n      elements.pointSets.forEach(function (pointSet) {\n        this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);\n        pointSet.destroy();\n      }, this);\n      if (elements.alarmSet) {\n        elements.alarmSet.destroy();\n        this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);\n      }\n\n      this.seriesElements.delete(toRaw(series));\n\n      this.clearLimitLines(series);\n    },\n    lineForSeries(series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const yAxisId = series.get('yAxisId') || mainYAxisId;\n      let offset = this.offset[yAxisId];\n\n      if (series.get('interpolate') === 'linear') {\n        return new MCTChartLineLinear(series, this, offset);\n      }\n\n      if (series.get('interpolate') === 'stepAfter') {\n        return new MCTChartLineStepAfter(series, this, offset);\n      }\n    },\n    limitLineForSeries(series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const yAxisId = series.get('yAxisId') || mainYAxisId;\n      let offset = this.offset[yAxisId];\n\n      return new MCTChartAlarmLineSet(series, this, offset, this.openmct.time.getBounds());\n    },\n    pointSetForSeries(series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const yAxisId = series.get('yAxisId') || mainYAxisId;\n      let offset = this.offset[yAxisId];\n\n      if (series.get('markers')) {\n        return new MCTChartPointSet(series, this, offset);\n      }\n    },\n    alarmPointSetForSeries(series) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      const yAxisId = series.get('yAxisId') || mainYAxisId;\n      let offset = this.offset[yAxisId];\n\n      if (series.get('alarmMarkers')) {\n        return new MCTChartAlarmPointSet(series, this, offset);\n      }\n    },\n    makeChartElement(series) {\n      const elements = {\n        lines: [],\n        pointSets: [],\n        limitLines: []\n      };\n\n      const line = this.lineForSeries(series);\n      if (line) {\n        elements.lines.push(line);\n        this.lines.push(line);\n      }\n\n      const pointSet = this.pointSetForSeries(series);\n      if (pointSet) {\n        elements.pointSets.push(pointSet);\n        this.pointSets.push(pointSet);\n      }\n\n      elements.alarmSet = this.alarmPointSetForSeries(series);\n      if (elements.alarmSet) {\n        this.alarmSets.push(elements.alarmSet);\n      }\n\n      this.seriesElements.set(toRaw(series), elements);\n    },\n    makeLimitLines(series) {\n      this.clearLimitLines(series);\n\n      if (!series || !series.get('limitLines')) {\n        return;\n      }\n\n      const limitElements = {\n        limitLines: []\n      };\n\n      const limitLine = this.limitLineForSeries(series);\n      if (limitLine) {\n        limitElements.limitLines.push(limitLine);\n        this.limitLines.push(limitLine);\n      }\n\n      this.seriesLimits.set(toRaw(series), limitElements);\n    },\n    clearLimitLines(series) {\n      const seriesLimits = this.seriesLimits.get(toRaw(series));\n\n      if (seriesLimits) {\n        seriesLimits.limitLines.forEach(function (line) {\n          this.limitLines.splice(this.limitLines.indexOf(line), 1);\n          line.destroy();\n        }, this);\n\n        this.seriesLimits.delete(toRaw(series));\n      }\n    },\n    canDraw(yAxisId) {\n      if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {\n        return false;\n      }\n\n      return true;\n    },\n    redrawIfNotAlreadyHandled(attribute, value, oldValue) {\n      if (Object.keys(HANDLED_ATTRIBUTES).includes(attribute) && oldValue) {\n        return;\n      }\n\n      if (Object.keys(IMPLICIT_HANDLED_ATTRIBUTES).includes(attribute) && oldValue) {\n        return;\n      }\n\n      if (Object.keys(NO_HANDLING_NEEDED_ATTRIBUTES).includes(attribute) && oldValue) {\n        return;\n      }\n\n      this.scheduleDraw(true);\n    },\n    scheduleDraw(updateLimitLines) {\n      if (!this.drawScheduled) {\n        const called = this.renderWhenVisible(this.draw.bind(this, updateLimitLines));\n        this.drawScheduled = called;\n        if (!this.drawnOnce && called) {\n          this.drawnOnce = true;\n          this.visibilityObserver.observe(this.chartContainer);\n        }\n      }\n    },\n    draw(updateLimitLines) {\n      this.drawScheduled = false;\n      if (this.isDestroyed || !this.chartVisible) {\n        return;\n      }\n\n      this.drawAPI.clear();\n      const mainYAxisId = this.config.yAxis.get('id');\n      //There has to be at least one yAxis\n      const yAxisIds = [mainYAxisId].concat(\n        this.config.additionalYAxes.map((yAxis) => yAxis.get('id'))\n      );\n\n      // Repeat drawing for all yAxes\n      yAxisIds.filter(this.canDraw).forEach((id, yAxisIndex) => {\n        this.updateViewport(id);\n        this.drawSeries(id);\n        if (yAxisIndex === 0) {\n          this.drawRectangles(id);\n        }\n\n        this.drawHighlights(id);\n        // only draw these in fixed time mode or plot is paused\n        if (this.annotationViewingAndEditingAllowed) {\n          this.prepareToDrawAnnotatedPoints(id);\n          this.prepareToDrawAnnotationSelections(id);\n        }\n      });\n      // We must do the limit line drawing after the drawAPI has been cleared (which sets the height and width of the draw API)\n      // and the viewport is updated so that we have the right height/width for limit line x and y calculations\n      if (updateLimitLines) {\n        this.updateLimitLines();\n      }\n    },\n    updateViewport(yAxisId) {\n      if (!this.chartVisible) {\n        return;\n      }\n      const mainYAxisId = this.config.yAxis.get('id');\n      const xRange = this.config.xAxis.get('displayRange');\n      let yRange;\n      if (yAxisId === mainYAxisId) {\n        yRange = this.config.yAxis.get('displayRange');\n      } else {\n        if (this.config.additionalYAxes.length) {\n          const yAxisForId = this.config.additionalYAxes.find(\n            (yAxis) => yAxis.get('id') === yAxisId\n          );\n          yRange = yAxisForId.get('displayRange');\n        }\n      }\n\n      if (!xRange || !yRange) {\n        return;\n      }\n\n      const dimensions = [xRange.max - xRange.min, yRange.max - yRange.min];\n\n      let origin;\n      origin = [this.offset[yAxisId].x(xRange.min), this.offset[yAxisId].y(yRange.min)];\n\n      this.drawAPI.setDimensions(dimensions, origin);\n    },\n    // match items by their yAxisId, but don't care if the series is hidden or not.\n    matchByYAxisIdExcludingVisibility() {\n      const args = Array.from(arguments).slice(0, 4);\n\n      return this.matchByYAxisId(...args, true);\n    },\n    matchByYAxisId(id, item, index, items, excludeVisibility = false) {\n      const mainYAxisId = this.config.yAxis.get('id');\n      let matchesId = false;\n      const axisSeriesAreVisible = excludeVisibility || this.hiddenYAxisIds.indexOf(id) < 0;\n      const series = item.series;\n      if (axisSeriesAreVisible && series) {\n        const seriesYAxisId = series.get('yAxisId') || mainYAxisId;\n        matchesId = seriesYAxisId === id;\n      }\n\n      return matchesId;\n    },\n    drawSeries(id) {\n      const lines = this.lines.filter(this.matchByYAxisId.bind(this, id));\n      lines.forEach(this.drawLine, this);\n      const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id));\n      pointSets.forEach(this.drawPoints, this);\n      const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));\n      alarmSets.forEach(this.drawAlarmPoints, this);\n    },\n    updateLimitLines() {\n      //reset\n      this.visibleLimitLabels = [];\n      this.visibleLimitLines = [];\n\n      this.config.series.models.forEach((series) => {\n        const yAxisId = series.get('yAxisId');\n\n        if (this.hiddenYAxisIds.indexOf(yAxisId) < 0) {\n          this.updateLimitLinesForSeries(yAxisId, series);\n        }\n      });\n    },\n    updateLimitLinesForSeries(yAxisId, series) {\n      if (!this.canDraw(yAxisId)) {\n        return;\n      }\n\n      this.updateViewport(yAxisId);\n\n      if (!this.drawAPI.origin) {\n        return;\n      }\n      let limitPointOverlap = [];\n\n      this.limitLines.forEach((limitLine) => {\n        limitLine.limits.forEach((limit) => {\n          if (series.keyString !== limit.seriesKey) {\n            return;\n          }\n\n          const showLabels = this.showLabels(limit.seriesKey);\n          if (showLabels) {\n            const overlap = this.getLimitOverlap(limit, limitPointOverlap);\n            limitPointOverlap.push(overlap);\n            this.visibleLimitLabels.push(this.getLimitProps(limit, overlap));\n          }\n\n          this.visibleLimitLines.push(this.getLimitElementProps(limit));\n        }, this);\n      });\n    },\n    showLabels(seriesKey) {\n      return this.showLimitLineLabels?.seriesKey === seriesKey;\n    },\n    getLimitElementProps(limit) {\n      let point = {\n        left: 0,\n        top: this.drawAPI.y(limit.point.y)\n      };\n\n      return {\n        point,\n        limit\n      };\n    },\n    getLimitElement(limit) {\n      let point = {\n        left: 0,\n        top: this.drawAPI.y(limit.point.y)\n      };\n      const { vNode, destroy } = mount(LimitLine, {\n        props: {\n          point,\n          limit\n        }\n      });\n\n      return {\n        el: vNode.el,\n        destroy\n      };\n    },\n    getLimitOverlap(limit, overlapMap) {\n      //calculate if limit lines are too close to each other\n      let limitTop = this.drawAPI.y(limit.point.y);\n      const needsVerticalAdjustment = limitTop - CLEARANCE <= 0;\n      let needsHorizontalAdjustment = false;\n      overlapMap.forEach((value) => {\n        let diffTop;\n        if (limitTop > value.overlapTop) {\n          diffTop = limitTop - value.overlapTop;\n        } else {\n          diffTop = value.overlapTop - limitTop;\n        }\n\n        //need to compare +ves to +ves and -ves to -ves\n        if (\n          !needsHorizontalAdjustment &&\n          Math.abs(diffTop) <= CLEARANCE &&\n          value.needsHorizontalAdjustment !== true\n        ) {\n          needsHorizontalAdjustment = true;\n        }\n      });\n\n      return {\n        needsHorizontalAdjustment,\n        needsVerticalAdjustment,\n        overlapTop: limitTop\n      };\n    },\n    getLimitProps(limit, overlap) {\n      let point = {\n        left: 0,\n        top: this.drawAPI.y(limit.point.y)\n      };\n      return {\n        limit: Object.assign({}, overlap, limit),\n        point\n      };\n    },\n    getLimitLabel(limit, overlap) {\n      let point = {\n        left: 0,\n        top: this.drawAPI.y(limit.point.y)\n      };\n      const { vNode, destroy } = mount(LimitLabel, {\n        props: {\n          limit: Object.assign({}, overlap, limit),\n          point\n        }\n      });\n\n      return {\n        el: vNode.el,\n        destroy\n      };\n    },\n    drawAlarmPoints(alarmSet) {\n      this.drawAPI.drawLimitPoints(\n        alarmSet.points,\n        alarmSet.series.get('color').asRGBAArray(),\n        alarmSet.series.get('markerSize')\n      );\n    },\n    drawPoints(chartElement) {\n      this.drawAPI.drawPoints(\n        chartElement.getBuffer(),\n        chartElement.color().asRGBAArray(),\n        chartElement.count,\n        chartElement.series.get('markerSize'),\n        chartElement.series.get('markerShape')\n      );\n    },\n    drawLine(chartElement, disconnected) {\n      if (chartElement) {\n        this.drawAPI.drawLine(\n          chartElement.getBuffer(),\n          chartElement.color().asRGBAArray(),\n          chartElement.count,\n          disconnected\n        );\n      }\n    },\n    prepareToDrawAnnotatedPoints(yAxisId) {\n      if (this.annotatedPointsBySeries && Object.values(this.annotatedPointsBySeries).length) {\n        const uniquePointsToDraw = new Set();\n\n        Object.keys(this.annotatedPointsBySeries).forEach((seriesKeyString) => {\n          const seriesModel = this.getSeries(seriesKeyString);\n          const matchesYAxis = this.matchByYAxisId(yAxisId, { series: seriesModel });\n          if (!matchesYAxis) {\n            return;\n          }\n          // annotation points are all within range (checked in MctPlot with FlatBush), so we don't need to check\n          const annotatedPointBuffer = new Float32Array(\n            this.annotatedPointsBySeries[seriesKeyString].length * 2\n          );\n          Object.values(this.annotatedPointsBySeries[seriesKeyString]).forEach(\n            (annotatedPoint, index) => {\n              const canvasXValue = this.offset[yAxisId].xVal(annotatedPoint.point, seriesModel);\n              const canvasYValue = this.offset[yAxisId].yVal(annotatedPoint.point, seriesModel);\n              const drawnPointKey = `${canvasXValue}|${canvasYValue}`;\n              if (!uniquePointsToDraw.has(drawnPointKey)) {\n                annotatedPointBuffer[index * 2] = canvasXValue;\n                annotatedPointBuffer[index * 2 + 1] = canvasYValue;\n                uniquePointsToDraw.add(drawnPointKey);\n              }\n            }\n          );\n          this.drawAnnotatedPoints(seriesModel, annotatedPointBuffer);\n        });\n      }\n    },\n    drawAnnotatedPoints(seriesModel, annotatedPointBuffer) {\n      if (annotatedPointBuffer && seriesModel) {\n        const color = seriesModel.get('color').asRGBAArray();\n        // set transparency\n        color[3] = 0.15;\n        const pointCount = annotatedPointBuffer.length / 2;\n        const shape = seriesModel.get('markerShape');\n\n        this.drawAPI.drawPoints(annotatedPointBuffer, color, pointCount, ANNOTATION_SIZE, shape);\n      }\n    },\n    prepareToDrawAnnotationSelections(yAxisId) {\n      if (\n        this.annotationSelectionsBySeries &&\n        Object.keys(this.annotationSelectionsBySeries).length\n      ) {\n        Object.keys(this.annotationSelectionsBySeries).forEach((seriesKeyString) => {\n          const seriesModel = this.getSeries(seriesKeyString);\n          const matchesYAxis = this.matchByYAxisId(yAxisId, { series: seriesModel });\n          if (matchesYAxis) {\n            const annotationSelectionBuffer = new Float32Array(\n              this.annotationSelectionsBySeries[seriesKeyString].length * 2\n            );\n            Object.values(this.annotationSelectionsBySeries[seriesKeyString]).forEach(\n              (annotatedSelectedPoint, index) => {\n                const canvasXValue = this.offset[yAxisId].xVal(\n                  annotatedSelectedPoint.point,\n                  seriesModel\n                );\n                const canvasYValue = this.offset[yAxisId].yVal(\n                  annotatedSelectedPoint.point,\n                  seriesModel\n                );\n                annotationSelectionBuffer[index * 2] = canvasXValue;\n                annotationSelectionBuffer[index * 2 + 1] = canvasYValue;\n              }\n            );\n            this.drawAnnotationSelections(seriesModel, annotationSelectionBuffer);\n          }\n        });\n      }\n    },\n    drawAnnotationSelections(seriesModel, annotationSelectionBuffer) {\n      const color = [255, 255, 255, 1]; // white\n      const pointCount = annotationSelectionBuffer.length / 2;\n      const shape = seriesModel.get('markerShape');\n\n      this.drawAPI.drawPoints(annotationSelectionBuffer, color, pointCount, ANNOTATION_SIZE, shape);\n    },\n    drawHighlights(yAxisId) {\n      if (this.highlights && this.highlights.length) {\n        const highlights = this.highlights.filter((highlight) => {\n          const series = this.getSeries(highlight.seriesKeyString);\n          return this.matchByYAxisId.bind(yAxisId, { series });\n        });\n        highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);\n      }\n    },\n    getSeries(keyStringToFind) {\n      const foundSeries = this.seriesModels.find((series) => {\n        return series.keyString === keyStringToFind;\n      });\n      return foundSeries;\n    },\n    drawHighlight(yAxisId, highlight) {\n      const series = this.getSeries(highlight.seriesKeyString);\n      const points = new Float32Array([\n        this.offset[yAxisId].xVal(highlight.point, series),\n        this.offset[yAxisId].yVal(highlight.point, series)\n      ]);\n\n      const color = series.get('color').asRGBAArray();\n      const pointCount = 1;\n      const shape = series.get('markerShape');\n\n      this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);\n    },\n    drawRectangles(yAxisId) {\n      if (this.rectangles) {\n        this.rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);\n      }\n    },\n    drawRectangle(yAxisId, rect) {\n      if (!rect.start.yAxisIds || !rect.end.yAxisIds) {\n        return;\n      }\n\n      const startYIndex = rect.start.yAxisIds.findIndex((id) => id === yAxisId);\n      const endYIndex = rect.end.yAxisIds.findIndex((id) => id === yAxisId);\n      if (rect.start.y[startYIndex] && rect.end.y[endYIndex]) {\n        this.drawAPI.drawSquare(\n          [this.offset[yAxisId].x(rect.start.x), this.offset[yAxisId].y(rect.start.y[startYIndex])],\n          [this.offset[yAxisId].x(rect.end.x), this.offset[yAxisId].y(rect.end.y[endYIndex])],\n          rect.color\n        );\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/chart/limitUtil.js",
    "content": "export function getLimitClass(limit, prefix) {\n  let cssClass = '';\n  //If color exists then use it, fall back to the cssClass\n  if (limit.color) {\n    cssClass = `${cssClass} ${prefix}${limit.color}`;\n  } else if (limit.cssClass) {\n    cssClass = `${cssClass}${limit.cssClass}`;\n  }\n\n  // If we applied the cssClass then skip these classes\n  if (limit.cssClass === undefined) {\n    if (limit.isUpper) {\n      cssClass = `${cssClass} ${prefix}upr`;\n    } else {\n      cssClass = `${cssClass} ${prefix}lwr`;\n    }\n\n    if (limit.level) {\n      cssClass = `${cssClass} ${prefix}${limit.level}`;\n    }\n\n    if (limit.needsHorizontalAdjustment) {\n      cssClass = `${cssClass} --align-label-right`;\n    }\n\n    if (limit.needsVerticalAdjustment) {\n      cssClass = `${cssClass} --align-label-below`;\n    }\n  }\n\n  return cssClass;\n}\n"
  },
  {
    "path": "src/plugins/plot/configuration/Collection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Model from './Model.js';\n\n/**\n * @template {Object} T\n * @template {Object} O\n * @extends {Model<T, O>}\n */\nexport default class Collection extends Model {\n  /** @type {Constructor} */\n  modelClass = Model;\n\n  initialize(options) {\n    super.initialize(options);\n    if (options.models) {\n      this.models = options.models.map(this.modelFn, this);\n    } else {\n      this.models = [];\n    }\n  }\n\n  modelFn(model) {\n    //TODO: Come back to this - why are we doing this?\n    if (model instanceof this.modelClass) {\n      model.collection = this;\n\n      return model;\n    }\n\n    return new this.modelClass({\n      collection: this,\n      model: model\n    });\n  }\n\n  first() {\n    return this.at(0);\n  }\n\n  forEach(iteree, context) {\n    this.models.forEach(iteree, context);\n  }\n\n  map(iteree, context) {\n    return this.models.map(iteree, context);\n  }\n\n  filter(iteree, context) {\n    return this.models.filter(iteree, context);\n  }\n\n  size() {\n    return this.models.length;\n  }\n\n  at(index) {\n    return this.models[index];\n  }\n\n  add(model) {\n    model = this.modelFn(model);\n    const index = this.models.length;\n    this.models.push(model);\n    this.emit('add', model, index);\n  }\n\n  insert(model, index) {\n    model = this.modelFn(model);\n    this.models.splice(index, 0, model);\n    this.emit('add', model, index + 1);\n  }\n\n  indexOf(model) {\n    return this.models.findIndex((m) => m === model);\n  }\n\n  remove(model) {\n    const index = this.indexOf(model);\n\n    if (index === -1) {\n      throw new Error('model not found in collection.');\n    }\n\n    this.models.splice(index, 1);\n    this.emit('remove', model, index);\n  }\n\n  destroy(model) {\n    this.forEach(function (m) {\n      m.destroy();\n    });\n    this.stopListening();\n  }\n}\n\n/** @typedef {any} TODO */\n\n/** @typedef {new (...args: any[]) => object} Constructor */\n"
  },
  {
    "path": "src/plugins/plot/configuration/ConfigStore.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nclass ConfigStore {\n  /** @type {Record<string, Destroyable>} */\n  store = {};\n\n  /**\n    @param {string} id\n    */\n  deleteStore(id) {\n    const obj = this.store[id];\n\n    if (obj) {\n      if (obj.destroy) {\n        obj.destroy();\n      }\n\n      delete this.store[id];\n    }\n  }\n\n  deleteAll() {\n    Object.keys(this.store).forEach((id) => this.deleteStore(id));\n  }\n\n  /**\n    @param {string} id\n    @param {any} config\n    */\n  add(id, config) {\n    this.store[id] = config;\n  }\n\n  /**\n    @param {string} id\n    */\n  get(id) {\n    return this.store[id];\n  }\n}\n\nconst STORE = new ConfigStore();\n\nexport default STORE;\n\n/** @typedef {{destroy?(): void}} Destroyable */\n"
  },
  {
    "path": "src/plugins/plot/configuration/LegendModel.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Model from './Model.js';\n/**\n * TODO: doc strings.\n */\nexport default class LegendModel extends Model {\n  listenToSeriesCollection(seriesCollection) {\n    this.seriesCollection = seriesCollection;\n    this.listenTo(this.seriesCollection, 'add', this.setHeight, this);\n    this.listenTo(this.seriesCollection, 'remove', this.setHeight, this);\n    this.listenTo(this, 'change:expanded', this.setHeight, this);\n    this.set('expanded', this.get('expandByDefault'));\n  }\n\n  setHeight() {\n    const expanded = this.get('expanded');\n    if (this.get('position') !== 'top') {\n      this.set('height', '0px');\n    } else {\n      this.set('height', expanded ? 20 * (this.seriesCollection.size() + 1) + 40 + 'px' : '21px');\n    }\n  }\n\n  /**\n   * @override\n   */\n  defaultModel(options) {\n    return {\n      position: 'top',\n      expandByDefault: false,\n      hideLegendWhenSmall: false,\n      valueToShowWhenCollapsed: 'nearestValue',\n      showTimestampWhenExpanded: true,\n      showValueWhenExpanded: true,\n      showMaximumWhenExpanded: true,\n      showMinimumWhenExpanded: true,\n      showUnitsWhenExpanded: true,\n      showLegendsForChildren: true\n    };\n  }\n\n  destroy() {\n    this.stopListening();\n  }\n}\n"
  },
  {
    "path": "src/plugins/plot/configuration/Model.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport eventHelpers from '../lib/eventHelpers.js';\n\n/**\n * @template {Object} T\n * @template {Object} O\n */\nexport default class Model extends EventEmitter {\n  /**\n   * @param {ModelOptions<T, O>} options\n   */\n  constructor(options) {\n    super();\n    Object.defineProperty(this, '_events', {\n      value: this._events,\n      enumerable: false,\n      configurable: false,\n      writable: true\n    });\n\n    //need to do this as we're already extending EventEmitter\n    eventHelpers.extend(this);\n\n    if (!options) {\n      options = {};\n    }\n\n    // FIXME: this.id is defined as a method further below, but here it is\n    // assigned a possibly-undefined value. Is this code unused?\n    this.id = options.id;\n\n    /** @type {ModelType<T>} */\n    this.model = options.model;\n    this.collection = options.collection;\n    const defaults = this.defaultModel(options);\n    if (!this.model) {\n      this.model = options.model = defaults;\n    } else {\n      _.defaultsDeep(this.model, defaults);\n    }\n\n    this.initialize(options);\n\n    /** @type {keyof ModelType<T> } */\n    this.idAttr = 'id';\n  }\n\n  /**\n   * @param {ModelOptions<T, O>} options\n   * @returns {ModelType<T>}\n   */\n  defaultModel(options) {\n    return {};\n  }\n\n  /**\n   * @abstract\n   * @param {ModelOptions<T, O>} options\n   */\n  initialize(options) {}\n\n  /**\n   * Destroy the model, removing all listeners and subscriptions.\n   */\n  destroy() {\n    this.emit('destroy');\n    this.removeAllListeners();\n  }\n\n  id() {\n    return this.get(this.idAttr);\n  }\n\n  /**\n   * @template {keyof ModelType<T>} K\n   * @param {K} attribute\n   * @returns {ModelType<T>[K]}\n   */\n  get(attribute) {\n    return this.model[attribute];\n  }\n\n  /**\n   * @template {keyof ModelType<T>} K\n   * @param {K} attribute\n   * @returns boolean\n   */\n  has(attribute) {\n    return _.has(this.model, attribute);\n  }\n\n  /**\n   * @template {keyof ModelType<T>} K\n   * @param {K} attribute\n   * @param {ModelType<T>[K]} value\n   */\n  set(attribute, value) {\n    const oldValue = this.model[attribute];\n    this.model[attribute] = value;\n    this.emit('change', attribute, value, oldValue, this);\n    this.emit('change:' + attribute, value, oldValue, this);\n  }\n\n  /**\n   * @template {keyof ModelType<T>} K\n   * @param {K} attribute\n   */\n  unset(attribute) {\n    const oldValue = this.model[attribute];\n    delete this.model[attribute];\n    this.emit('change', attribute, undefined, oldValue, this);\n    this.emit('change:' + attribute, undefined, oldValue, this);\n  }\n}\n\n/** @typedef {any} TODO */\n\n/**\n * @typedef {import('../../../../openmct.js').OpenMCT} OpenMCT\n */\n\n/**\n@template {Object} T\n@typedef {{\n    id?: string\n} & T} ModelType\n*/\n\n/**\n@template {Object} T\n@template {Object} O\n@typedef {{\n    model?: ModelType<T>\n    models?: T[]\n    openmct: OpenMCT\n    id?: string\n    [k: string]: unknown\n} & O} ModelOptions\n*/\n"
  },
  {
    "path": "src/plugins/plot/configuration/PlotConfigurationModel.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport _ from 'lodash';\n\nimport LegendModel from './LegendModel.js';\nimport Model from './Model.js';\nimport SeriesCollection from './SeriesCollection.js';\nimport XAxisModel from './XAxisModel.js';\nimport YAxisModel from './YAxisModel.js';\n\nconst MAX_Y_AXES = 3;\nconst MAIN_Y_AXES_ID = 1;\nconst MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;\n\n/**\n * PlotConfiguration model stores the configuration of a plot and some\n * limited state.  The individual parts of the plot configuration model\n * handle setting defaults and updating in response to various changes.\n *\n * @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}\n */\nexport default class PlotConfigurationModel extends Model {\n  /**\n   * Initializes all sub models and then passes references to submodels\n   * to those that need it.\n   *\n   * @override\n   * @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options\n   */\n  initialize(options) {\n    this.openmct = options.openmct;\n\n    // This is a type assertion for TypeScript, this error is never thrown in practice.\n    if (!options.model) {\n      throw new Error('Not a collection model.');\n    }\n\n    this.xAxis = new XAxisModel({\n      model: options.model.xAxis,\n      plot: this,\n      openmct: options.openmct\n    });\n    this.yAxis = new YAxisModel({\n      model: options.model.yAxis,\n      plot: this,\n      openmct: options.openmct,\n      id: options.model.yAxis.id || MAIN_Y_AXES_ID\n    });\n    //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis\n    //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES\n    this.additionalYAxes = [];\n    const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes);\n\n    for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) {\n      const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1;\n      const yAxis =\n        hasAdditionalAxesConfiguration &&\n        options.model.additionalYAxes.find((additionalYAxis) => additionalYAxis?.id === yAxisId);\n      if (yAxis) {\n        this.additionalYAxes.push(\n          new YAxisModel({\n            model: yAxis,\n            plot: this,\n            openmct: options.openmct,\n            id: yAxis.id\n          })\n        );\n      } else {\n        this.additionalYAxes.push(\n          new YAxisModel({\n            plot: this,\n            openmct: options.openmct,\n            id: yAxisId\n          })\n        );\n      }\n    }\n    // end add additional axes\n\n    this.legend = new LegendModel({\n      model: options.model.legend,\n      plot: this,\n      openmct: options.openmct\n    });\n    this.series = new SeriesCollection({\n      models: options.model.series,\n      plot: this,\n      openmct: options.openmct,\n      palette: options.palette\n    });\n\n    if (this.get('domainObject').type === 'telemetry.plot.overlay') {\n      this.removeMutationListener = this.openmct.objects.observe(\n        this.get('domainObject'),\n        '*',\n        this.updateDomainObject.bind(this)\n      );\n    }\n\n    this.yAxis.listenToSeriesCollection(this.series);\n    this.additionalYAxes.forEach((yAxis) => {\n      yAxis.listenToSeriesCollection(this.series);\n    });\n    this.legend.listenToSeriesCollection(this.series);\n\n    this.listenTo(this, 'destroy', this.onDestroy, this);\n  }\n  /**\n   * Retrieve the persisted series config for a given identifier.\n   * @param {import('openmct').Identifier} identifier\n   * @returns {import('./PlotSeries').PlotSeriesModelType=}\n   */\n  getPersistedSeriesConfig(identifier) {\n    const domainObject = this.get('domainObject');\n    if (!domainObject.configuration || !domainObject.configuration.series) {\n      return;\n    }\n\n    return domainObject.configuration.series.filter(function (seriesConfig) {\n      return (\n        seriesConfig.identifier.key === identifier.key &&\n        seriesConfig.identifier.namespace === identifier.namespace\n      );\n    })[0];\n  }\n  /**\n   * Retrieve the persisted filters for a given identifier.\n   */\n  getPersistedFilters(identifier) {\n    const domainObject = this.get('domainObject');\n    const keystring = this.openmct.objects.makeKeyString(identifier);\n\n    if (!domainObject.configuration || !domainObject.configuration.filters) {\n      return;\n    }\n\n    return domainObject.configuration.filters[keystring];\n  }\n  /**\n   * Update the domain object with the given value.\n   */\n  updateDomainObject(domainObject) {\n    this.set('domainObject', domainObject);\n  }\n\n  /**\n   * Clean up all objects and remove all listeners.\n   */\n  onDestroy() {\n    this.xAxis.destroy();\n    this.yAxis.destroy();\n    this.additionalYAxes.forEach((additionalYAxis) => additionalYAxis.destroy());\n    this.series.destroy();\n    this.legend.destroy();\n    this.stopListening();\n    if (this.removeMutationListener) {\n      this.removeMutationListener();\n    }\n  }\n  /**\n   * Return defaults, which are extracted from the passed in domain\n   * object.\n   * @override\n   * @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options\n   */\n  defaultModel(options) {\n    return {\n      series: [],\n      domainObject: options.domainObject,\n      xAxis: {},\n      yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),\n      additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []),\n      legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})\n    };\n  }\n}\n\n/** @typedef {any} TODO */\n\n/** @typedef {import('./PlotSeries').default} PlotSeries */\n\n/**\n@typedef {{\n    configuration: {\n        series: import('./PlotSeries').PlotSeriesModelType[]\n        yAxis: import('./YAxisModel').YAxisModelType\n    },\n}} SomeDomainObject_NeedsName\n*/\n\n/**\n@typedef {{\n    xAxis: import('./XAxisModel').XAxisModelType\n    yAxis: import('./YAxisModel').YAxisModelType\n    legend: TODO\n    series: PlotSeries[]\n    domainObject: SomeDomainObject_NeedsName\n}} PlotConfigModelType\n*/\n\n/** @typedef {TODO} SomeOtherDomainObject */\n\n/**\nTODO: Is SomeOtherDomainObject the same domain object as with SomeDomainObject_NeedsName?\n@typedef {{\n    plot: import('./PlotConfigurationModel').default\n    domainObject: SomeOtherDomainObject\n}} PlotConfigModelOptions\n*/\n"
  },
  {
    "path": "src/plugins/plot/configuration/PlotSeries.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport configStore from '../configuration/ConfigStore.js';\nimport { MARKER_SHAPES } from '../draw/MarkerShapes.js';\nimport { symlog } from '../mathUtils.js';\nimport Model from './Model.js';\n\n/**\n * Plot series handle interpreting telemetry metadata for a single telemetry\n * object, querying for that data, and formatting it for display purposes.\n *\n * Plot series emit both collection events and model events:\n * `change` when any property changes\n * `change:<prop_name>` when a specific property changes.\n * `destroy`: when series is destroyed\n * `add`: whenever a data point is added to a series\n * `remove`: whenever a data point is removed from a series.\n * `reset`: whenever the collection is emptied.\n *\n * Plot series have the following Model properties:\n *\n * `name`: name of series.\n * `identifier`: the Open MCT identifier for the telemetry source for this\n *               series.\n * `xKey`: the telemetry value key for x values fetched from this series.\n * `yKey`: the telemetry value key for y values fetched from this series.\n * `interpolate`: interpolate method, either `undefined` (no interpolation),\n *                `linear` (points are connected via straight lines), or\n *                `stepAfter` (points are connected by steps).\n * `markers`: boolean, whether or not this series should render with markers.\n * `markerShape`: string, shape of markers.\n * `markerSize`: number, size in pixels of markers for this series.\n * `alarmMarkers`: whether or not to display alarm markers for this series.\n * `stats`: An object that tracks the min and max y values observed in this\n *          series.  This property is checked and updated whenever data is\n *          added.\n *\n * Plot series have the following instance properties:\n *\n * `metadata`: the Open MCT Telemetry Metadata Manager for the associated\n *             telemetry point.\n * `formats`: the Open MCT format map for this telemetry point.\n *\n * @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}\n */\n\nconst FLOAT32_MAX = 3.4e38;\nconst FLOAT32_MIN = -3.4e38;\n\nexport default class PlotSeries extends Model {\n  logMode = false;\n\n  /**\n     @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options\n     */\n  constructor(options) {\n    super(options);\n\n    this.logMode = this.getLogMode(options);\n\n    this.listenTo(this, 'change:xKey', this.onXKeyChange, this);\n    this.listenTo(this, 'change:yKey', this.onYKeyChange, this);\n    this.persistedConfig = options.persistedConfig;\n    this.filters = options.filters;\n\n    // Model.apply(this, arguments);\n    this.onXKeyChange(this.get('xKey'));\n    this.onYKeyChange(this.get('yKey'));\n\n    this.unPlottableValues = [undefined, Infinity, -Infinity];\n  }\n\n  getLogMode(options) {\n    const yAxisId = this.get('yAxisId');\n    if (yAxisId === 1) {\n      return options.collection.plot.model.yAxis.logMode;\n    } else {\n      const foundYAxis = options.collection.plot.model.additionalYAxes.find(\n        (yAxis) => yAxis.id === yAxisId\n      );\n\n      return foundYAxis ? foundYAxis.logMode : false;\n    }\n  }\n\n  /**\n   * Set defaults for telemetry series.\n   * @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options\n   * @override\n   */\n  defaultModel(options) {\n    this.metadata = options.openmct.telemetry.getMetadata(options.domainObject);\n\n    this.formats = options.openmct.telemetry.getFormatMap(this.metadata);\n\n    //if the object is missing or doesn't have metadata for some reason\n    let range = {};\n    if (this.metadata) {\n      range = this.metadata.valuesForHints(['range'])[0];\n    }\n\n    return {\n      name: options.domainObject.name,\n      unit: range.unit,\n      xKey: options.collection.plot.xAxis.get('key'),\n      yKey: range.key,\n      markers: true,\n      markerShape: 'point',\n      markerSize: 2.0,\n      alarmMarkers: true,\n      limitLines: false,\n      yAxisId: options.model.yAxisId || 1\n    };\n  }\n\n  /**\n   * Remove real-time subscription when destroyed.\n   * @override\n   */\n  destroy() {\n    //this triggers Model.destroy which in turn triggers destroy methods for other classes.\n    super.destroy();\n    this.stopListening();\n    this.openmct.time.off('boundsChanged', this.updateLimits);\n\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n\n    if (this.unsubscribeLimits) {\n      this.unsubscribeLimits();\n    }\n\n    if (this.removeMutationListener) {\n      this.removeMutationListener();\n    }\n\n    configStore.deleteStore(this.dataStoreId);\n  }\n\n  /**\n   * Set defaults for telemetry series.\n   * @override\n   * @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options\n   */\n  initialize(options) {\n    this.openmct = options.openmct;\n    this.domainObject = options.domainObject;\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`;\n    this.updateSeriesData([]);\n    this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);\n    this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);\n    this.limits = [];\n    this.openmct.time.on('boundsChanged', this.updateLimits);\n    this.removeMutationListener = this.openmct.objects.observe(\n      this.domainObject,\n      'name',\n      this.updateName.bind(this)\n    );\n  }\n\n  /**\n   * @param {Bounds} bounds\n   */\n  updateLimits(bounds) {\n    this.emit('limitBounds', bounds);\n  }\n\n  /**\n   * Fetch historical data and establish a realtime subscription.  Returns\n   * a promise that is resolved when all connections have been successfully\n   * established.\n   *\n   * @returns {Promise}\n   */\n  async fetch(options) {\n    let strategy;\n\n    if (this.model.interpolate !== 'none') {\n      strategy = 'minmax';\n    }\n\n    options = Object.assign(\n      {},\n      {\n        size: 1000,\n        strategy,\n        filters: this.filters\n      },\n      options || {}\n    );\n\n    if (!this.unsubscribe) {\n      this.unsubscribe = this.openmct.telemetry.subscribe(\n        this.domainObject,\n        (data) => {\n          // We cannot assume that the incoming data is chronologically sound, so sorted = false\n          this.addAll(_(data).sortBy(this.getXVal).value(), false);\n        },\n        {\n          filters: this.filters,\n          strategy: this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH\n        }\n      );\n    }\n\n    try {\n      const points = await this.openmct.telemetry.request(this.domainObject, options);\n      // if derived, we can't use the old data\n      let data = this.getSeriesData();\n\n      if (this.metadata.value(this.get('yKey')).derived) {\n        data = [];\n      }\n\n      // eslint-disable-next-line you-dont-need-lodash-underscore/concat\n      const newPoints = _(data)\n        .concat(points)\n        .sortBy(this.getXVal)\n        .uniq(true, (point) => [this.getXVal(point), this.getYVal(point)].join())\n        .value();\n      this.reset(newPoints);\n    } catch (error) {\n      console.warn('Error fetching data', error);\n    }\n  }\n\n  updateName(name) {\n    if (name !== this.get('name')) {\n      this.set('name', name);\n    }\n  }\n  /**\n   * Update x formatter on x change.\n   */\n  onXKeyChange(xKey) {\n    const format = this.formats[xKey];\n    if (format) {\n      this.getXVal = format.parse.bind(format);\n    }\n  }\n\n  /**\n   * Update y formatter on change, default to stepAfter interpolation if\n   * y range is an enumeration.\n   */\n  onYKeyChange(newKey, oldKey) {\n    if (newKey === oldKey) {\n      return;\n    }\n\n    const valueMetadata = this.metadata.value(newKey);\n    //TODO: Should we do this even if there is a persisted config?\n    if (!this.persistedConfig || !this.persistedConfig.interpolate) {\n      if (valueMetadata.format === 'enum') {\n        this.set('interpolate', 'stepAfter');\n      } else {\n        this.set('interpolate', 'linear');\n      }\n    }\n\n    this.evaluate = function (datum) {\n      return this.limitEvaluator.evaluate(datum, valueMetadata);\n    }.bind(this);\n    this.set('unit', valueMetadata.unit);\n    const format = this.formats[newKey];\n    this.getYVal = (value) => {\n      const y = format.parse(value);\n\n      return this.logMode ? symlog(y, 10) : y;\n    };\n  }\n\n  formatX(point) {\n    return this.formats[this.get('xKey')].format(point);\n  }\n\n  formatY(point) {\n    return this.formats[this.get('yKey')].format(point);\n  }\n\n  /**\n   * Clear stats and recalculate from existing data.\n   */\n  resetStats() {\n    this.unset('stats');\n    this.getSeriesData().forEach(this.updateStats, this);\n  }\n\n  /**\n   * Reset plot series.  If new data is provided, will add that\n   * data to series after reset.\n   */\n  reset(newData) {\n    this.updateSeriesData([]);\n    this.resetStats();\n    this.emit('reset');\n    if (newData) {\n      this.addAll(newData, true);\n    }\n  }\n  /**\n   * Return the point closest to a given x value.\n   */\n  nearestPoint(xValue) {\n    const insertIndex = this.sortedIndex(xValue);\n    const data = this.getSeriesData();\n    const lowPoint = data[insertIndex - 1];\n    const highPoint = data[insertIndex];\n    const indexVal = this.getXVal(xValue);\n    const lowDistance = lowPoint ? indexVal - this.getXVal(lowPoint) : Number.POSITIVE_INFINITY;\n    const highDistance = highPoint ? this.getXVal(highPoint) - indexVal : Number.POSITIVE_INFINITY;\n    const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint;\n\n    return nearestPoint;\n  }\n  /**\n   * Override this to implement plot series loading functionality.  Must return\n   * a promise that is resolved when loading is completed.\n   *\n   * @returns {Promise}\n   */\n  async load(options) {\n    await this.fetch(options);\n    this.emit('load');\n    await this.loadLimits();\n  }\n\n  async loadLimits() {\n    const limitsResponse = await this.limitDefinition.limits();\n    this.limits = {};\n    if (!this.unsubscribeLimits) {\n      this.unsubscribeLimits = this.openmct.telemetry.subscribeToLimits(\n        this.domainObject,\n        this.limitsUpdated.bind(this)\n      );\n    }\n    this.limitsUpdated(limitsResponse);\n  }\n\n  limitsUpdated(limitsResponse) {\n    if (limitsResponse) {\n      this.limits = limitsResponse;\n    } else {\n      this.limits = {};\n    }\n\n    this.emit('limits', this);\n    this.emit('change:limitLines', this);\n  }\n\n  /**\n   * Find the insert index for a given point to maintain sort order.\n   * @private\n   */\n  sortedIndex(point) {\n    // lodash's sortedIndexBy uses binary search, so it should be quite efficient for most cases.\n    return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);\n  }\n  /**\n   * Update min/max stats for the series.\n   * @private\n   */\n  updateStats(point) {\n    const value = this.getYVal(point);\n    let stats = this.get('stats');\n    let changed = false;\n    if (!stats) {\n      if ([Infinity, -Infinity].includes(value) || !this.isValidFloat32(value)) {\n        return;\n      }\n\n      stats = {\n        minValue: value,\n        minPoint: point,\n        maxValue: value,\n        maxPoint: point\n      };\n      changed = true;\n    } else {\n      if (stats.maxValue < value && value !== Infinity && this.isValidFloat32(value)) {\n        stats.maxValue = value;\n        stats.maxPoint = point;\n        changed = true;\n      }\n\n      if (stats.minValue > value && value !== -Infinity && this.isValidFloat32(value)) {\n        stats.minValue = value;\n        stats.minPoint = point;\n        changed = true;\n      }\n    }\n\n    if (changed) {\n      this.set('stats', {\n        minValue: stats.minValue,\n        minPoint: stats.minPoint,\n        maxValue: stats.maxValue,\n        maxPoint: stats.maxPoint\n      });\n    }\n  }\n\n  /**\n   * Add a point to the data array while maintaining the sort order of\n   * the array and preventing insertion of points with a duplicate x\n   * value. Can provide an optional argument to append a point without\n   * maintaining sort order and dupe checks, which improves performance\n   * when adding an array of points that are already properly sorted.\n   *\n   * @private\n   * @param {Object} newData a telemetry datum.\n   * @param {boolean} [sorted] default false, if true will append\n   *                  a point to the end without dupe checking.\n   */\n  add(newData, sorted = false) {\n    let data = this.getSeriesData();\n    let insertIndex = data.length;\n    const currentYVal = this.getYVal(newData);\n    const lastYVal = this.getYVal(data[insertIndex - 1]);\n    const currentXVal = this.getXVal(newData);\n\n    if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {\n      console.warn(`[Plot] Invalid Y Values detected: ${currentYVal} ${lastYVal}`);\n\n      return;\n    }\n\n    // if the first new data point has an X value > the last data point we already have,  stick it at the end.\n    const isDataInThePast = currentXVal <= this.getXVal(data[insertIndex - 1]);\n    if (!sorted && isDataInThePast) {\n      insertIndex = this.sortedIndex(newData);\n      if (this.getXVal(data[insertIndex]) === currentXVal) {\n        return;\n      }\n\n      if (this.getXVal(data[insertIndex - 1]) === currentXVal) {\n        return;\n      }\n    }\n\n    this.updateStats(newData);\n    newData.mctLimitState = this.evaluate(newData);\n    // Note: Splicing is a performance bottleneck for large data sets when the insert index\n    // is NOT at the end of the array, this is because inserting into the middle of an array requires\n    // shifting all subsequent elements. For now, leave it as is since for the\n    // majority of cases real-time data will be appended to the end of the array,\n    // and historical data will be added in sorted order.\n    data.splice(insertIndex, 0, newData);\n    this.updateSeriesData(data);\n    this.emit('add', newData, insertIndex, this);\n  }\n\n  addAll(points, sorted = false) {\n    for (let i = 0; i < points.length; i++) {\n      this.add(points[i], sorted);\n    }\n  }\n\n  /**\n   *\n   * @private\n   */\n  isValueInvalid(val) {\n    return Number.isNaN(val) || this.unPlottableValues.includes(val) || !this.isValidFloat32(val);\n  }\n\n  /**\n   *\n   * @private\n   */\n  isValidFloat32(val) {\n    return val < FLOAT32_MAX && val > FLOAT32_MIN;\n  }\n\n  /**\n   * Remove a point from the data array and notify listeners.\n   * @private\n   */\n  remove(point) {\n    let data = this.getSeriesData();\n    const index = data.indexOf(point);\n    data.splice(index, 1);\n    this.updateSeriesData(data);\n    this.emit('remove', point, index, this);\n  }\n  /**\n   * Purges records outside a given x range.  Changes removal method based\n   * on number of records to remove: for large purge, reset data and\n   * rebuild array.  for small purge, removes points and emits updates.\n   *\n   * @public\n   * @param {Object} range\n   * @param {number} range.min minimum x value to keep\n   * @param {number} range.max maximum x value to keep.\n   */\n  purgeRecordsOutsideRange(range) {\n    const startIndex = this.sortedIndex(range.min);\n    const endIndex = this.sortedIndex(range.max) + 1;\n    let data = this.getSeriesData();\n    const pointsToRemove = startIndex + (data.length - endIndex + 1);\n    if (pointsToRemove > 0) {\n      if (pointsToRemove < 1000) {\n        data.slice(0, startIndex).forEach(this.remove, this);\n        data.slice(endIndex, data.length).forEach(this.remove, this);\n        this.updateSeriesData(data);\n        this.resetStats();\n      } else {\n        const newData = this.getSeriesData().slice(startIndex, endIndex);\n        this.reset(newData);\n      }\n    }\n  }\n  /**\n   * Updates filters, clears the plot series, unsubscribes and resubscribes\n   * @public\n   */\n  updateFiltersAndRefresh(updatedFilters) {\n    if (updatedFilters === undefined) {\n      return;\n    }\n\n    let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));\n\n    if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {\n      this.filters = deepCopiedFilters;\n      this.reset();\n      if (this.unsubscribe) {\n        this.unsubscribe();\n        delete this.unsubscribe;\n      }\n\n      this.fetch();\n    } else {\n      this.filters = deepCopiedFilters;\n    }\n  }\n  getDisplayRange(xKey) {\n    const unsortedData = this.getSeriesData();\n    this.updateSeriesData([]);\n    unsortedData.forEach((point) => this.add(point, false));\n\n    let data = this.getSeriesData();\n    const minValue = this.getXVal(data[0]);\n    const maxValue = this.getXVal(data[data.length - 1]);\n\n    return {\n      min: minValue,\n      max: maxValue\n    };\n  }\n  markerOptionsDisplayText() {\n    const showMarkers = this.get('markers');\n    if (!showMarkers) {\n      return 'Disabled';\n    }\n\n    const markerShapeKey = this.get('markerShape');\n    const markerShape = MARKER_SHAPES[markerShapeKey].label;\n    const markerSize = this.get('markerSize');\n\n    return `${markerShape}: ${markerSize}px`;\n  }\n  nameWithUnit() {\n    let unit = this.get('unit');\n\n    return this.get('name') + (unit ? ' ' + unit : '');\n  }\n\n  /**\n   * Update the series data with the given value.\n   */\n  updateSeriesData(data) {\n    configStore.add(this.dataStoreId, data);\n  }\n\n  /**\n     * Update the series data with the given value.\n     * This return type definition is totally wrong, only covers sinewave generator. It needs to be generic.\n     * @return-example {Array<{\n            cos: number\n            sin: number\n            mctLimitState: {\n                cssClass: string\n                high: number\n                low: {sin: number, cos: number}\n                name: string\n            }\n            utc: number\n            wavelength: number\n            yesterday: number\n        }>}\n     */\n  getSeriesData() {\n    return configStore.get(this.dataStoreId) || [];\n  }\n}\n\n/** @typedef {any} TODO */\n\n/** @typedef {{key: string, namespace: string}} Identifier */\n\n/**\n@typedef {{\n    identifier: Identifier\n    name: string\n    unit: string\n    xKey: string\n    yKey: string\n    markers: boolean\n    markerShape: keyof typeof MARKER_SHAPES\n    markerSize: number\n    alarmMarkers: boolean\n    limitLines: boolean\n    interpolate: boolean\n    stats: TODO\n}} PlotSeriesModelType\n*/\n\n/**\n@typedef {{\n    model: PlotSeriesModelType\n    collection: import('./SeriesCollection').default\n    persistedConfig: PlotSeriesModelType\n    filters: TODO\n}} PlotSeriesModelOptions\n*/\n\n/**\n@typedef {import('@/api/time/TimeContext').Bounds} Bounds\n*/\n"
  },
  {
    "path": "src/plugins/plot/configuration/SeriesCollection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport _ from 'lodash';\n\nimport Color from '@/ui/color/Color';\nimport ColorPalette from '@/ui/color/ColorPalette';\n\nimport Collection from './Collection.js';\nimport PlotSeries from './PlotSeries.js';\n\n/**\n * @extends {Collection<SeriesCollectionModelType, SeriesCollectionOptions>}\n */\nexport default class SeriesCollection extends Collection {\n  /**\n    @override\n    @param {import('./Model').ModelOptions<SeriesCollectionModelType, SeriesCollectionOptions>} options\n    */\n  initialize(options) {\n    super.initialize(options);\n    this.modelClass = PlotSeries;\n    this.plot = options.plot;\n    this.openmct = options.openmct;\n    this.palette = options.palette || new ColorPalette();\n    this.listenTo(this, 'add', this.onSeriesAdd, this);\n    this.listenTo(this, 'remove', this.onSeriesRemove, this);\n    this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);\n\n    const domainObject = this.plot.get('domainObject');\n\n    if (this.openmct.objects.isMissing(domainObject)) {\n      return;\n    }\n\n    if (domainObject.telemetry) {\n      this.addTelemetryObject(domainObject);\n    } else {\n      this.watchTelemetryContainer(domainObject);\n    }\n  }\n  trackPersistedConfig(domainObject) {\n    domainObject.configuration.series.forEach(function (seriesConfig) {\n      const series = this.byIdentifier(seriesConfig.identifier);\n      if (series) {\n        series.persistedConfig = seriesConfig;\n        if (!series.persistedConfig.yAxisId) {\n          return;\n        }\n\n        if (series.get('yAxisId') !== series.persistedConfig.yAxisId) {\n          series.set('yAxisId', series.persistedConfig.yAxisId);\n        }\n      }\n    }, this);\n  }\n  watchTelemetryContainer(domainObject) {\n    if (domainObject.type === 'telemetry.plot.stacked') {\n      return;\n    }\n\n    const composition = this.openmct.composition.get(domainObject);\n    this.listenTo(composition, 'add', this.addTelemetryObject, this);\n    this.listenTo(composition, 'remove', this.removeTelemetryObject, this);\n    composition.load();\n  }\n  addTelemetryObject(domainObject, index) {\n    let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier);\n    const filters = this.plot.getPersistedFilters(domainObject.identifier);\n    const plotObject = this.plot.get('domainObject');\n\n    if (!seriesConfig) {\n      seriesConfig = {\n        identifier: domainObject.identifier\n      };\n\n      if (plotObject.type === 'telemetry.plot.overlay') {\n        this.openmct.objects.mutate(\n          plotObject,\n          'configuration.series[' + this.size() + ']',\n          seriesConfig\n        );\n        seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier);\n      }\n    }\n\n    // Clone to prevent accidental mutation by ref.\n    seriesConfig = JSON.parse(JSON.stringify(seriesConfig));\n\n    if (!seriesConfig) {\n      throw 'not possible';\n    }\n\n    this.add(\n      new PlotSeries({\n        model: seriesConfig,\n        domainObject: domainObject,\n        openmct: this.openmct,\n        collection: this,\n        persistedConfig: this.plot.getPersistedSeriesConfig(domainObject.identifier),\n        filters: filters\n      })\n    );\n  }\n  removeTelemetryObject(identifier) {\n    const plotObject = this.plot.get('domainObject');\n    if (plotObject.type === 'telemetry.plot.overlay') {\n      const persistedIndex = plotObject.configuration.series.findIndex((s) => {\n        return _.isEqual(identifier, s.identifier);\n      });\n\n      const configIndex = this.models.findIndex((m) => {\n        return _.isEqual(m.domainObject.identifier, identifier);\n      });\n\n      /*\n      when cancelling out of edit mode, the config store and domain object are out of sync\n      thus it is necessary to check both and remove the models that are no longer in composition\n      */\n      if (persistedIndex === -1) {\n        this.remove(this.at(configIndex));\n      } else {\n        this.remove(this.at(persistedIndex));\n        // Because this is triggered by a composition change, we have\n        // to defer mutation of our plot object, otherwise we might\n        // mutate an outdated version of the plotObject.\n        setTimeout(\n          function () {\n            const newPlotObject = this.plot.get('domainObject');\n            const cSeries = newPlotObject.configuration.series.slice();\n            cSeries.splice(persistedIndex, 1);\n            this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries);\n          }.bind(this)\n        );\n      }\n    }\n  }\n  onSeriesAdd(series) {\n    let seriesColor = series.get('color');\n    if (seriesColor) {\n      if (!(seriesColor instanceof Color)) {\n        seriesColor = Color.fromHexString(seriesColor);\n        series.set('color', seriesColor);\n      }\n\n      this.palette.remove(seriesColor);\n    } else {\n      series.set('color', this.palette.getNextColor());\n    }\n\n    this.listenTo(series, 'change:color', this.updateColorPalette, this);\n  }\n  onSeriesRemove(series) {\n    this.palette.return(series.get('color'));\n    this.stopListening(series);\n    series.destroy();\n  }\n  updateColorPalette(newColor, oldColor) {\n    this.palette.remove(newColor);\n    const seriesWithColor = this.filter(function (series) {\n      return series.get('color') === newColor;\n    })[0];\n    if (!seriesWithColor) {\n      this.palette.return(oldColor);\n    }\n  }\n  byIdentifier(identifier) {\n    return this.filter(function (series) {\n      const seriesIdentifier = series.get('identifier');\n\n      return (\n        seriesIdentifier.namespace === identifier.namespace &&\n        seriesIdentifier.key === identifier.key\n      );\n    })[0];\n  }\n\n  destroy() {\n    super.destroy();\n    this.plot = undefined;\n    this.stopListening();\n  }\n}\n\n/**\n@typedef {PlotSeries} SeriesCollectionModelType\n*/\n\n/**\n@typedef {{\n    plot: import('./PlotConfigurationModel').default\n}} SeriesCollectionOptions\n*/\n"
  },
  {
    "path": "src/plugins/plot/configuration/XAxisModel.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Model from './Model.js';\n\n/**\n * @extends {Model<XAxisModelType, XAxisModelOptions>}\n */\nexport default class XAxisModel extends Model {\n  // Despite providing template types to the Model class, we still need to\n  // re-define the type of the following initialize() method's options arg. Tracking\n  // issue for this: https://github.com/microsoft/TypeScript/issues/32082\n  // When they fix it, we can remove the `@param` we have here.\n  /**\n   * @override\n   * @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options\n   */\n  initialize(options) {\n    this.plot = options.plot;\n\n    // This is a type assertion for TypeScript, this error is not thrown in practice.\n    if (!options.model) {\n      throw new Error('Not a collection model.');\n    }\n\n    this.set('label', options.model.name || '');\n\n    this.on('change:range', (newValue) => {\n      if (!this.get('frozen')) {\n        this.set('displayRange', newValue);\n      }\n    });\n\n    this.on('change:frozen', (frozen) => {\n      if (!frozen) {\n        this.set('range', this.get('range'));\n      }\n    });\n\n    if (this.get('range')) {\n      this.set('range', this.get('range'));\n    }\n\n    this.listenTo(this, 'change:key', this.changeKey, this);\n  }\n\n  /**\n   * @param {string} newKey\n   */\n  changeKey(newKey) {\n    const series = this.plot.series.first();\n    if (series) {\n      const xMetadata = series.metadata.value(newKey);\n      const xFormat = series.formats[newKey];\n      this.set('label', xMetadata.name);\n      this.set('format', xFormat.format.bind(xFormat));\n    } else {\n      this.set('format', function (x) {\n        return x;\n      });\n      this.set('label', newKey);\n    }\n\n    this.plot.series.forEach(function (plotSeries) {\n      plotSeries.set('xKey', newKey);\n    });\n  }\n  resetSeries() {\n    this.plot.series.forEach(function (plotSeries) {\n      plotSeries.reset();\n    });\n  }\n  /**\n   * @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options\n   * @override\n   */\n  defaultModel(options) {\n    const bounds = options.openmct.time.getBounds();\n    const timeSystem = options.openmct.time.getTimeSystem();\n    const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);\n\n    /** @type {XAxisModelType} */\n    const defaultModel = {\n      name: timeSystem.name,\n      key: timeSystem.key,\n      format: format.format.bind(format),\n      range: {\n        min: bounds.start,\n        max: bounds.end\n      },\n      frozen: false\n    };\n\n    return defaultModel;\n  }\n\n  destroy() {\n    this.plot = undefined;\n    this.stopListening();\n  }\n}\n\n/** @typedef {any} TODO */\n\n/** @typedef {TODO} OpenMCT */\n\n/**\n@typedef {{\n    min: number\n    max: number\n}} NumberRange\n*/\n\n/**\n@typedef {import(\"./Model\").ModelType<{\n    range?: NumberRange\n    displayRange: NumberRange\n    frozen: boolean\n    label: string\n    format: (n: number) => string\n    values: Array<TODO>\n}>} AxisModelType\n*/\n\n/**\n@typedef {AxisModelType & {\n    name: string\n    key: string\n}} XAxisModelType\n*/\n\n/**\n@typedef {{\n    plot: import('./PlotConfigurationModel').default\n}} XAxisModelOptions\n*/\n"
  },
  {
    "path": "src/plugins/plot/configuration/YAxisModel.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { antisymlog, symlog } from '../mathUtils.js';\nimport Model from './Model.js';\n\n/**\n * YAxis model\n *\n * TODO: docstrings.\n *\n * has the following Model properties:\n *\n * `autoscale`: boolean, whether or not to autoscale.\n * `autoscalePadding`: float, percent of padding to display in plots.\n * `displayRange`: the current display range for the axis.\n * `format`: the formatter for the axis.\n * `frozen`: boolean, if true, displayRange will not be updated automatically.\n *           Used to temporarily disable automatic updates during user interaction.\n * `label`: label to display on axis.\n * `stats`: Min and Max Values of data, automatically updated by observing\n *          plot series.\n * `values`: for enumerated types, an array of possible display values.\n * `range`: the user-configured range to use for display, when autoscale is\n *         disabled.\n *\n * @extends {Model<YAxisModelType, YAxisModelOptions>}\n */\nexport default class YAxisModel extends Model {\n  /**\n   * @override\n   * @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options\n   */\n  initialize(options) {\n    this.plot = options.plot;\n    this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);\n    this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);\n    this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);\n    this.listenTo(this, 'change:logMode', this.onLogModeChange, this);\n    this.listenTo(this, 'change:frozen', this.toggleFreeze, this);\n    this.listenTo(this, 'change:range', this.updateDisplayRange, this);\n    const range = this.get('range');\n    this.updateDisplayRange(range);\n    //This is an edge case and should not happen\n    const invalidRange = !range || range?.min === undefined || range?.max === undefined;\n    const invalidAutoScaleOff = options.model.autoscale === false && invalidRange;\n    if (invalidAutoScaleOff) {\n      this.set('autoscale', true);\n    }\n  }\n  /**\n   * @param {import('./SeriesCollection').default} seriesCollection\n   */\n  listenToSeriesCollection(seriesCollection) {\n    this.seriesCollection = seriesCollection;\n    this.listenTo(\n      this.seriesCollection,\n      'add',\n      (series) => {\n        this.trackSeries(series);\n        this.updateFromSeries(this.seriesCollection);\n      },\n      this\n    );\n    this.listenTo(\n      this.seriesCollection,\n      'remove',\n      (series) => {\n        this.untrackSeries(series);\n        this.updateFromSeries(this.seriesCollection);\n      },\n      this\n    );\n    this.seriesCollection.forEach(this.trackSeries, this);\n    this.updateFromSeries(this.seriesCollection);\n  }\n  toggleFreeze(frozen) {\n    if (!frozen) {\n      this.toggleAutoscale(this.get('autoscale'));\n    }\n  }\n  applyPadding(range) {\n    let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding');\n    if (padding === 0) {\n      padding = 1;\n    }\n\n    return {\n      min: range.min - padding,\n      max: range.max + padding\n    };\n  }\n  updatePadding(newPadding) {\n    if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) {\n      this.set('displayRange', this.applyPadding(this.get('stats')));\n    }\n  }\n  calculateAutoscaleExtents(newStats) {\n    if (this.get('autoscale') && !this.get('frozen')) {\n      if (!newStats) {\n        this.unset('displayRange');\n      } else {\n        this.set('displayRange', this.applyPadding(newStats));\n      }\n    }\n  }\n  updateStats(seriesStats) {\n    if (!this.has('stats')) {\n      this.set('stats', {\n        min: seriesStats.minValue,\n        max: seriesStats.maxValue\n      });\n\n      return;\n    }\n\n    const stats = this.get('stats');\n    let changed = false;\n    if (stats.min > seriesStats.minValue) {\n      changed = true;\n      stats.min = seriesStats.minValue;\n    }\n\n    if (stats.max < seriesStats.maxValue) {\n      changed = true;\n      stats.max = seriesStats.maxValue;\n    }\n\n    if (changed) {\n      this.set('stats', {\n        min: stats.min,\n        max: stats.max\n      });\n    }\n  }\n  resetStats() {\n    //TODO: do we need the series id here?\n    this.unset('stats');\n    this.getSeriesForYAxis(this.seriesCollection).forEach((series) => {\n      if (series.has('stats')) {\n        this.updateStats(series.get('stats'));\n      }\n    });\n  }\n  getSeriesForYAxis(seriesCollection) {\n    return seriesCollection.filter((series) => {\n      const seriesYAxisId = series.get('yAxisId') || 1;\n\n      return seriesYAxisId === this.id;\n    });\n  }\n\n  getYAxisForId(id) {\n    const plotModel = this.plot.get('domainObject');\n    let yAxis;\n    if (this.id === 1) {\n      yAxis = plotModel.configuration?.yAxis;\n    } else {\n      if (plotModel.configuration?.additionalYAxes) {\n        yAxis = plotModel.configuration.additionalYAxes.find(\n          (additionalYAxis) => additionalYAxis.id === id\n        );\n      }\n    }\n\n    return yAxis;\n  }\n  /**\n   * @param {import('./PlotSeries').default} series\n   */\n  trackSeries(series) {\n    this.listenTo(series, 'change:stats', (seriesStats) => {\n      if (series.get('yAxisId') !== this.id) {\n        return;\n      }\n\n      if (!seriesStats) {\n        this.resetStats();\n      } else {\n        this.updateStats(seriesStats);\n      }\n    });\n    this.listenTo(series, 'change:yKey', () => {\n      if (series.get('yAxisId') !== this.id) {\n        return;\n      }\n\n      this.updateFromSeries(this.seriesCollection);\n    });\n\n    this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => {\n      if (oldYAxisId && this.id === oldYAxisId) {\n        this.resetStats();\n        this.updateFromSeries(this.seriesCollection);\n      }\n\n      if (series.get('yAxisId') === this.id) {\n        this.resetStats();\n        this.updateFromSeries(this.seriesCollection);\n      }\n    });\n  }\n  untrackSeries(series) {\n    this.stopListening(series);\n    this.resetStats();\n    this.updateFromSeries(this.seriesCollection);\n  }\n\n  /**\n   * This is called in order to map the user-provided `range` to the\n   * `displayRange` that we actually use for plot display.\n   *\n   * @param {import('./XAxisModel').NumberRange} range\n   */\n  updateDisplayRange(range) {\n    if (this.get('autoscale')) {\n      return;\n    }\n\n    const _range = { ...range };\n\n    if (this.get('logMode')) {\n      _range.min = symlog(range.min, 10);\n      _range.max = symlog(range.max, 10);\n    }\n\n    this.set('displayRange', _range);\n  }\n\n  /**\n   * @param {boolean} autoscale\n   */\n  toggleAutoscale(autoscale) {\n    if (autoscale && this.has('stats')) {\n      this.set('displayRange', this.applyPadding(this.get('stats')));\n\n      return;\n    }\n\n    const range = this.get('range');\n\n    if (range) {\n      // If we already have a user-defined range, make sure it maps to the\n      // range we'll actually use for the ticks.\n\n      const _range = { ...range };\n\n      if (this.get('logMode')) {\n        _range.min = symlog(range.min, 10);\n        _range.max = symlog(range.max, 10);\n      }\n\n      this.set('displayRange', _range);\n    }\n  }\n\n  /** @param {boolean} logMode */\n  onLogModeChange(logMode) {\n    const range = this.get('displayRange');\n\n    if (logMode) {\n      range.min = symlog(range.min, 10);\n      range.max = symlog(range.max, 10);\n    } else {\n      range.min = antisymlog(range.min, 10);\n      range.max = antisymlog(range.max, 10);\n    }\n\n    this.set('displayRange', range);\n\n    this.resetSeries();\n  }\n  resetSeries() {\n    const series = this.getSeriesForYAxis(this.seriesCollection);\n    series.forEach((plotSeries) => {\n      plotSeries.logMode = this.get('logMode');\n      plotSeries.reset(plotSeries.getSeriesData());\n    });\n    // Update the series collection labels and formatting\n    this.updateFromSeries(this.seriesCollection);\n  }\n\n  /**\n   * For a given series collection, get the metadata of the current yKey for each series.\n   * Then return first available value of the given property from the metadata.\n   * @param {import('./SeriesCollection').default} series\n   * @param {string} property\n   */\n  getMetadataValueByProperty(series, property) {\n    return series\n      .map((s) => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : ''))\n      .reduce((a, b) => {\n        if (a === undefined) {\n          return b;\n        }\n\n        if (a === b) {\n          return a;\n        }\n\n        return '';\n      }, undefined);\n  }\n  /**\n   * Update yAxis format, values, and label from known series.\n   * @param {import('./SeriesCollection').default} seriesCollection\n   */\n  updateFromSeries(seriesCollection) {\n    const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);\n    if (!seriesForThisYAxis.length) {\n      return;\n    }\n\n    const yAxis = this.getYAxisForId(this.id);\n    const label = yAxis?.label;\n    const sampleSeries = seriesForThisYAxis[0];\n    if (!sampleSeries || !sampleSeries.metadata) {\n      if (!label) {\n        this.unset('label');\n      }\n\n      return;\n    }\n\n    const yKey = sampleSeries.get('yKey');\n    const yMetadata = sampleSeries.metadata.value(yKey);\n    const yFormat = sampleSeries.formats[yKey];\n\n    if (this.get('logMode')) {\n      this.set('format', (n) => yFormat.format(antisymlog(n, 10)));\n    } else {\n      this.set('format', (n) => yFormat.format(n));\n    }\n\n    this.set('values', yMetadata.values);\n\n    if (!label) {\n      const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');\n      if (labelName) {\n        this.set('label', labelName);\n\n        return;\n      }\n\n      //if the name is not available, set the units as the label\n      const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');\n      if (labelUnits) {\n        this.set('label', labelUnits);\n\n        return;\n      }\n    }\n  }\n  /**\n   * @override\n   * @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options\n   * @returns {Partial<YAxisModelType>}\n   */\n  defaultModel(options) {\n    return {\n      frozen: false,\n      autoscale: true,\n      logMode: options.model?.logMode ?? false,\n      autoscalePadding: 0.1,\n      id: options.id,\n      range: options.model?.range\n    };\n  }\n\n  destroy() {\n    this.plot = undefined;\n    this.stopListening();\n  }\n}\n\n/** @typedef {any} TODO */\n\n/**\n@typedef {import('./XAxisModel').AxisModelType & {\n    autoscale: boolean\n    logMode: boolean\n    autoscalePadding: number\n    stats?: import('./XAxisModel').NumberRange\n    values: Array<TODO>\n}} YAxisModelType\n*/\n\n/**\n@typedef {{\n    plot: import('./PlotConfigurationModel').default\n}} YAxisModelOptions\n*/\n"
  },
  {
    "path": "src/plugins/plot/draw/Draw2D.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport eventHelpers from '../lib/eventHelpers.js';\nimport { MARKER_SHAPES } from './MarkerShapes.js';\n/**\n * Create a new draw API utilizing the Canvas's 2D API for rendering.\n *\n * @constructor\n * @param {CanvasElement} canvas the canvas object to render upon\n * @throws {Error} an error is thrown if Canvas's 2D API is unavailable\n */\n\n/**\n * Create a new draw API utilizing the Canvas's 2D API for rendering.\n *\n * @constructor\n * @param {CanvasElement} canvas the canvas object to render upon\n * @throws {Error} an error is thrown if Canvas's 2D API is unavailable\n */\nclass Draw2D extends EventEmitter {\n  constructor(canvas) {\n    super();\n    eventHelpers.extend(this);\n    this.canvas = canvas;\n    this.c2d = canvas.getContext('2d');\n    this.width = canvas.width;\n    this.height = canvas.height;\n    this.dimensions = [this.width, this.height];\n    this.origin = [0, 0];\n\n    if (!this.c2d) {\n      throw new Error('Canvas 2d API unavailable.');\n    }\n  }\n  // Convert from logical to physical x coordinates\n  x(v) {\n    return ((v - this.origin[0]) / this.dimensions[0]) * this.width;\n  }\n  // Convert from logical to physical y coordinates\n  y(v) {\n    return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height;\n  }\n  // Set the color to be used for drawing operations\n  setColor(color) {\n    const mappedColor = color\n      .map(function (c, i) {\n        return i < 3 ? Math.floor(c * 255) : c;\n      })\n      .join(',');\n    this.c2d.strokeStyle = 'rgba(' + mappedColor + ')';\n    this.c2d.fillStyle = 'rgba(' + mappedColor + ')';\n  }\n  clear() {\n    this.width = this.canvas.width = this.canvas.offsetWidth;\n    this.height = this.canvas.height = this.canvas.offsetHeight;\n    this.c2d.clearRect(0, 0, this.width, this.height);\n  }\n  setDimensions(newDimensions, newOrigin) {\n    this.dimensions = newDimensions;\n    this.origin = newOrigin;\n  }\n  drawLine(buf, color, points) {\n    let i;\n\n    this.setColor(color);\n\n    // Configure context to draw two-pixel-thick lines\n    this.c2d.lineWidth = 1;\n\n    // Start a new path...\n    if (buf.length > 1) {\n      this.c2d.beginPath();\n      this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));\n    }\n\n    // ...and add points to it...\n    for (i = 2; i < points * 2; i = i + 2) {\n      this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));\n    }\n\n    // ...before finally drawing it.\n    this.c2d.stroke();\n  }\n  drawSquare(min, max, color) {\n    const x1 = this.x(min[0]);\n    const y1 = this.y(min[1]);\n    const w = this.x(max[0]) - x1;\n    const h = this.y(max[1]) - y1;\n\n    this.setColor(color);\n    this.c2d.fillRect(x1, y1, w, h);\n  }\n  drawPoints(buf, color, points, pointSize, shape) {\n    const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this);\n\n    this.setColor(color);\n\n    for (let i = 0; i < points; i++) {\n      drawC2DShape(this.x(buf[i * 2]), this.y(buf[i * 2 + 1]), pointSize);\n    }\n  }\n  drawLimitPoint(x, y, size) {\n    this.c2d.fillRect(x + size, y, size, size);\n    this.c2d.fillRect(x, y + size, size, size);\n    this.c2d.fillRect(x - size, y, size, size);\n    this.c2d.fillRect(x, y - size, size, size);\n  }\n  drawLimitPoints(points, color, pointSize) {\n    const limitSize = pointSize * 2;\n    const offset = limitSize / 2;\n\n    this.setColor(color);\n\n    for (let i = 0; i < points.length; i++) {\n      this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize);\n    }\n  }\n}\nexport default Draw2D;\n"
  },
  {
    "path": "src/plugins/plot/draw/DrawLoader.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Draw2D from './Draw2D.js';\nimport DrawWebGL from './DrawWebGL.js';\n\nconst CHARTS = [\n  {\n    MAX_INSTANCES: 16,\n    API: DrawWebGL,\n    ALLOCATIONS: []\n  },\n  {\n    MAX_INSTANCES: Number.POSITIVE_INFINITY,\n    API: Draw2D,\n    ALLOCATIONS: []\n  }\n];\n/**\n * Draw loader attaches a draw API to a canvas element and returns the\n * draw API.\n */\n\nexport const DrawLoader = {\n  /**\n     * Return the first draw API available.  Returns\n     * `undefined` if a draw API could not be constructed.\n     *.\n     * @param {CanvasElement} canvas - The canvas element to attach\n     the draw API to.\n     */\n  getDrawAPI: function (canvas, overlay) {\n    let api;\n\n    CHARTS.forEach(function (CHART_TYPE) {\n      if (api) {\n        return;\n      }\n\n      if (CHART_TYPE.ALLOCATIONS.length >= CHART_TYPE.MAX_INSTANCES) {\n        return;\n      }\n\n      try {\n        api = new CHART_TYPE.API(canvas, overlay);\n        CHART_TYPE.ALLOCATIONS.push(api);\n      } catch (e) {\n        console.warn(\n          ['Could not instantiate chart', CHART_TYPE.API.name, ';', e.message].join(' ')\n        );\n      }\n    });\n\n    if (!api) {\n      console.warn('Cannot initialize mct-chart.');\n    }\n\n    return api;\n  },\n  /**\n   * Returns a fallback draw api.\n   */\n  getFallbackDrawAPI: function (canvas, overlay) {\n    const api = new CHARTS[1].API(canvas, overlay);\n    CHARTS[1].ALLOCATIONS.push(api);\n\n    return api;\n  },\n  releaseDrawAPI: function (api) {\n    CHARTS.forEach(function (CHART_TYPE) {\n      if (api instanceof CHART_TYPE.API) {\n        CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1);\n      }\n    });\n    if (api.destroy) {\n      api.destroy();\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/plot/draw/DrawWebGL.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport eventHelpers from '../lib/eventHelpers.js';\nimport { MARKER_SHAPES } from './MarkerShapes.js';\n\n// WebGL shader sources (for drawing plain colors)\n// discard; stops the pixel from being drawn\nconst FRAGMENT_SHADER = `\n        precision mediump float;\n        uniform vec4 uColor;\n        uniform int uMarkerShape;\n        \n        void main(void) {\n            gl_FragColor = uColor;\n\n            if (uMarkerShape > 1) {\n                vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;\n\n                if (uMarkerShape == 2) { // circle\n                    float distance = length(clipSpacePointCoord);\n\n                    if (distance > 1.0) {\n                        discard;\n                    }\n                } else if (uMarkerShape == 3) { // diamond\n                    float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);\n\n                    if (distance > 1.0) {\n                        discard;\n                    }\n                } else if (uMarkerShape == 4) { // triangle\n                    float x = clipSpacePointCoord.x;\n                    float y = clipSpacePointCoord.y;\n                    float distance = 2.0 * x - 1.0;\n                    float distance2 = -2.0 * x - 1.0;\n\n                    if (distance > y || distance2 > y) {\n                        discard;\n                    }\n                }\n\n            }\n        }\n    `;\n\n/* This code is taking a 2D vertex position (aVertexPosition) and transforming it into a clip-space coordinate system (where x and y range from -1 to 1)\n   1. (aVertexPosition - uOrigin): effectively translates the aVertexPosition so that the uOrigin becomes the new (0,0). It shifts the coordinate system.\n   2. (aVertexPosition - uOrigin) / uDimensions: performs a normalization step. If uDimensions represents the full width and height, this scales the coordinates so that they fall within the range [0, 1] for both x and y, relative to the uOrigin and uDimensions bounding box\n   3. 2.0 * ((aVertexPosition - uOrigin) / uDimensions): scales the coordinates to be in the range [0, 2]\n   4. 2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1): shifts the range from [0, 2] to [-1, 1]\n*/\nconst VERTEX_SHADER = `\n        attribute vec2 aVertexPosition;\n        uniform vec2 uDimensions;\n        uniform vec2 uOrigin;\n        uniform float uPointSize;\n        \n        void main(void) {\n            gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);\n            gl_PointSize = uPointSize;\n        }\n    `;\n\n/**\n * Create a draw api utilizing WebGL.\n *\n * @constructor\n * @param {CanvasElement} canvas the canvas object to render upon\n * @throws {Error} an error is thrown if WebGL is unavailable.\n */\nclass DrawWebGL extends EventEmitter {\n  constructor(canvas, overlay) {\n    super();\n    eventHelpers.extend(this);\n    this.canvas = canvas;\n    this.gl =\n      this.canvas.getContext('webgl', { preserveDrawingBuffer: true }) ||\n      this.canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true });\n\n    this.overlay = overlay;\n    this.c2d = overlay.getContext('2d');\n    if (!this.c2d) {\n      throw new Error('No canvas 2d!');\n    }\n\n    // Ensure a context was actually available before proceeding\n    if (!this.gl) {\n      throw new Error('WebGL unavailable.');\n    }\n\n    this.initContext();\n\n    this.listenTo(this.canvas, 'webglcontextlost', this.onContextLost, this);\n  }\n  onContextLost(event) {\n    this.emit('error');\n    this.isContextLost = true;\n    this.destroy();\n    // TODO re-initialize and re-draw on context restored\n  }\n  initContext() {\n    // Initialize shaders\n    this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);\n    this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);\n    this.gl.compileShader(this.vertexShader);\n\n    this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);\n    this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);\n    this.gl.compileShader(this.fragmentShader);\n\n    // Assemble vertex/fragment shaders into programs\n    this.program = this.gl.createProgram();\n    this.gl.attachShader(this.program, this.vertexShader);\n    this.gl.attachShader(this.program, this.fragmentShader);\n    this.gl.linkProgram(this.program);\n    this.gl.useProgram(this.program);\n\n    // Get locations for attribs/uniforms from the\n    // shader programs (to pass values into shaders at draw-time)\n\n    // only available to the vertex shader - this returns an INDEX into the list of attributes maintained by the GPU\n    this.aVertexPosition = this.gl.getAttribLocation(this.program, 'aVertexPosition');\n\n    // available to both vertex and fragment shaders\n    this.uColor = this.gl.getUniformLocation(this.program, 'uColor');\n    this.uMarkerShape = this.gl.getUniformLocation(this.program, 'uMarkerShape');\n    this.uDimensions = this.gl.getUniformLocation(this.program, 'uDimensions');\n    this.uOrigin = this.gl.getUniformLocation(this.program, 'uOrigin');\n    this.uPointSize = this.gl.getUniformLocation(this.program, 'uPointSize');\n\n    // enable the attribute to that it can be used / accessed\n    this.gl.enableVertexAttribArray(this.aVertexPosition);\n\n    // Create a buffer to hold points which will be drawn\n    this.buffer = this.gl.createBuffer();\n\n    // Enable blending, for smoothness\n    this.gl.enable(this.gl.BLEND);\n    // sfactor is the source alpha value, dfactor is one minus source alpha value\n    // conceptually, color(RGBA) = (sourceColor * sfactor) + (destinationColor * dfactor)\n    this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);\n  }\n  destroy() {\n    // Lose the context and delete all associated resources\n    // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly\n    this.gl?.getExtension('WEBGL_lose_context')?.loseContext();\n    this.gl?.deleteBuffer(this.buffer);\n    this.buffer = undefined;\n    this.gl?.deleteProgram(this.program);\n    this.program = undefined;\n    this.gl?.deleteShader(this.vertexShader);\n    this.vertexShader = undefined;\n    this.gl?.deleteShader(this.fragmentShader);\n    this.fragmentShader = undefined;\n    this.gl = undefined;\n\n    this.stopListening();\n    this.canvas = undefined;\n    this.overlay = undefined;\n  }\n  // Convert from logical to physical x coordinates\n  x(v) {\n    return ((v - this.origin[0]) / this.dimensions[0]) * this.width;\n  }\n  // Convert from logical to physical y coordinates\n  y(v) {\n    return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height;\n  }\n  doDraw(drawType, buf, color, points, shape) {\n    if (this.isContextLost) {\n      return;\n    }\n\n    const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;\n\n    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);\n    this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);\n    this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);\n    this.gl.uniform4fv(this.uColor, color);\n    this.gl.uniform1i(this.uMarkerShape, shapeCode);\n    if (points !== 0) {\n      this.gl.drawArrays(drawType, 0, points);\n    }\n  }\n  clear() {\n    if (this.isContextLost) {\n      return;\n    }\n\n    this.height = this.canvas.height = this.canvas.offsetHeight;\n    this.width = this.canvas.width = this.canvas.offsetWidth;\n    this.overlay.height = this.overlay.offsetHeight;\n    this.overlay.width = this.overlay.offsetWidth;\n    // Set the viewport size; note that we use the width/height\n    // that our WebGL context reports, which may be lower\n    // resolution than the canvas we requested.\n    this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);\n    this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT);\n  }\n  /**\n   * Set the logical boundaries of the chart.\n   * @param {number[]} dimensions the horizontal and\n   *        vertical dimensions of the chart\n   * @param {number[]} origin the horizontal/vertical\n   *        origin of the chart\n   */\n  setDimensions(dimensions, origin) {\n    this.dimensions = dimensions;\n    this.origin = origin;\n    if (this.isContextLost) {\n      return;\n    }\n\n    if (dimensions && dimensions.length > 0 && origin && origin.length > 0) {\n      this.gl?.uniform2fv(this.uDimensions, dimensions);\n      this.gl?.uniform2fv(this.uOrigin, origin);\n    }\n  }\n  /**\n   * Draw the supplied buffer as a line strip (a sequence\n   * of line segments), in the chosen color.\n   * @param {Float32Array} buf the line strip to draw,\n   *        in alternating x/y positions\n   * @param {number[]} color the color to use when drawing\n   *        the line, as an RGBA color where each element\n   *        is in the range of 0.0-1.0\n   * @param {number} points the number of points to draw\n   */\n  drawLine(buf, color, points) {\n    if (this.isContextLost) {\n      return;\n    }\n\n    this.doDraw(this.gl.LINE_STRIP, buf, color, points);\n  }\n  /**\n   * Draw the buffer as points.\n   *\n   */\n  drawPoints(buf, color, points, pointSize, shape) {\n    if (this.isContextLost) {\n      return;\n    }\n\n    this.gl.uniform1f(this.uPointSize, pointSize);\n    this.doDraw(this.gl.POINTS, buf, color, points, shape);\n  }\n  /**\n   * Draw a rectangle extending from one corner to another,\n   * in the chosen color.\n   * @param {number[]} min the first corner of the rectangle\n   * @param {number[]} max the opposite corner\n   * @param {number[]} color the color to use when drawing\n   *        the rectangle, as an RGBA color where each element\n   *        is in the range of 0.0-1.0\n   */\n  drawSquare(min, max, color) {\n    if (this.isContextLost) {\n      return;\n    }\n\n    this.doDraw(\n      this.gl.TRIANGLE_FAN,\n      new Float32Array(min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])),\n      color,\n      4\n    );\n  }\n  drawLimitPoint(x, y, size) {\n    this.c2d.fillRect(x + size, y, size, size);\n    this.c2d.fillRect(x, y + size, size, size);\n    this.c2d.fillRect(x - size, y, size, size);\n    this.c2d.fillRect(x, y - size, size, size);\n  }\n  drawLimitPoints(points, color, pointSize) {\n    const limitSize = pointSize * 2;\n    const offset = limitSize / 2;\n\n    const mappedColor = color\n      .map(function (c, i) {\n        return i < 3 ? Math.floor(c * 255) : c;\n      })\n      .join(',');\n    this.c2d.strokeStyle = 'rgba(' + mappedColor + ')';\n    this.c2d.fillStyle = 'rgba(' + mappedColor + ')';\n\n    for (let i = 0; i < points.length; i++) {\n      this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize);\n    }\n  }\n}\nexport default DrawWebGL;\n"
  },
  {
    "path": "src/plugins/plot/draw/MarkerShapes.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @label string (required) display name of shape\n * @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader\n * @drawC2D function (required) canvas2d draw function\n */\nexport const MARKER_SHAPES = {\n  point: {\n    label: 'Point',\n    drawWebGL: 1,\n    drawC2D: function (x, y, size) {\n      const offset = size / 2;\n\n      this.c2d.fillRect(x - offset, y - offset, size, size);\n    }\n  },\n  circle: {\n    label: 'Circle',\n    drawWebGL: 2,\n    drawC2D: function (x, y, size) {\n      const radius = size / 2;\n\n      this.c2d.beginPath();\n      this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false);\n      this.c2d.closePath();\n      this.c2d.fill();\n    }\n  },\n  diamond: {\n    label: 'Diamond',\n    drawWebGL: 3,\n    drawC2D: function (x, y, size) {\n      const offset = size / 2;\n      const top = [x, y + offset];\n      const right = [x + offset, y];\n      const bottom = [x, y - offset];\n      const left = [x - offset, y];\n\n      this.c2d.beginPath();\n      this.c2d.moveTo(...top);\n      this.c2d.lineTo(...right);\n      this.c2d.lineTo(...bottom);\n      this.c2d.lineTo(...left);\n      this.c2d.closePath();\n      this.c2d.fill();\n    }\n  },\n  triangle: {\n    label: 'Triangle',\n    drawWebGL: 4,\n    drawC2D: function (x, y, size) {\n      const offset = size / 2;\n      const v1 = [x, y - offset];\n      const v2 = [x - offset, y + offset];\n      const v3 = [x + offset, y + offset];\n\n      this.c2d.beginPath();\n      this.c2d.moveTo(...v1);\n      this.c2d.lineTo(...v2);\n      this.c2d.lineTo(...v3);\n      this.c2d.closePath();\n      this.c2d.fill();\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/plot/inspector/PlotOptions.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div>\n    <div v-if=\"canEdit\">\n      <PlotOptionsEdit />\n    </div>\n    <div v-else>\n      <PlotOptionsBrowse />\n    </div>\n  </div>\n</template>\n\n<script>\nimport PlotOptionsBrowse from './PlotOptionsBrowse.vue';\nimport PlotOptionsEdit from './PlotOptionsEdit.vue';\nexport default {\n  components: {\n    PlotOptionsBrowse,\n    PlotOptionsEdit\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/PlotOptionsBrowse.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul v-if=\"loaded\" class=\"js-plot-options-browse\" aria-label=\"Plot Configuration\">\n    <li v-if=\"showPlotSeries\" class=\"c-tree\" aria-labelledby=\"plot-series-header\">\n      <h2 id=\"plot-series-header\" class=\"--first\">Plot Series</h2>\n      <ul aria-label=\"Plot Series Items\" class=\"l-inspector-part\">\n        <PlotOptionsItem v-for=\"series in plotSeries\" :key=\"series.keyString\" :series=\"series\" />\n      </ul>\n    </li>\n    <ul v-if=\"showYAxisProperties\" aria-label=\"Y Axes\" class=\"l-inspector-part js-yaxis-properties\">\n      <li\n        v-for=\"(yAxis, index) in yAxesWithSeries\"\n        :key=\"`yAxis-${index}`\"\n        :aria-labelledby=\"getYAxisHeaderId(index)\"\n      >\n        <h2 :id=\"getYAxisHeaderId(index)\">\n          Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}\n        </h2>\n        <ul class=\"grid-properties\" :aria-label=\"`Y Axis ${yAxis.id} Properties`\">\n          <li\n            v-for=\"(prop, key) in yAxisProperties(yAxis)\"\n            :key=\"key\"\n            :aria-labelledby=\"getYAxisPropId(index, prop.label)\"\n            class=\"grid-row\"\n            role=\"grid\"\n          >\n            <div\n              :id=\"getYAxisPropId(index, prop.label)\"\n              class=\"grid-cell label\"\n              :title=\"prop.title\"\n              role=\"gridcell\"\n            >\n              {{ prop.label }}\n            </div>\n            <div class=\"grid-cell value\" role=\"gridcell\">{{ prop.value }}</div>\n          </li>\n        </ul>\n      </li>\n    </ul>\n    <li v-if=\"showLegendProperties\" class=\"grid-properties\" aria-label=\"Legend Configuration\">\n      <ul class=\"l-inspector-part js-legend-properties\" aria-labelledby=\"legend-header\">\n        <h2 id=\"legend-header\" class=\"--first\">Legend</h2>\n        <li v-for=\"(prop, key) in legendProperties\" :key=\"key\" class=\"grid-row\">\n          <div class=\"u-contents\" :aria-label=\"prop.label\">\n            <div class=\"grid-cell label\" :title=\"prop.title\">{{ prop.label }}</div>\n            <div class=\"grid-cell value\" :class=\"prop.class\">{{ prop.value }}</div>\n          </div>\n        </li>\n      </ul>\n    </li>\n  </ul>\n</template>\n\n<script>\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport PlotOptionsItem from './PlotOptionsItem.vue';\n\nexport default {\n  components: {\n    PlotOptionsItem\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  data() {\n    return {\n      config: {},\n      position: '',\n      hideLegendWhenSmall: '',\n      expandByDefault: '',\n      valueToShowWhenCollapsed: '',\n      showTimestampWhenExpanded: '',\n      showValueWhenExpanded: '',\n      showMinimumWhenExpanded: '',\n      showMaximumWhenExpanded: '',\n      showUnitsWhenExpanded: '',\n      showLegendsForChildren: '',\n      loaded: false,\n      plotSeries: [],\n      yAxes: []\n    };\n  },\n  computed: {\n    isNestedWithinAStackedPlot() {\n      return this.path.find(\n        (pathObject, pathObjIndex) =>\n          pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked'\n      );\n    },\n    isStackedPlotObject() {\n      return this.path.find(\n        (pathObject, pathObjIndex) =>\n          pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked'\n      );\n    },\n    showLegendDetails() {\n      return (\n        !this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)\n      );\n    },\n    yAxesWithSeries() {\n      return this.yAxes.filter((yAxis) => yAxis.seriesCount > 0);\n    },\n    showPlotSeries() {\n      return !this.isStackedPlotObject;\n    },\n    showYAxisProperties() {\n      return this.plotSeries.length && !this.isStackedPlotObject;\n    },\n    showLegendProperties() {\n      return this.isStackedPlotObject || !this.isNestedWithinAStackedPlot;\n    },\n    legendProperties() {\n      const props = {};\n\n      if (this.isStackedPlotObject) {\n        props.showLegendsForChildren = {\n          label: 'Show legend per plot',\n          title: 'Display legends per sub plot.',\n          value: this.showLegendsForChildren ? 'Yes' : 'No'\n        };\n      }\n\n      if (this.showLegendDetails) {\n        Object.assign(props, {\n          position: {\n            label: 'Position',\n            title: 'The position of the legend relative to the plot display area.',\n            value: this.position,\n            class: 'capitalize'\n          },\n          hideLegendWhenSmall: {\n            label: 'Hide when plot small',\n            title: 'Hide the legend when the plot is small',\n            value: this.hideLegendWhenSmall ? 'Yes' : 'No'\n          },\n          expandByDefault: {\n            label: 'Expand by Default',\n            title: 'Show the legend expanded by default',\n            value: this.expandByDefault ? 'Yes' : 'No'\n          },\n          valueToShowWhenCollapsed: {\n            label: 'Show when collapsed:',\n            title: \"What to display in the legend when it's collapsed.\",\n            value: this.valueToShowWhenCollapsed.replace('nearest', '')\n          },\n          expandedValues: {\n            label: 'Show when expanded:',\n            title: \"What to display in the legend when it's expanded.\",\n            value: this.getExpandedValues(),\n            class: 'comma-list'\n          }\n        });\n      }\n\n      return props;\n    }\n  },\n  mounted() {\n    eventHelpers.extend(this);\n    this.config = this.getConfig();\n    if (!this.isStackedPlotObject) {\n      this.initYAxesConfiguration();\n      this.registerListeners();\n    }\n    this.initLegendConfiguration();\n\n    this.loaded = true;\n  },\n  beforeUnmount() {\n    this.stopListening();\n  },\n  methods: {\n    initYAxesConfiguration() {\n      if (this.config) {\n        let range = this.config.yAxis.get('range');\n\n        this.yAxes.push({\n          id: this.config.yAxis.id,\n          seriesCount: 0,\n          label: this.config.yAxis.get('label'),\n          autoscale: this.config.yAxis.get('autoscale'),\n          logMode: this.config.yAxis.get('logMode'),\n          autoscalePadding: this.config.yAxis.get('autoscalePadding'),\n          rangeMin: range?.min ?? '',\n          rangeMax: range?.max ?? ''\n        });\n        this.config.additionalYAxes.forEach((yAxis) => {\n          range = yAxis.get('range');\n\n          this.yAxes.push({\n            id: yAxis.id,\n            seriesCount: 0,\n            label: yAxis.get('label'),\n            autoscale: yAxis.get('autoscale'),\n            logMode: yAxis.get('logMode'),\n            autoscalePadding: yAxis.get('autoscalePadding'),\n            rangeMin: range?.min ?? '',\n            rangeMax: range?.max ?? ''\n          });\n        });\n      }\n    },\n    initLegendConfiguration() {\n      if (this.config) {\n        this.position = this.config.legend.get('position');\n        this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');\n        this.expandByDefault = this.config.legend.get('expandByDefault');\n        this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');\n        this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');\n        this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');\n        this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');\n        this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');\n        this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');\n        this.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');\n      }\n    },\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      return configStore.get(configId);\n    },\n    registerListeners() {\n      if (this.config) {\n        this.config.series.forEach(this.addSeries, this);\n\n        this.listenTo(this.config.series, 'add', this.addSeries, this);\n        this.listenTo(this.config.series, 'remove', this.removeSeries, this);\n      }\n    },\n\n    setYAxisLabel(yAxisId) {\n      const found = this.yAxes.find((yAxis) => yAxis.id === yAxisId);\n      if (found && found.seriesCount > 0) {\n        const mainYAxisId = this.config.yAxis.id;\n        if (mainYAxisId === yAxisId) {\n          found.label = this.config.yAxis.get('label');\n        } else {\n          const additionalYAxis = this.config.additionalYAxes.find((axis) => axis.id === yAxisId);\n          if (additionalYAxis) {\n            found.label = additionalYAxis.get('label');\n          }\n        }\n      }\n    },\n\n    addSeries(series, index) {\n      const yAxisId = series.get('yAxisId');\n      this.updateAxisUsageCount(yAxisId, 1);\n      this.plotSeries[index] = series;\n      this.setYAxisLabel(yAxisId);\n    },\n\n    removeSeries(plotSeries, index) {\n      const yAxisId = plotSeries.get('yAxisId');\n      this.updateAxisUsageCount(yAxisId, -1);\n      this.plotSeries.splice(index, 1);\n      this.setYAxisLabel(yAxisId);\n    },\n\n    updateAxisUsageCount(yAxisId, updateCount) {\n      const foundYAxis = this.yAxes.find((yAxis) => yAxis.id === yAxisId);\n      if (foundYAxis) {\n        foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;\n      }\n    },\n    getYAxisHeaderId(index) {\n      return `yAxis-${index}-header`;\n    },\n\n    getYAxisPropId(index, label) {\n      return `y-axis-${index}-${label.toLowerCase().replace(' ', '-')}`;\n    },\n\n    yAxisAriaLabel(yAxis) {\n      return this.yAxesWithSeries.length > 1\n        ? `Y Axis ${yAxis.id} Properties`\n        : 'Y Axis Properties';\n    },\n    yAxisProperties(yAxis) {\n      const props = {\n        label: {\n          label: 'Label',\n          title: 'Manually override how the Y axis is labeled.',\n          value: yAxis.label ? yAxis.label : 'Not defined'\n        },\n        logMode: {\n          label: 'Log mode',\n          title: 'Enable log mode.',\n          value: yAxis.logMode ? 'Enabled' : 'Disabled'\n        },\n        autoscale: {\n          label: 'Auto scale',\n          title: 'Automatically scale the Y axis to keep all values in view.',\n          value: yAxis.autoscale ? `Enabled: ${yAxis.autoscalePadding}` : 'Disabled'\n        }\n      };\n\n      if (!yAxis.autoscale) {\n        if (yAxis.rangeMin !== '') {\n          props.rangeMin = {\n            label: 'Minimum value',\n            title: 'Minimum Y axis value.',\n            value: yAxis.rangeMin\n          };\n        }\n        if (yAxis.rangeMax !== '') {\n          props.rangeMax = {\n            label: 'Maximum value',\n            title: 'Maximum Y axis value.',\n            value: yAxis.rangeMax\n          };\n        }\n      }\n\n      return props;\n    },\n    getExpandedValues() {\n      const values = [];\n      if (this.showTimestampWhenExpanded) {\n        values.push('Timestamp');\n      }\n      if (this.showValueWhenExpanded) {\n        values.push('Value');\n      }\n      if (this.showMinimumWhenExpanded) {\n        values.push('Min');\n      }\n      if (this.showMaximumWhenExpanded) {\n        values.push('Max');\n      }\n      if (this.showUnitsWhenExpanded) {\n        values.push('Unit');\n      }\n      return values.join(', ');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/PlotOptionsEdit.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div v-if=\"loaded\" class=\"js-plot-options-edit\">\n    <ul v-if=\"!isStackedPlotObject\" class=\"c-tree\" role=\"tree\">\n      <h2 class=\"--first\" title=\"Display properties for this Plot Series object\">Plot Series</h2>\n      <li v-for=\"series in plotSeries\" :key=\"series.keyString\">\n        <SeriesForm :series=\"series\" @series-updated=\"updateSeriesConfigForObject\" />\n      </li>\n    </ul>\n    <YAxisForm\n      v-for=\"(yAxisId, index) in yAxesIds\"\n      :id=\"yAxisId.id\"\n      :key=\"`yAxis-${index}`\"\n      class=\"grid-properties js-yaxis-grid-properties\"\n      :y-axis=\"config.yAxis\"\n      role=\"group\"\n      aria-labelledby=\"y-axis-group\"\n      @series-updated=\"updateSeriesConfigForObject\"\n    />\n    <ul\n      v-if=\"isStackedPlotObject || !isStackedPlotNestedObject\"\n      class=\"l-inspector-part\"\n      aria-label=\"Legend Properties\"\n      role=\"tree\"\n    >\n      <h2 class=\"--first\" title=\"Legend options\">Legend</h2>\n      <LegendForm role=\"treeitem\" tabindex=\"0\" class=\"grid-properties\" :legend=\"config.legend\" />\n    </ul>\n  </div>\n</template>\n<script>\nimport _ from 'lodash';\n\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport LegendForm from './forms/LegendForm.vue';\nimport SeriesForm from './forms/SeriesForm.vue';\nimport YAxisForm from './forms/YAxisForm.vue';\n\nexport default {\n  components: {\n    LegendForm,\n    SeriesForm,\n    YAxisForm\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  data() {\n    return {\n      config: {},\n      yAxes: [],\n      plotSeries: [],\n      loaded: false\n    };\n  },\n  computed: {\n    isStackedPlotNestedObject() {\n      return this.path.find(\n        (pathObject, pathObjIndex) =>\n          pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked'\n      );\n    },\n    isStackedPlotObject() {\n      return this.path.find(\n        (pathObject, pathObjIndex) =>\n          pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'\n      );\n    },\n    yAxesIds() {\n      if (this.isStackedPlotObject) {\n        return [];\n      }\n      return this.yAxes.filter((yAxis) => yAxis.seriesCount > 0);\n    }\n  },\n  created() {\n    eventHelpers.extend(this);\n    this.config = this.getConfig();\n  },\n  mounted() {\n    if (!this.isStackedPlotObject) {\n      this.yAxes = [\n        {\n          id: this.config.yAxis.id,\n          seriesCount: 0\n        }\n      ];\n      if (this.config.additionalYAxes) {\n        this.yAxes = this.yAxes.concat(\n          this.config.additionalYAxes.map((yAxis) => {\n            return {\n              id: yAxis.id,\n              seriesCount: 0\n            };\n          })\n        );\n      }\n\n      this.registerListeners();\n    }\n\n    this.loaded = true;\n  },\n  beforeUnmount() {\n    this.stopListening();\n  },\n  methods: {\n    getConfig() {\n      this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      return configStore.get(this.configId);\n    },\n    registerListeners() {\n      this.config.series.forEach(this.addSeries, this);\n\n      this.listenTo(this.config.series, 'add', this.addSeries, this);\n      this.listenTo(this.config.series, 'remove', this.removeSeries, this);\n    },\n\n    findYAxisForId(yAxisId) {\n      return this.yAxes.find((yAxis) => yAxis.id === yAxisId);\n    },\n\n    setYAxisLabel(yAxisId) {\n      const found = this.findYAxisForId(yAxisId);\n      if (found && found.seriesCount > 0) {\n        const mainYAxisId = this.config.yAxis.id;\n        if (mainYAxisId === yAxisId) {\n          found.label = this.config.yAxis.get('label');\n        } else {\n          const additionalYAxis = this.config.additionalYAxes.find((axis) => axis.id === yAxisId);\n          if (additionalYAxis) {\n            found.label = additionalYAxis.get('label');\n          }\n        }\n      }\n    },\n\n    addSeries(series, index) {\n      const yAxisId = series.get('yAxisId');\n      this.incrementAxisUsageCount(yAxisId);\n      this.plotSeries[index] = series;\n      this.setYAxisLabel(yAxisId);\n\n      if (this.isStackedPlotObject) {\n        return;\n      }\n\n      // If the series moves to a different yAxis, update the seriesCounts for both yAxes\n      // so we can display the configuration options for all used yAxes\n      this.listenTo(\n        series,\n        'change:yAxisId',\n        (newYAxisId, oldYAxisId) => {\n          this.incrementAxisUsageCount(newYAxisId);\n          this.decrementAxisUsageCount(oldYAxisId);\n        },\n        this\n      );\n    },\n\n    removeSeries(series, index) {\n      const yAxisId = series.get('yAxisId');\n      this.decrementAxisUsageCount(yAxisId);\n      this.plotSeries.splice(index, 1);\n      this.setYAxisLabel(yAxisId);\n\n      if (this.isStackedPlotObject) {\n        return;\n      }\n\n      this.stopListening(series, 'change:yAxisId');\n    },\n\n    incrementAxisUsageCount(yAxisId) {\n      this.updateAxisUsageCount(yAxisId, 1);\n    },\n\n    decrementAxisUsageCount(yAxisId) {\n      this.updateAxisUsageCount(yAxisId, -1);\n    },\n\n    updateAxisUsageCount(yAxisId, updateCount) {\n      const foundYAxis = this.findYAxisForId(yAxisId);\n      if (!foundYAxis) {\n        throw new Error(`yAxis with id ${yAxisId} not found`);\n      }\n\n      foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;\n    },\n\n    updateSeriesConfigForObject(config) {\n      const stackedPlotObject = this.path.find(\n        (pathObject) => pathObject.type === 'telemetry.plot.stacked'\n      );\n      let index = stackedPlotObject.configuration.series.findIndex((seriesConfig) => {\n        return this.openmct.objects.areIdsEqual(seriesConfig.identifier, config.identifier);\n      });\n      if (index < 0) {\n        index = stackedPlotObject.configuration.series.length;\n        const configPath = `configuration.series[${index}]`;\n        let newConfig = {\n          identifier: config.identifier\n        };\n        _.set(newConfig, `${config.path}`, config.value);\n        this.openmct.objects.mutate(stackedPlotObject, configPath, newConfig);\n      } else {\n        const configPath = `configuration.series[${index}].${config.path}`;\n        this.openmct.objects.mutate(stackedPlotObject, configPath, config.value);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/PlotOptionsItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <li class=\"c-tree__item menus-to-left\" :class=\"isAliasClass\" :aria-label=\"ariaLabel\">\n    <span\n      class=\"c-disclosure-triangle is-enabled flex-elem\"\n      :class=\"expandedCssClass\"\n      role=\"button\"\n      :aria-label=\"ariaLabelExpandCollapse\"\n      tabindex=\"0\"\n      @click=\"toggleExpanded\"\n      @keydown.enter=\"toggleExpanded\"\n    ></span>\n    <div class=\"c-object-label\" :class=\"statusClass\">\n      <div class=\"c-object-label__type-icon\" :class=\"getSeriesClass\">\n        <span class=\"is-status__indicator\" title=\"This item is missing or suspect\"></span>\n      </div>\n      <div class=\"c-object-label__name\">{{ series.domainObject.name }}</div>\n    </div>\n  </li>\n  <li v-show=\"expanded\" class=\"c-tree__item menus-to-left\" role=\"table\">\n    <ul class=\"grid-properties js-plot-options-browse-properties\" role=\"rowgroup\">\n      <li class=\"grid-row\" role=\"row\">\n        <div\n          class=\"grid-cell label\"\n          title=\"The field to be plotted as a value for this series.\"\n          role=\"cell\"\n        >\n          Value\n        </div>\n        <div class=\"grid-cell value\" role=\"cell\">\n          {{ yKey }}\n        </div>\n      </li>\n      <li class=\"grid-row\" role=\"row\">\n        <div\n          class=\"grid-cell label\"\n          title=\"The rendering method to join lines for this series.\"\n          role=\"cell\"\n        >\n          Line Method\n        </div>\n        <div class=\"grid-cell value\" role=\"cell\">\n          {{\n            {\n              none: 'None',\n              linear: 'Linear interpolation',\n              stepAfter: 'Step After'\n            }[interpolate]\n          }}\n        </div>\n      </li>\n      <li class=\"grid-row\" role=\"row\">\n        <div\n          class=\"grid-cell label\"\n          title=\"Whether markers are displayed, and their size.\"\n          role=\"cell\"\n        >\n          Markers\n        </div>\n        <div class=\"grid-cell value\" role=\"cell\">\n          {{ markerOptionsDisplayText }}\n        </div>\n      </li>\n      <li class=\"grid-row\" role=\"row\" title=\"Display markers visually denoting points in alarm.\">\n        <div class=\"grid-cell label\" role=\"cell\">Alarm Markers</div>\n        <div class=\"grid-cell value\" role=\"cell\">\n          {{ alarmMarkers ? 'Enabled' : 'Disabled' }}\n        </div>\n      </li>\n      <li class=\"grid-row\" role=\"row\">\n        <div\n          class=\"grid-cell label\"\n          title=\"Display lines visually denoting alarm limits.\"\n          role=\"cell\"\n        >\n          Limit Lines\n        </div>\n        <div class=\"grid-cell value\" role=\"cell\">\n          {{ limitLines ? 'Enabled' : 'Disabled' }}\n        </div>\n      </li>\n      <ColorSwatch\n        :current-color=\"seriesHexColor\"\n        edit-title=\"Manually set the plot line and marker color for this series.\"\n        view-title=\"The plot line and marker color for this series.\"\n        short-label=\"Color\"\n      />\n    </ul>\n  </li>\n</template>\n\n<script>\nimport ColorSwatch from '@/ui/color/ColorSwatch.vue';\n\nexport default {\n  components: {\n    ColorSwatch\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    series: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  data() {\n    return {\n      expanded: false,\n      status: null\n    };\n  },\n  computed: {\n    ariaLabel() {\n      return this.series?.domainObject?.name ?? '';\n    },\n    ariaLabelExpandCollapse() {\n      const name = this.series.domainObject.name ? ` ${this.series.domainObject.name}` : '';\n\n      return `${this.expanded ? 'Collapse' : 'Expand'}${name} Plot Series Options`;\n    },\n    isAliasClass() {\n      let cssClass = '';\n      const domainObjectPath = [this.series.domainObject, ...this.path];\n      if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {\n        cssClass = 'is-alias';\n      }\n\n      return cssClass;\n    },\n    getSeriesClass() {\n      let cssClass = '';\n      let type = this.openmct.types.get(this.series.domainObject.type);\n      if (type.definition.cssClass) {\n        cssClass = `${cssClass} ${type.definition.cssClass}`;\n      }\n\n      return cssClass;\n    },\n    expandedCssClass() {\n      if (this.expanded === true) {\n        return 'c-disclosure-triangle--expanded';\n      }\n\n      return '';\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    },\n    yKey() {\n      return this.series.get('yKey');\n    },\n    interpolate() {\n      return this.series.get('interpolate');\n    },\n    markerOptionsDisplayText() {\n      return this.series.markerOptionsDisplayText();\n    },\n    alarmMarkers() {\n      return this.series.get('alarmMarkers');\n    },\n    limitLines() {\n      return this.series.get('limitLines');\n    },\n    seriesHexColor() {\n      return this.series.get('color').asHexString();\n    }\n  },\n  created() {\n    this.status = this.openmct.status.get(this.series.domainObject.identifier);\n    this.removeStatusListener = this.openmct.status.observe(\n      this.series.domainObject.identifier,\n      this.setStatus\n    );\n  },\n  beforeUnmount() {\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n  },\n  methods: {\n    toggleExpanded() {\n      this.expanded = !this.expanded;\n    },\n    setStatus(status) {\n      this.status = status;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/PlotsInspectorViewProvider.js",
    "content": "import mount from 'utils/mount';\n\nimport PlotOptions from './PlotOptions.vue';\n\nexport default function PlotsInspectorViewProvider(openmct) {\n  return {\n    key: 'plots-inspector',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      let object = selection[0][0].context.item;\n      let parent = selection[0].length > 1 && selection[0][1].context.item;\n\n      const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';\n      const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';\n\n      return isOverlayPlotObject || isParentStackedPlotObject;\n    },\n    view: function (selection) {\n      let _destroy = null;\n      let objectPath;\n\n      if (selection.length) {\n        objectPath = selection[0].map((selectionItem) => {\n          return selectionItem.context.item;\n        });\n      }\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlotOptions: PlotOptions\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item,\n                path: objectPath\n              },\n              template: '<plot-options></plot-options>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js",
    "content": "import mount from 'utils/mount';\n\nimport PlotOptions from './PlotOptions.vue';\n\nexport default function StackedPlotsInspectorViewProvider(openmct) {\n  return {\n    key: 'stacked-plots-inspector',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      const object = selection[0][0].context.item;\n\n      const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';\n\n      return isStackedPlotObject;\n    },\n    view: function (selection) {\n      let _destroy = null;\n      let objectPath;\n\n      if (selection.length) {\n        objectPath = selection[0].map((selectionItem) => {\n          return selectionItem.context.item;\n        });\n      }\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                PlotOptions: PlotOptions\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item,\n                path: objectPath\n              },\n              template: '<plot-options></plot-options>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/inspector/forms/LegendForm.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div>\n    <li v-if=\"isStackedPlotObject\" class=\"grid-row\">\n      <div class=\"grid-cell label\" title=\"Display legends per sub plot.\">Show legend per plot</div>\n      <div class=\"grid-cell value\">\n        <input\n          v-model=\"showLegendsForChildren\"\n          aria-label=\"Show Legends For Children\"\n          type=\"checkbox\"\n          @change=\"updateForm('showLegendsForChildren')\"\n        />\n      </div>\n    </li>\n    <li v-if=\"showLegendDetails\" class=\"grid-row\">\n      <div\n        class=\"grid-cell label\"\n        title=\"The position of the legend relative to the plot display area.\"\n      >\n        Position\n      </div>\n      <div class=\"grid-cell value\">\n        <select v-model=\"position\" @change=\"updateForm('position')\">\n          <option value=\"top\">Top</option>\n          <option value=\"right\">Right</option>\n          <option value=\"bottom\">Bottom</option>\n          <option value=\"left\">Left</option>\n        </select>\n      </div>\n    </li>\n    <li v-if=\"showLegendDetails\" class=\"grid-row\">\n      <div class=\"grid-cell label\" title=\"Hide the legend when the plot is small\">\n        Hide when plot small\n      </div>\n      <div class=\"grid-cell value\">\n        <input\n          v-model=\"hideLegendWhenSmall\"\n          type=\"checkbox\"\n          @change=\"updateForm('hideLegendWhenSmall')\"\n        />\n      </div>\n    </li>\n    <li v-if=\"showLegendDetails\" class=\"grid-row\">\n      <div class=\"grid-cell label\" title=\"Show the legend expanded by default\">\n        Expand by default\n      </div>\n      <div class=\"grid-cell value\">\n        <input\n          v-model=\"expandByDefault\"\n          aria-label=\"Expand By Default\"\n          type=\"checkbox\"\n          @change=\"updateForm('expandByDefault')\"\n        />\n      </div>\n    </li>\n    <li v-if=\"showLegendDetails\" class=\"grid-row\">\n      <div class=\"grid-cell label\" title=\"What to display in the legend when it's collapsed.\">\n        When collapsed show\n      </div>\n      <div class=\"grid-cell value\">\n        <select v-model=\"valueToShowWhenCollapsed\" @change=\"updateForm('valueToShowWhenCollapsed')\">\n          <option value=\"none\">Nothing</option>\n          <option value=\"nearestTimestamp\">Nearest timestamp</option>\n          <option value=\"nearestValue\">Nearest value</option>\n          <option value=\"min\">Minimum value</option>\n          <option value=\"max\">Maximum value</option>\n          <option value=\"unit\">Unit</option>\n        </select>\n      </div>\n    </li>\n    <li v-if=\"showLegendDetails\" class=\"grid-row\">\n      <div class=\"grid-cell label\" title=\"What to display in the legend when it's expanded.\">\n        When expanded show\n      </div>\n      <div class=\"grid-cell value\">\n        <ul>\n          <li>\n            <input\n              v-model=\"showTimestampWhenExpanded\"\n              type=\"checkbox\"\n              @change=\"updateForm('showTimestampWhenExpanded')\"\n            />\n            Nearest timestamp\n          </li>\n          <li>\n            <input\n              v-model=\"showValueWhenExpanded\"\n              type=\"checkbox\"\n              @change=\"updateForm('showValueWhenExpanded')\"\n            />\n            Nearest value\n          </li>\n          <li>\n            <input\n              v-model=\"showMinimumWhenExpanded\"\n              type=\"checkbox\"\n              @change=\"updateForm('showMinimumWhenExpanded')\"\n            />\n            Minimum value\n          </li>\n          <li>\n            <input\n              v-model=\"showMaximumWhenExpanded\"\n              type=\"checkbox\"\n              @change=\"updateForm('showMaximumWhenExpanded')\"\n            />\n            Maximum value\n          </li>\n          <li>\n            <input\n              v-model=\"showUnitsWhenExpanded\"\n              type=\"checkbox\"\n              @change=\"updateForm('showUnitsWhenExpanded')\"\n            />\n            Unit\n          </li>\n        </ul>\n      </div>\n    </li>\n  </div>\n</template>\n<script>\nimport _ from 'lodash';\n\nimport { coerce, objectPath, validate } from './formUtil.js';\n\nexport default {\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    legend: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  data() {\n    return {\n      position: '',\n      hideLegendWhenSmall: '',\n      expandByDefault: '',\n      valueToShowWhenCollapsed: '',\n      showTimestampWhenExpanded: '',\n      showValueWhenExpanded: '',\n      showMinimumWhenExpanded: '',\n      showMaximumWhenExpanded: '',\n      showUnitsWhenExpanded: '',\n      showLegendsForChildren: '',\n      validation: {}\n    };\n  },\n  computed: {\n    isStackedPlotObject() {\n      return this.path.find(\n        (pathObject, pathObjIndex) =>\n          pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'\n      );\n    },\n    showLegendDetails() {\n      return (\n        !this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)\n      );\n    }\n  },\n  mounted() {\n    this.initialize();\n    this.initFormValues();\n  },\n  methods: {\n    initialize() {\n      this.fields = [\n        {\n          modelProp: 'position',\n          objectPath: 'configuration.legend.position'\n        },\n        {\n          modelProp: 'hideLegendWhenSmall',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.hideLegendWhenSmall'\n        },\n        {\n          modelProp: 'expandByDefault',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.expandByDefault'\n        },\n        {\n          modelProp: 'valueToShowWhenCollapsed',\n          objectPath: 'configuration.legend.valueToShowWhenCollapsed'\n        },\n        {\n          modelProp: 'showValueWhenExpanded',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showValueWhenExpanded'\n        },\n        {\n          modelProp: 'showTimestampWhenExpanded',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showTimestampWhenExpanded'\n        },\n        {\n          modelProp: 'showMaximumWhenExpanded',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showMaximumWhenExpanded'\n        },\n        {\n          modelProp: 'showMinimumWhenExpanded',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showMinimumWhenExpanded'\n        },\n        {\n          modelProp: 'showUnitsWhenExpanded',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showUnitsWhenExpanded'\n        },\n        {\n          modelProp: 'showLegendsForChildren',\n          coerce: Boolean,\n          objectPath: 'configuration.legend.showLegendsForChildren'\n        }\n      ];\n    },\n    initFormValues() {\n      this.position = this.legend.get('position');\n      this.hideLegendWhenSmall = this.legend.get('hideLegendWhenSmall');\n      this.expandByDefault = this.legend.get('expandByDefault');\n      this.valueToShowWhenCollapsed = this.legend.get('valueToShowWhenCollapsed');\n      this.showTimestampWhenExpanded = this.legend.get('showTimestampWhenExpanded');\n      this.showValueWhenExpanded = this.legend.get('showValueWhenExpanded');\n      this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');\n      this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');\n      this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');\n      this.showLegendsForChildren = this.legend.get('showLegendsForChildren');\n    },\n    updateForm(formKey) {\n      const newVal = this[formKey];\n      const oldVal = this.legend.get(formKey);\n      const formField = this.fields.find((field) => field.modelProp === formKey);\n\n      const path = objectPath(formField.objectPath);\n      const validationResult = validate(newVal, this.legend, formField.validate);\n      if (validationResult === true) {\n        delete this.validation[formKey];\n      } else {\n        this.validation[formKey] = validationResult;\n\n        return;\n      }\n\n      if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {\n        this.legend.set(formKey, coerce(newVal, formField.coerce));\n        if (path) {\n          this.openmct.objects.mutate(\n            this.domainObject,\n            path(this.domainObject, this.legend),\n            coerce(newVal, formField.coerce)\n          );\n        }\n      }\n    },\n    setStatus(status) {\n      this.status = status;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/forms/SeriesForm.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <ul>\n    <li class=\"c-tree__item menus-to-left\" :class=\"isAliasCss\" role=\"treeitem\">\n      <span\n        class=\"c-disclosure-triangle is-enabled flex-elem\"\n        :class=\"expandedCssClass\"\n        role=\"button\"\n        :aria-label=\"ariaLabelValue\"\n        tabindex=\"0\"\n        @click=\"toggleExpanded\"\n        @keydown.enter=\"toggleExpanded\"\n      >\n      </span>\n      <div :class=\"objectLabelCss\">\n        <div class=\"c-object-label__type-icon\" :class=\"seriesCss\">\n          <span class=\"is-status__indicator\" title=\"This item is missing or suspect\"></span>\n        </div>\n        <div class=\"c-object-label__name\">{{ series.domainObject.name }}</div>\n      </div>\n    </li>\n    <ul v-show=\"expanded\" class=\"grid-properties js-plot-options-edit-properties\">\n      <li class=\"grid-row\">\n        <!-- Value to be displayed -->\n        <div class=\"grid-cell label\" title=\"The field to be plotted as a value for this series.\">\n          Value\n        </div>\n        <div class=\"grid-cell value\">\n          <select v-model=\"yKey\" @change=\"updateForm('yKey')\">\n            <option v-for=\"option in yKeyOptions\" :key=\"option.value\" :value=\"option.value\">\n              {{ option.name }}\n            </option>\n          </select>\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"The rendering method to join lines for this series.\">\n          Line Method\n        </div>\n        <div class=\"grid-cell value\">\n          <select v-model=\"interpolate\" @change=\"updateForm('interpolate')\">\n            <option value=\"none\">None</option>\n            <option value=\"linear\">Linear interpolate</option>\n            <option value=\"stepAfter\">Step after</option>\n          </select>\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Whether markers are displayed.\">Markers</div>\n        <div class=\"grid-cell value\">\n          <input v-model=\"markers\" type=\"checkbox\" @change=\"updateForm('markers')\" />\n          <select v-show=\"markers\" v-model=\"markerShape\" @change=\"updateForm('markerShape')\">\n            <option\n              v-for=\"option in markerShapeOptions\"\n              :key=\"option.value\"\n              :value=\"option.value\"\n              :selected=\"option.value == markerShape\"\n            >\n              {{ option.name }}\n            </option>\n          </select>\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Display markers visually denoting points in alarm.\">\n          <label for=\"alarm-markers-checkbox\">Alarm Markers</label>\n        </div>\n        <div class=\"grid-cell value\">\n          <input\n            id=\"alarm-markers-checkbox\"\n            v-model=\"alarmMarkers\"\n            type=\"checkbox\"\n            @change=\"updateForm('alarmMarkers')\"\n          />\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div id=\"limit-lines-checkbox\" class=\"grid-cell label\" title=\"Display limit lines\">\n          Limit lines\n        </div>\n        <div class=\"grid-cell value\">\n          <input\n            v-model=\"limitLines\"\n            aria-labelledby=\"limit-lines-checkbox\"\n            type=\"checkbox\"\n            @change=\"updateForm('limitLines')\"\n          />\n        </div>\n      </li>\n      <li v-show=\"markers || alarmMarkers\" class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"The size of regular and alarm markers for this series.\">\n          Marker Size:\n        </div>\n        <div class=\"grid-cell value\">\n          <input\n            v-model=\"markerSize\"\n            class=\"c-input--flex\"\n            type=\"text\"\n            @change=\"updateForm('markerSize')\"\n          />\n        </div>\n      </li>\n      <li v-show=\"interpolate !== 'none' || markers\" class=\"grid-row\">\n        <ColorSwatch\n          :current-color=\"currentColor\"\n          edit-title=\"Manually set the plot line and marker color for this series.\"\n          view-title=\"The plot line and marker color for this series.\"\n          short-label=\"Color\"\n          @color-set=\"setColor\"\n        />\n      </li>\n    </ul>\n  </ul>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport ColorSwatch from '@/ui/color/ColorSwatch.vue';\n\nimport { MARKER_SHAPES } from '../../draw/MarkerShapes.js';\nimport { coerce, objectPath, validate } from './formUtil.js';\n\nexport default {\n  components: {\n    ColorSwatch\n  },\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    series: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['series-updated'],\n  data() {\n    return {\n      expanded: false,\n      markerShapeOptions: [],\n      yKey: this.series.get('yKey'),\n      yKeyOptions: [],\n      interpolate: this.series.get('interpolate'),\n      markers: this.series.get('markers'),\n      markerShape: this.series.get('markerShape'),\n      alarmMarkers: this.series.get('alarmMarkers'),\n      limitLines: this.series.get('limitLines'),\n      markerSize: this.series.get('markerSize'),\n      validation: {},\n      swatchActive: false,\n      status: null\n    };\n  },\n  computed: {\n    ariaLabelValue() {\n      const name = this.series.domainObject.name ? ` ${this.series.domainObject.name}` : '';\n\n      return `${this.expanded ? 'Collapse' : 'Expand'}${name} Plot Series Options`;\n    },\n    colorPalette() {\n      return this.series.collection.palette.groups();\n    },\n    objectLabelCss() {\n      return this.status ? `c-object-label is-status--${this.status}` : 'c-object-label';\n    },\n    seriesCss() {\n      const type = this.openmct.types.get(this.series.domainObject.type);\n      const baseClass = 'c-object-label__type-icon';\n      const typeClass = type.definition.cssClass || '';\n\n      return `${baseClass} ${typeClass}`.trim();\n    },\n    isAliasCss() {\n      let cssClass = '';\n      const domainObjectPath = [this.series.domainObject, ...this.path];\n      if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {\n        cssClass = 'is-alias';\n      }\n\n      return cssClass;\n    },\n    expandedCssClass() {\n      return this.expanded ? 'c-disclosure-triangle--expanded' : '';\n    },\n    currentColor() {\n      return this.series.get('color').asHexString();\n    }\n  },\n  created() {\n    this.initialize();\n\n    this.status = this.openmct.status.get(this.series.domainObject.identifier);\n    this.removeStatusListener = this.openmct.status.observe(\n      this.series.domainObject.identifier,\n      this.setStatus\n    );\n  },\n  beforeUnmount() {\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n  },\n  methods: {\n    initialize() {\n      this.fields = [\n        {\n          modelProp: 'yKey',\n          objectPath: this.dynamicPathForKey('yKey')\n        },\n        {\n          modelProp: 'interpolate',\n          objectPath: this.dynamicPathForKey('interpolate')\n        },\n        {\n          modelProp: 'markers',\n          objectPath: this.dynamicPathForKey('markers')\n        },\n        {\n          modelProp: 'markerShape',\n          objectPath: this.dynamicPathForKey('markerShape')\n        },\n        {\n          modelProp: 'markerSize',\n          coerce: Number,\n          objectPath: this.dynamicPathForKey('markerSize')\n        },\n        {\n          modelProp: 'alarmMarkers',\n          coerce: Boolean,\n          objectPath: this.dynamicPathForKey('alarmMarkers')\n        },\n        {\n          modelProp: 'limitLines',\n          coerce: Boolean,\n          objectPath: this.dynamicPathForKey('limitLines')\n        }\n      ];\n\n      const metadata = this.series.metadata;\n      this.yKeyOptions = metadata.valuesForHints(['range']).map(function (o) {\n        return {\n          name: o.key,\n          value: o.key\n        };\n      });\n      this.markerShapeOptions = Object.entries(MARKER_SHAPES).map(([key, obj]) => {\n        return {\n          name: obj.label,\n          value: key\n        };\n      });\n    },\n    dynamicPathForKey(key) {\n      return function (object, model) {\n        const modelIdentifier = model.get('identifier');\n        const index = object.configuration.series.findIndex((s) => {\n          return _.isEqual(s.identifier, modelIdentifier);\n        });\n\n        return 'configuration.series[' + index + '].' + key;\n      };\n    },\n    /**\n     * Set the color for the current plot series.  If the new color was\n     * already assigned to a different plot series, then swap the colors.\n     */\n    setColor: function (color) {\n      const oldColor = this.series.get('color');\n      const otherSeriesWithColor = this.series.collection.filter(function (s) {\n        return s.get('color') === color;\n      })[0];\n\n      this.series.set('color', color);\n\n      if (!this.domainObject.configuration || !this.domainObject.configuration.series) {\n        this.$emit('series-updated', {\n          identifier: this.domainObject.identifier,\n          path: `series.color`,\n          value: color.asHexString()\n        });\n      } else {\n        const getPath = this.dynamicPathForKey('color');\n        const seriesColorPath = getPath(this.domainObject, this.series);\n\n        this.openmct.objects.mutate(this.domainObject, seriesColorPath, color.asHexString());\n      }\n\n      if (otherSeriesWithColor) {\n        otherSeriesWithColor.set('color', oldColor);\n\n        if (!this.domainObject.configuration || !this.domainObject.configuration.series) {\n          this.$emit('series-updated', {\n            identifier: this.domainObject.identifier,\n            path: `series.color`,\n            value: oldColor.asHexString()\n          });\n        } else {\n          const getPath = this.dynamicPathForKey('color');\n          const otherSeriesColorPath = getPath(this.domainObject, otherSeriesWithColor);\n\n          this.openmct.objects.mutate(\n            this.domainObject,\n            otherSeriesColorPath,\n            oldColor.asHexString()\n          );\n        }\n      }\n    },\n    toggleExpanded() {\n      this.expanded = !this.expanded;\n    },\n    updateForm(formKey) {\n      const newVal = this[formKey];\n      const oldVal = this.series.get(formKey);\n      const formField = this.fields.find((field) => field.modelProp === formKey);\n\n      const path = objectPath(formField.objectPath);\n      const validationResult = validate(newVal, this.series, formField.validate);\n      if (validationResult === true) {\n        delete this.validation[formKey];\n      } else {\n        this.validation[formKey] = validationResult;\n\n        return;\n      }\n\n      if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {\n        this.series.set(formKey, coerce(newVal, formField.coerce));\n        if (path) {\n          if (!this.domainObject.configuration || !this.domainObject.configuration.series) {\n            this.$emit('series-updated', {\n              identifier: this.domainObject.identifier,\n              path: `series.${formKey}`,\n              value: coerce(newVal, formField.coerce)\n            });\n          } else {\n            this.openmct.objects.mutate(\n              this.domainObject,\n              path(this.domainObject, this.series),\n              coerce(newVal, formField.coerce)\n            );\n          }\n        }\n      }\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    toggleSwatch() {\n      this.swatchActive = !this.swatchActive;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/forms/YAxisForm.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div v-if=\"loaded\">\n    <ul\n      class=\"l-inspector-part\"\n      :aria-label=\"id > 1 ? `Y Axis ${id} Properties` : 'Y Axis Properties'\"\n    >\n      <h2>Y Axis {{ id > 1 ? id : '' }}</h2>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Manually override how the Y axis is labeled.\">\n          Label\n        </div>\n        <div class=\"grid-cell value\">\n          <label :for=\"`y-axis-${id}-label`\" class=\"visually-hidden\">Y Axis {{ id }} Label</label>\n          <input\n            :id=\"`y-axis-${id}-label`\"\n            v-model=\"label\"\n            class=\"c-input--flex\"\n            type=\"text\"\n            @change=\"updateForm('label')\"\n          />\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div :id=\"`log-mode-checkbox-${id}`\" class=\"grid-cell label\" title=\"Enable log mode.\">\n          Log mode\n        </div>\n        <div class=\"grid-cell value\">\n          <input\n            :id=\"`log-mode-input-${id}`\"\n            v-model=\"logMode\"\n            class=\"js-log-mode-input\"\n            type=\"checkbox\"\n            @change=\"updateForm('logMode')\"\n          />\n          <label :for=\"`log-mode-input-${id}`\" class=\"visually-hidden\"\n            >Y Axis {{ id }} Log mode</label\n          >\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div\n          :id=\"`autoscale-checkbox-${id}`\"\n          class=\"grid-cell label\"\n          title=\"Automatically scale the Y axis to keep all values in view.\"\n        >\n          Auto scale\n        </div>\n        <div class=\"grid-cell value\">\n          <input\n            :id=\"`autoscale-input-${id}`\"\n            v-model=\"autoscale\"\n            type=\"checkbox\"\n            @change=\"updateForm('autoscale')\"\n          />\n          <label :for=\"`autoscale-input-${id}`\" class=\"visually-hidden\"\n            >Y Axis {{ id }} Auto scale</label\n          >\n        </div>\n      </li>\n      <li v-show=\"autoscale\" class=\"grid-row\">\n        <div\n          class=\"grid-cell label\"\n          title=\"Percentage of padding above and below plotted min and max values. 0.1, 1.0, etc.\"\n        >\n          Padding\n        </div>\n        <div class=\"grid-cell value\">\n          <label :for=\"`autoscale-padding-${id}`\" class=\"visually-hidden\"\n            >Y Axis {{ id }} Autoscale Padding</label\n          >\n          <input\n            :id=\"`autoscale-padding-${id}`\"\n            v-model=\"autoscalePadding\"\n            class=\"c-input--flex\"\n            type=\"text\"\n            @change=\"updateForm('autoscalePadding')\"\n          />\n        </div>\n      </li>\n    </ul>\n    <ul v-show=\"!autoscale\" class=\"l-inspector-part\">\n      <div v-show=\"!autoscale && validationErrors.range\" class=\"grid-span-all form-error\">\n        {{ validationErrors.range }}\n      </div>\n      <li class=\"grid-row force-border\">\n        <div class=\"grid-cell label\" title=\"Minimum Y axis value.\">Minimum Value</div>\n        <div class=\"grid-cell value\">\n          <label :for=\"`range-min-${id}`\" class=\"visually-hidden\"\n            >Y Axis {{ id }} Minimum value</label\n          >\n          <input\n            :id=\"`range-min-${id}`\"\n            v-model=\"rangeMin\"\n            class=\"c-input--flex\"\n            type=\"number\"\n            @change=\"updateForm('range')\"\n          />\n        </div>\n      </li>\n      <li class=\"grid-row\">\n        <div class=\"grid-cell label\" title=\"Maximum Y axis value.\">Maximum Value</div>\n        <div class=\"grid-cell value\">\n          <label :for=\"`range-max-${id}`\" class=\"visually-hidden\"\n            >Y Axis {{ id }} Maximum value</label\n          >\n          <input\n            :id=\"`range-max-${id}`\"\n            v-model=\"rangeMax\"\n            class=\"c-input--flex\"\n            type=\"number\"\n            @change=\"updateForm('range')\"\n          />\n        </div>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport configStore from '../../configuration/ConfigStore.js';\nimport eventHelpers from '../../lib/eventHelpers.js';\nimport { objectPath } from './formUtil.js';\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    id: {\n      type: Number,\n      required: true\n    }\n  },\n  emits: ['series-updated'],\n  data() {\n    return {\n      yAxis: null,\n      label: '',\n      autoscale: '',\n      logMode: false,\n      autoscalePadding: '',\n      rangeMin: '',\n      rangeMax: '',\n      validationErrors: {},\n      loaded: false\n    };\n  },\n  beforeUnmount() {\n    if (this.autoscale === false && this.validationErrors.range) {\n      this.autoscale = true;\n      this.updateForm('autoscale');\n    }\n  },\n  mounted() {\n    eventHelpers.extend(this);\n    this.getConfig();\n    this.loaded = true;\n    this.initFields();\n    this.initFormValues();\n  },\n  methods: {\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        const mainYAxisId = config.yAxis.id;\n        this.isAdditionalYAxis = this.id !== mainYAxisId;\n        if (this.isAdditionalYAxis) {\n          this.additionalYAxes = config.additionalYAxes;\n          this.yAxis = config.additionalYAxes.find((yAxis) => yAxis.id === this.id);\n        } else {\n          this.yAxis = config.yAxis;\n        }\n      }\n    },\n    initFields() {\n      const prefix = `configuration.${this.getPrefix()}`;\n      this.fields = {\n        label: {\n          objectPath: `${prefix}.label`\n        },\n        autoscale: {\n          coerce: Boolean,\n          objectPath: `${prefix}.autoscale`\n        },\n        autoscalePadding: {\n          coerce: Number,\n          objectPath: `${prefix}.autoscalePadding`\n        },\n        logMode: {\n          coerce: Boolean,\n          objectPath: `${prefix}.logMode`\n        },\n        range: {\n          objectPath: `${prefix}.range`,\n          coerce: function coerceRange(range) {\n            const newRange = {};\n\n            if (range && typeof range.min !== 'undefined' && range.min !== null) {\n              newRange.min = Number(range.min);\n            }\n\n            if (range && typeof range.max !== 'undefined' && range.max !== null) {\n              newRange.max = Number(range.max);\n            }\n\n            return newRange;\n          },\n          validate: function validateRange(range, model) {\n            if (!range) {\n              return 'Need range';\n            }\n\n            if (range.min === '' || range.min === null || typeof range.min === 'undefined') {\n              return 'Must specify Minimum';\n            }\n\n            if (range.max === '' || range.max === null || typeof range.max === 'undefined') {\n              return 'Must specify Maximum';\n            }\n\n            if (Number.isNaN(Number(range.min))) {\n              return 'Minimum must be a number.';\n            }\n\n            if (Number.isNaN(Number(range.max))) {\n              return 'Maximum must be a number.';\n            }\n\n            if (Number(range.min) > Number(range.max)) {\n              return 'Minimum must be less than Maximum.';\n            }\n          }\n        }\n      };\n    },\n    initFormValues() {\n      this.label = this.yAxis.get('label');\n      this.autoscale = this.yAxis.get('autoscale');\n      this.logMode = this.yAxis.get('logMode');\n      this.autoscalePadding = this.yAxis.get('autoscalePadding');\n      const range = this.yAxis.get('range');\n      if (range && range.min !== undefined && range.max !== undefined) {\n        this.rangeMin = range.min;\n        this.rangeMax = range.max;\n      }\n    },\n    getPrefix() {\n      let prefix = 'yAxis';\n      if (this.isAdditionalYAxis) {\n        let index = -1;\n        if (this.domainObject?.configuration?.additionalYAxes) {\n          index = this.domainObject?.configuration?.additionalYAxes.findIndex((yAxis) => {\n            return yAxis.id === this.id;\n          });\n        }\n\n        if (index < 0) {\n          index = 0;\n        }\n\n        prefix = `additionalYAxes[${index}]`;\n      }\n\n      return prefix;\n    },\n    updateForm(formKey) {\n      let newVal;\n      if (formKey === 'range') {\n        newVal = {\n          min: this.rangeMin,\n          max: this.rangeMax\n        };\n      } else {\n        newVal = this[formKey];\n      }\n\n      let oldVal = this.yAxis.get(formKey);\n      const formField = this.fields[formKey];\n\n      const validationError = formField.validate?.(newVal, this.yAxis);\n      this.validationErrors[formKey] = validationError;\n      if (validationError) {\n        return;\n      }\n\n      newVal = formField.coerce?.(newVal) ?? newVal;\n      oldVal = formField.coerce?.(oldVal) ?? oldVal;\n\n      const path = objectPath(formField.objectPath);\n      if (!_.isEqual(newVal, oldVal)) {\n        // We mutate the model for the plots first PlotConfigurationModel - this triggers changes that affects the plot behavior\n        this.yAxis.set(formKey, newVal);\n        // Then we mutate the domain object configuration to persist the settings\n        if (path) {\n          if (this.isAdditionalYAxis) {\n            if (this.domainObject.configuration && this.domainObject.configuration.series) {\n              //update the id\n              this.openmct.objects.mutate(\n                this.domainObject,\n                `configuration.${this.getPrefix()}.id`,\n                this.id\n              );\n              //update the yAxes values\n              this.openmct.objects.mutate(\n                this.domainObject,\n                path(this.domainObject, this.yAxis),\n                newVal\n              );\n            } else {\n              this.$emit('series-updated', {\n                identifier: this.domainObject.identifier,\n                path: `${this.getPrefix()}.${formKey}`,\n                id: this.id,\n                value: newVal\n              });\n            }\n          } else {\n            if (this.domainObject.configuration && this.domainObject.configuration.series) {\n              this.openmct.objects.mutate(\n                this.domainObject,\n                path(this.domainObject, this.yAxis),\n                newVal\n              );\n            } else {\n              this.$emit('series-updated', {\n                identifier: this.domainObject.identifier,\n                path: `${this.getPrefix()}.${formKey}`,\n                value: newVal\n              });\n            }\n          }\n\n          //If autoscale is turned off, we must know what the user defined min and max ranges are\n          if (formKey === 'autoscale' && this.autoscale === false) {\n            const rangeFormField = this.fields.range;\n            this.validationErrors.range = rangeFormField.validate?.(\n              {\n                min: this.rangeMin,\n                max: this.rangeMax\n              },\n              this.yAxis\n            );\n          }\n        }\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/inspector/forms/formUtil.js",
    "content": "export function coerce(value, coerceFunc) {\n  if (coerceFunc) {\n    return coerceFunc(value);\n  }\n\n  return value;\n}\n\nexport function validate(value, model, validateFunc) {\n  if (validateFunc) {\n    return validateFunc(value, model);\n  }\n\n  return true;\n}\n\nexport function objectPath(path) {\n  return path && typeof path !== 'function' ? () => path : path;\n}\n"
  },
  {
    "path": "src/plugins/plot/legend/PlotLegend.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-plot-legend gl-plot-legend\"\n    :class=\"{\n      'hover-on-plot': !!highlights.length,\n      'is-legend-hidden': isLegendHidden\n    }\"\n  >\n    <button\n      class=\"c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled\"\n      :class=\"{ 'c-disclosure-triangle--expanded': isLegendExpanded }\"\n      :aria-label=\"ariaLabelValue\"\n      tabindex=\"0\"\n      @click=\"toggleLegend\"\n    ></button>\n\n    <div class=\"c-plot-legend__wrapper\" :class=\"{ 'is-cursor-locked': cursorLocked }\">\n      <!-- COLLAPSED PLOT LEGEND -->\n      <div\n        v-if=\"!isLegendExpanded\"\n        class=\"plot-wrapper-collapsed-legend\"\n        aria-label=\"Plot Legend Collapsed\"\n        :class=\"{ 'is-cursor-locked': cursorLocked }\"\n      >\n        <div\n          class=\"c-state-indicator__alert-cursor-lock icon-cursor-lock\"\n          title=\"Cursor is point locked. Click anywhere in the plot to unlock.\"\n        ></div>\n        <PlotLegendItemCollapsed\n          v-for=\"(seriesObject, seriesIndex) in seriesModels\"\n          :key=\"`${seriesObject.keyString}-${seriesIndex}-collapsed`\"\n          :highlights=\"highlights\"\n          :value-to-show-when-collapsed=\"valueToShowWhenCollapsed\"\n          :series-key-string=\"seriesObject.keyString\"\n          @legend-hover-changed=\"legendHoverChanged\"\n        />\n      </div>\n      <!-- EXPANDED PLOT LEGEND -->\n      <div\n        v-else\n        class=\"plot-wrapper-expanded-legend\"\n        aria-label=\"Plot Legend Expanded\"\n        :class=\"{ 'is-cursor-locked': cursorLocked }\"\n      >\n        <div\n          class=\"c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock\"\n          title=\"Click anywhere in the plot to unlock.\"\n        >\n          Cursor locked to point\n        </div>\n        <table>\n          <thead>\n            <tr>\n              <th>Name</th>\n              <th v-if=\"showTimestampWhenExpanded\">Timestamp</th>\n              <th v-if=\"showValueWhenExpanded\">Value</th>\n              <th v-if=\"showUnitsWhenExpanded\">Unit</th>\n              <th v-if=\"showMinimumWhenExpanded\" class=\"mobile-hide\">Min</th>\n              <th v-if=\"showMaximumWhenExpanded\" class=\"mobile-hide\">Max</th>\n            </tr>\n          </thead>\n          <tbody>\n            <PlotLegendItemExpanded\n              v-for=\"(seriesObject, seriesIndex) in seriesModels\"\n              :key=\"`${seriesObject.keyString}-${seriesIndex}-expanded`\"\n              :series-key-string=\"seriesObject.keyString\"\n              :highlights=\"highlights\"\n              @legend-hover-changed=\"legendHoverChanged\"\n            />\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </div>\n</template>\n<script>\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport PlotLegendItemCollapsed from './PlotLegendItemCollapsed.vue';\nimport PlotLegendItemExpanded from './PlotLegendItemExpanded.vue';\n\nexport default {\n  components: {\n    PlotLegendItemExpanded,\n    PlotLegendItemCollapsed\n  },\n  inject: ['openmct', 'domainObject'],\n  props: {\n    cursorLocked: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    highlights: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['legend-hover-changed', 'position', 'expanded'],\n  data() {\n    return {\n      isLegendExpanded: false,\n      seriesModels: [],\n      loaded: false\n    };\n  },\n  computed: {\n    ariaLabelValue() {\n      const name = this.domainObject.name ? ` ${this.domainObject.name}` : '';\n\n      return `${this.isLegendExpanded ? 'Collapse' : 'Expand'}${name} Legend`;\n    },\n    showUnitsWhenExpanded() {\n      return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;\n    },\n    showMinimumWhenExpanded() {\n      return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;\n    },\n    showMaximumWhenExpanded() {\n      return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;\n    },\n    showValueWhenExpanded() {\n      return this.loaded && this.legend.get('showValueWhenExpanded') === true;\n    },\n    showTimestampWhenExpanded() {\n      return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;\n    },\n    isLegendHidden() {\n      return this.loaded && this.legend.get('hideLegendWhenSmall') === true;\n    },\n    valueToShowWhenCollapsed() {\n      return this.loaded && this.legend.get('valueToShowWhenCollapsed');\n    }\n  },\n  created() {\n    eventHelpers.extend(this);\n    this.config = this.getConfig();\n    this.legend = this.config.legend;\n    this.seriesModels = [];\n    this.listenTo(this.config.legend, 'change:position', this.updatePosition, this);\n\n    if (this.domainObject.type === 'telemetry.plot.stacked') {\n      this.objectComposition = this.openmct.composition.get(this.domainObject);\n      this.objectComposition.on('add', this.addTelemetryObject);\n      this.objectComposition.on('remove', this.removeTelemetryObject);\n      this.objectComposition.load();\n    } else {\n      this.registerListeners(this.config);\n    }\n    this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);\n  },\n  mounted() {\n    this.loaded = true;\n    this.isLegendExpanded = this.legend.get('expanded') === true;\n    this.$emit('expanded', this.isLegendExpanded);\n    this.updatePosition();\n  },\n  beforeUnmount() {\n    if (this.objectComposition) {\n      this.objectComposition.off('add', this.addTelemetryObject);\n      this.objectComposition.off('remove', this.removeTelemetryObject);\n    }\n\n    this.stopListening();\n  },\n  methods: {\n    changeExpandDefault() {\n      this.isLegendExpanded = this.config.legend.model.expandByDefault;\n      this.legend.set('expanded', this.isLegendExpanded);\n      this.$emit('expanded', this.isLegendExpanded);\n    },\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      return configStore.get(configId);\n    },\n    addTelemetryObject(object) {\n      //get the config for each child\n      const configId = this.openmct.objects.makeKeyString(object.identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        this.registerListeners(config);\n      }\n    },\n    removeTelemetryObject(identifier) {\n      const configId = this.openmct.objects.makeKeyString(identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        config.series.forEach(this.removeSeries, this);\n      }\n    },\n    registerListeners(config) {\n      //listen to any changes to the telemetry endpoints that are associated with the child\n      this.listenTo(config.series, 'add', this.addSeries, this);\n      this.listenTo(config.series, 'remove', this.removeSeries, this);\n      config.series.forEach(this.addSeries, this);\n    },\n    addSeries(series) {\n      const existingSeries = this.getSeries(series.keyString);\n      if (existingSeries) {\n        return;\n      }\n      this.seriesModels.push(series);\n    },\n    removeSeries(plotSeries) {\n      this.stopListening(plotSeries);\n\n      const seriesIndex = this.seriesModels.findIndex(\n        (series) => series.keyString === plotSeries.keyString\n      );\n      this.seriesModels.splice(seriesIndex, 1);\n    },\n    getSeries(keyStringToFind) {\n      const foundSeries = this.seriesModels.find((series) => {\n        return series.keyString === keyStringToFind;\n      });\n      return foundSeries;\n    },\n    toggleLegend() {\n      this.isLegendExpanded = !this.isLegendExpanded;\n      this.legend.set('expanded', this.isLegendExpanded);\n      this.$emit('expanded', this.isLegendExpanded);\n    },\n    legendHoverChanged(data) {\n      this.$emit('legend-hover-changed', data);\n    },\n    updatePosition() {\n      this.$emit('position', this.legend.get('position'));\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/legend/PlotLegendItemCollapsed.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"plot-legend-item\"\n    :aria-label=\"`Plot Legend Item for ${seriesName}`\"\n    :class=\"{\n      'is-stale': isStale,\n      'is-status--missing': isMissing\n    }\"\n    @mouseover=\"toggleHover(true)\"\n    @mouseleave=\"toggleHover(false)\"\n  >\n    <div\n      ref=\"series\"\n      class=\"plot-series-swatch-and-name\"\n      @mouseover.ctrl=\"showToolTip\"\n      @mouseleave=\"hideToolTip\"\n    >\n      <span class=\"plot-series-color-swatch\" :style=\"{ 'background-color': colorAsHexString }\" />\n      <span class=\"is-status__indicator\" title=\"This item is missing or suspect\" />\n      <span class=\"plot-series-name\">{{ nameWithUnit }}</span>\n    </div>\n    <div\n      v-show=\"\n        !!highlights.length &&\n        valueToShowWhenCollapsed !== 'none' &&\n        valueToShowWhenCollapsed !== 'unit'\n      \"\n      class=\"plot-series-value hover-value-enabled\"\n      :class=\"[\n        { 'cursor-hover': notNearest },\n        valueToDisplayWhenCollapsedClass,\n        mctLimitStateClass\n      ]\"\n    >\n      <span v-if=\"valueToShowWhenCollapsed === 'nearestValue'\">{{ formattedYValue }}</span>\n      <span v-else-if=\"valueToShowWhenCollapsed === 'nearestTimestamp'\">{{ formattedXValue }}</span>\n      <span v-else>{{ formattedYValueFromStats }}</span>\n    </div>\n  </div>\n</template>\n<script>\nimport { getLimitClass } from '@/plugins/plot/chart/limitUtil';\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport configStore from '../configuration/ConfigStore.js';\nimport eventHelpers from '../lib/eventHelpers.js';\n\nexport default {\n  mixins: [stalenessMixin, tooltipHelpers],\n  inject: ['openmct', 'domainObject'],\n  props: {\n    seriesKeyString: {\n      type: String,\n      required: true\n    },\n    highlights: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['legend-hover-changed'],\n  data() {\n    return {\n      isMissing: false,\n      colorAsHexString: '',\n      nameWithUnit: '',\n      seriesName: '',\n      formattedYValue: '',\n      formattedXValue: '',\n      mctLimitStateClass: '',\n      formattedYValueFromStats: '',\n      loaded: false\n    };\n  },\n  computed: {\n    valueToShowWhenCollapsed() {\n      return this.loaded ? this.legend.get('valueToShowWhenCollapsed') : [];\n    },\n    valueToDisplayWhenCollapsedClass() {\n      return `value-to-display-${this.valueToShowWhenCollapsed}`;\n    },\n    notNearest() {\n      return this.valueToShowWhenCollapsed.indexOf('nearest') > -1;\n    }\n  },\n  watch: {\n    highlights: {\n      handler(newHighlights) {\n        const highlightedObject = newHighlights.find(\n          (highlight) => highlight.seriesKeyString === this.seriesKeyString\n        );\n        if (newHighlights.length === 0 || highlightedObject) {\n          this.initialize(highlightedObject);\n        }\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.seriesModels = [];\n    eventHelpers.extend(this);\n    this.config = this.getConfig();\n    if (this.domainObject.type === 'telemetry.plot.stacked') {\n      this.objectComposition = this.openmct.composition.get(this.domainObject);\n      this.objectComposition.on('add', this.addTelemetryObject);\n      this.objectComposition.on('remove', this.removeTelemetryObject);\n      this.objectComposition.load();\n    } else {\n      this.registerListeners(this.config);\n    }\n    this.legend = this.config.legend;\n    this.loaded = true;\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  beforeUnmount() {\n    this.stopListening();\n\n    if (this.objectComposition) {\n      this.objectComposition.off('add', this.addTelemetryObject);\n      this.objectComposition.off('remove', this.removeTelemetryObject);\n    }\n  },\n  methods: {\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      return configStore.get(configId);\n    },\n    registerListeners(config) {\n      //listen to any changes to the telemetry endpoints that are associated with the child\n      this.listenTo(config.series, 'add', this.onSeriesAdd, this);\n      this.listenTo(config.series, 'remove', this.onSeriesRemove, this);\n      config.series.forEach(this.onSeriesAdd, this);\n    },\n    addTelemetryObject(object) {\n      //get the config for each child\n      const configId = this.openmct.objects.makeKeyString(object.identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        this.registerListeners(config);\n      }\n    },\n    removeTelemetryObject(identifier) {\n      const configId = this.openmct.objects.makeKeyString(identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        config.series.forEach(this.onSeriesRemove, this);\n      }\n    },\n    onSeriesAdd(series) {\n      if (series.keyString !== this.seriesKeyString) {\n        return;\n      }\n      const existingSeries = this.getSeries(series.keyString);\n      if (existingSeries) {\n        return;\n      }\n      this.seriesModels.push(series);\n      this.listenTo(\n        series,\n        'change:color',\n        (newColor) => {\n          this.updateColor(newColor);\n        },\n        this\n      );\n      this.listenTo(\n        series,\n        'change:name',\n        () => {\n          this.updateName();\n        },\n        this\n      );\n      this.subscribeToStaleness(series.domainObject);\n      this.initialize();\n    },\n    onSeriesRemove(seriesToRemove) {\n      const seriesIndexToRemove = this.seriesModels.findIndex(\n        (series) => series.keyString === seriesToRemove.keyString\n      );\n      if (seriesIndexToRemove === -1) {\n        return;\n      }\n      this.seriesModels.splice(seriesIndexToRemove, 1);\n    },\n    getSeries(keyStringToFind) {\n      const foundSeries = this.seriesModels.find((series) => {\n        return series.keyString === keyStringToFind;\n      });\n      return foundSeries;\n    },\n    initialize(highlightedObject) {\n      const seriesKeyStringToUse = highlightedObject?.seriesKeyString || this.seriesKeyString;\n      const seriesObject = this.getSeries(seriesKeyStringToUse);\n\n      this.isMissing = seriesObject.domainObject.status === 'missing';\n      this.colorAsHexString = seriesObject.get('color').asHexString();\n      this.seriesName = seriesObject.domainObject.name;\n      this.nameWithUnit = seriesObject.nameWithUnit();\n\n      const closest = seriesObject.closest;\n      if (closest) {\n        this.formattedYValue = seriesObject.formatY(closest);\n        this.formattedXValue = seriesObject.formatX(closest);\n        this.mctLimitStateClass = closest.mctLimitState\n          ? getLimitClass(closest.mctLimitState, 'c-plot-limit--')\n          : '';\n      } else {\n        this.formattedYValue = '';\n        this.formattedXValue = '';\n        this.mctLimitStateClass = '';\n      }\n\n      const stats = seriesObject.get('stats');\n      if (stats) {\n        this.formattedYValueFromStats = seriesObject.formatY(\n          stats[this.valueToShowWhenCollapsed + 'Point']\n        );\n      } else {\n        this.formattedYValueFromStats = '';\n      }\n    },\n    updateColor(newColor) {\n      this.colorAsHexString = newColor.asHexString();\n    },\n    updateName() {\n      const seriesObject = this.getSeries(this.seriesKeyString);\n      this.nameWithUnit = seriesObject.nameWithUnit();\n    },\n    toggleHover(hover) {\n      this.hover = hover;\n      this.$emit(\n        'legend-hover-changed',\n        this.hover\n          ? {\n              seriesKey: this.seriesKeyString\n            }\n          : undefined\n      );\n    },\n    async showToolTip() {\n      const seriesObject = this.getSeries(this.seriesKeyString);\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(\n        await this.getTelemetryPathString(seriesObject.domainObject.identifier),\n        BELOW,\n        'series'\n      );\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/legend/PlotLegendItemExpanded.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <tr\n    class=\"plot-legend-item\"\n    :aria-label=\"`Plot Legend Item for ${domainObject?.name}`\"\n    :class=\"{\n      'is-stale': isStale,\n      'is-status--missing': isMissing\n    }\"\n    @mouseover=\"toggleHover(true)\"\n    @mouseleave=\"toggleHover(false)\"\n  >\n    <td class=\"plot-series-swatch-and-name\">\n      <span class=\"plot-series-color-swatch\" :style=\"{ 'background-color': colorAsHexString }\">\n      </span>\n      <span class=\"is-status__indicator\" title=\"This item is missing or suspect\"></span>\n      <span\n        ref=\"seriesName\"\n        class=\"plot-series-name\"\n        @mouseover.ctrl=\"showToolTip\"\n        @mouseleave=\"hideToolTip\"\n      >\n        {{ name }}\n      </span>\n    </td>\n\n    <td v-if=\"showTimestampWhenExpanded\">\n      <span class=\"plot-series-value cursor-hover hover-value-enabled\">\n        {{ formattedXValue }}\n      </span>\n    </td>\n    <td v-if=\"showValueWhenExpanded\">\n      <span\n        class=\"plot-series-value cursor-hover hover-value-enabled\"\n        :class=\"[mctLimitStateClass]\"\n      >\n        {{ formattedYValue }}\n      </span>\n    </td>\n    <td v-if=\"showUnitsWhenExpanded\">\n      <span class=\"plot-series-value cursor-hover hover-value-enabled\">\n        {{ unit }}\n      </span>\n    </td>\n    <td v-if=\"showMinimumWhenExpanded\" class=\"mobile-hide\">\n      <span class=\"plot-series-value\">\n        {{ formattedMinY }}\n      </span>\n    </td>\n    <td v-if=\"showMaximumWhenExpanded\" class=\"mobile-hide\">\n      <span class=\"plot-series-value\">\n        {{ formattedMaxY }}\n      </span>\n    </td>\n  </tr>\n</template>\n\n<script>\nimport { getLimitClass } from '@/plugins/plot/chart/limitUtil';\nimport eventHelpers from '@/plugins/plot/lib/eventHelpers';\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport configStore from '../configuration/ConfigStore.js';\n\nexport default {\n  mixins: [stalenessMixin, tooltipHelpers],\n  inject: ['openmct', 'domainObject'],\n  props: {\n    seriesKeyString: {\n      type: String,\n      required: true\n    },\n    highlights: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  emits: ['legend-hover-changed'],\n  data() {\n    return {\n      isMissing: false,\n      colorAsHexString: '',\n      name: '',\n      nameWithUnit: '',\n      unit: '',\n      formattedYValue: '',\n      formattedXValue: '',\n      formattedMinY: '',\n      formattedMaxY: '',\n      mctLimitStateClass: '',\n      loaded: false\n    };\n  },\n  computed: {\n    showUnitsWhenExpanded() {\n      return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;\n    },\n    showMinimumWhenExpanded() {\n      return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;\n    },\n    showMaximumWhenExpanded() {\n      return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;\n    },\n    showValueWhenExpanded() {\n      return this.loaded && this.legend.get('showValueWhenExpanded') === true;\n    },\n    showTimestampWhenExpanded() {\n      return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;\n    }\n  },\n  watch: {\n    highlights: {\n      handler(newHighlights) {\n        const highlightedObject = newHighlights.find(\n          (highlight) => highlight.seriesKeyString === this.seriesKeyString\n        );\n        if (newHighlights.length === 0 || highlightedObject) {\n          this.initialize(highlightedObject);\n        }\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.seriesModels = [];\n    eventHelpers.extend(this);\n    this.config = this.getConfig();\n\n    if (this.domainObject.type === 'telemetry.plot.stacked') {\n      this.objectComposition = this.openmct.composition.get(this.domainObject);\n      this.objectComposition.on('add', this.addTelemetryObject);\n      this.objectComposition.on('remove', this.removeTelemetryObject);\n      this.objectComposition.load();\n    } else {\n      this.registerListeners(this.config);\n    }\n\n    this.legend = this.config.legend;\n    this.loaded = true;\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  beforeUnmount() {\n    this.stopListening();\n\n    if (this.objectComposition) {\n      this.objectComposition.off('add', this.addTelemetryObject);\n      this.objectComposition.off('remove', this.removeTelemetryObject);\n    }\n  },\n  methods: {\n    registerListeners(config) {\n      //listen to any changes to the telemetry endpoints that are associated with the child\n      this.listenTo(config.series, 'add', this.onSeriesAdd, this);\n      this.listenTo(config.series, 'remove', this.onSeriesRemove, this);\n      config.series.forEach(this.onSeriesAdd, this);\n    },\n    addTelemetryObject(object) {\n      //get the config for each child\n      const configId = this.openmct.objects.makeKeyString(object.identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        this.registerListeners(config);\n      }\n    },\n    removeTelemetryObject(identifier) {\n      const configId = this.openmct.objects.makeKeyString(identifier);\n      const config = configStore.get(configId);\n      if (config) {\n        config.series.forEach(this.onSeriesRemove, this);\n      }\n    },\n    onSeriesAdd(series) {\n      if (series.keyString !== this.seriesKeyString) {\n        return;\n      }\n      const existingSeries = this.getSeries(series.keyString);\n      if (existingSeries) {\n        return;\n      }\n      this.seriesModels.push(series);\n      this.listenTo(\n        series,\n        'change:color',\n        (newColor) => {\n          this.updateColor(newColor);\n        },\n        this\n      );\n      this.listenTo(\n        series,\n        'change:name',\n        () => {\n          this.updateName();\n        },\n        this\n      );\n      this.subscribeToStaleness(series.domainObject);\n      this.initialize();\n    },\n    onSeriesRemove(seriesToRemove) {\n      const seriesIndexToRemove = this.seriesModels.findIndex(\n        (series) => series.keyString === seriesToRemove.keyString\n      );\n      this.seriesModels.splice(seriesIndexToRemove, 1);\n    },\n    getSeries(keyStringToFind) {\n      const foundSeries = this.seriesModels.find((series) => {\n        return series.keyString === keyStringToFind;\n      });\n      return foundSeries;\n    },\n    getConfig() {\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      return configStore.get(configId);\n    },\n    initialize(highlightedObject) {\n      const seriesKeyStringToUse = highlightedObject?.seriesKeyString || this.seriesKeyString;\n      const seriesObject = this.getSeries(seriesKeyStringToUse);\n\n      this.isMissing = seriesObject.domainObject.status === 'missing';\n      this.colorAsHexString = seriesObject.get('color').asHexString();\n      this.name = seriesObject.get('name');\n      this.unit = seriesObject.get('unit');\n      const closest = seriesObject.closest;\n      if (closest) {\n        this.formattedYValue = seriesObject.formatY(closest);\n        this.formattedXValue = seriesObject.formatX(closest);\n        this.mctLimitStateClass = seriesObject.closest.mctLimitState\n          ? getLimitClass(seriesObject.closest.mctLimitState, 'c-plot-limit--')\n          : '';\n      } else {\n        this.formattedYValue = '';\n        this.formattedXValue = '';\n        this.mctLimitStateClass = '';\n      }\n\n      const stats = seriesObject.get('stats');\n      if (stats) {\n        this.formattedMinY = seriesObject.formatY(stats.minPoint);\n        this.formattedMaxY = seriesObject.formatY(stats.maxPoint);\n      } else {\n        this.formattedMinY = '';\n        this.formattedMaxY = '';\n      }\n    },\n    updateColor(newColor) {\n      this.colorAsHexString = newColor.asHexString();\n    },\n    updateName() {\n      const seriesObject = this.getSeries(this.seriesKeyString);\n      this.nameWithUnit = seriesObject.nameWithUnit();\n    },\n    toggleHover(hover) {\n      this.hover = hover;\n      this.$emit('legend-hover-changed', {\n        seriesKey: this.hover ? this.seriesKeyString : ''\n      });\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      const seriesIdentifier = this.openmct.objects.parseKeyString(this.seriesKeyString);\n      this.buildToolTip(await this.getTelemetryPathString(seriesIdentifier), BELOW, 'seriesName');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/lib/eventHelpers.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @type {EventHelpers}\n */\nconst helperFunctions = {\n  listenTo: function (object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    const listener = {\n      object: object,\n      event: event,\n      callback: callback,\n      context: context,\n      _cb: context ? callback.bind(context) : callback\n    };\n    if (object.addEventListener) {\n      object.addEventListener(event, listener._cb);\n    } else {\n      object.on(event, listener._cb, listener.context);\n    }\n\n    this._listeningTo.push(listener);\n  },\n\n  stopListening: function (object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    this._listeningTo\n      .filter(function (listener) {\n        if (object && object !== listener.object) {\n          return false;\n        }\n\n        if (event && event !== listener.event) {\n          return false;\n        }\n\n        if (callback && callback !== listener.callback) {\n          return false;\n        }\n\n        if (context && context !== listener.context) {\n          return false;\n        }\n\n        return true;\n      })\n      .map(function (listener) {\n        if (listener.unlisten) {\n          listener.unlisten();\n        } else if (listener.object.removeEventListener) {\n          listener.object.removeEventListener(listener.event, listener._cb);\n        } else {\n          listener.object.off(listener.event, listener._cb, listener.context);\n        }\n\n        return listener;\n      })\n      .forEach(function (listener) {\n        this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);\n      }, this);\n  },\n\n  extend: function (object) {\n    object.listenTo = helperFunctions.listenTo;\n    object.stopListening = helperFunctions.stopListening;\n  }\n};\n\nexport default helperFunctions;\n\n/**\n@typedef {{\n    listenTo: (object: any, event: any, callback: any, context: any) => void,\n    stopListening: (object: any, event: any, callback: any, context: any) => void\n}} EventHelpers\n*/\n"
  },
  {
    "path": "src/plugins/plot/mathUtils.js",
    "content": "/** The natural number `e`. */\nexport const e = Math.exp(1);\n\n/**\nReturns the logarithm of a number, using the given base or the natural number\n`e` as base if not specified.\n@param {number} n\n@param {number=} base log base, defaults to e\n*/\nexport function log(n, base = e) {\n  if (base === e) {\n    return Math.log(n);\n  }\n\n  return Math.log(n) / Math.log(base);\n}\n\n/**\nReturns the inverse of the logarithm of a number, using the given base or the\nnatural number `e` as base if not specified.\n@param {number} n\n@param {number=} base log base, defaults to e\n*/\nexport function antilog(n, base = e) {\n  return Math.pow(base, n);\n}\n\n/**\nA symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258\n@param {number} n\n@param {number=} base log base, defaults to e\n*/\nexport function symlog(n, base = e) {\n  return Math.sign(n) * log(Math.abs(n) + 1, base);\n}\n\n/**\nAn inverse symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258\n@param {number} n\n@param {number=} base log base, defaults to e\n*/\nexport function antisymlog(n, base = e) {\n  return Math.sign(n) * (antilog(Math.abs(n), base) - 1);\n}\n"
  },
  {
    "path": "src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js",
    "content": "export default function OverlayPlotCompositionPolicy(openmct) {\n  return {\n    allow: function (parent, child) {\n      if (\n        parent.type === 'telemetry.plot.overlay' &&\n        !openmct.telemetry.hasNumericTelemetry(child)\n      ) {\n        return false;\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport Plot from '../PlotView.vue';\n\nexport default function OverlayPlotViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: 'plot-overlay',\n    name: 'Overlay Plot',\n    cssClass: 'icon-telemetry',\n    canView(domainObject, objectPath) {\n      return domainObject.type === 'telemetry.plot.overlay';\n    },\n\n    canEdit(domainObject, objectPath) {\n      return domainObject.type === 'telemetry.plot.overlay';\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show: function (element, isEditing, { renderWhenVisible }) {\n          let isCompact = isCompactView(objectPath);\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                Plot\n              },\n              provide: {\n                openmct,\n                domainObject,\n                objectPath,\n                renderWhenVisible\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact\n                  }\n                };\n              },\n              template: '<plot ref=\"plotComponent\" :options=\"options\"></plot>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n        getViewContext() {\n          if (!component) {\n            return {};\n          }\n\n          return component.$refs.plotComponent.getViewContext();\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/overlayPlot/overlayPlotStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function overlayPlotStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return (\n        domainObject?.type === 'telemetry.plot.overlay' &&\n        !domainObject?.configuration?.objectStyles\n      );\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      domainObject.configuration.objectStyles = {};\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/overlayPlot/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\nimport {\n  createMouseEvent,\n  createOpenMct,\n  renderWhenVisible,\n  resetApplicationState,\n  spyOnBuiltins\n} from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport configStore from '../configuration/ConfigStore.js';\nimport PlotOptions from '../inspector/PlotOptions.vue';\nimport Plot from '../PlotView.vue';\nimport PlotVuePlugin from '../plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let mockObjectPath;\n  let overlayPlotObject = {\n    identifier: {\n      namespace: '',\n      key: 'test-plot'\n    },\n    type: 'telemetry.plot.overlay',\n    name: 'Test Overlay Plot',\n    composition: [],\n    configuration: {\n      series: []\n    }\n  };\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'time-strip',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    const testTelemetry = [\n      {\n        utc: 1,\n        'some-key': 'some-value 1',\n        'some-other-key': 'some-other-value 1',\n        'some-key2': 'some-value2 1',\n        'some-other-key2': 'some-other-value2 1'\n      },\n      {\n        utc: 2,\n        'some-key': 'some-value 2',\n        'some-other-key': 'some-other-value 2',\n        'some-key2': 'some-value2 2',\n        'some-other-key2': 'some-other-value2 2'\n      },\n      {\n        utc: 3,\n        'some-key': 'some-value 3',\n        'some-other-key': 'some-other-value 3',\n        'some-key2': 'some-value2 2',\n        'some-other-key2': 'some-other-value2 2'\n      }\n    ];\n\n    const timeSystem = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 0,\n        end: 4\n      }\n    };\n\n    openmct = createOpenMct(timeSystem);\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(testTelemetry);\n\n      return telemetryPromise;\n    });\n\n    openmct.install(new PlotVuePlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n    document.body.appendChild(element);\n\n    spyOn(window, 'ResizeObserver').and.returnValue({\n      observe() {},\n      unobserve() {},\n      disconnect() {}\n    });\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    spyOnBuiltins(['requestAnimationFrame']);\n    window.requestAnimationFrame.and.callFake((callBack) => {\n      callBack();\n    });\n\n    openmct.router.path = [overlayPlotObject];\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n    configStore.deleteAll();\n    await resetApplicationState(openmct);\n  });\n\n  afterAll(() => {\n    openmct.router.path = null;\n  });\n\n  describe('the plot views', () => {\n    it('provides an overlay plot view for objects with telemetry', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'telemetry.plot.overlay',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key'\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay');\n      expect(plotView).toBeDefined();\n    });\n  });\n\n  describe('The overlay plot view with multiple axes', () => {\n    let testTelemetryObject;\n    let testTelemetryObject2;\n    let config;\n    let mockComposition;\n    let destroyPlot;\n\n    afterAll(() => {\n      destroyPlot();\n      openmct.router.path = null;\n    });\n\n    beforeEach(async () => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      testTelemetryObject2 = {\n        identifier: {\n          namespace: '',\n          key: 'test-object2'\n        },\n        type: 'test-object',\n        name: 'Test Object2',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key2',\n              name: 'Some attribute2',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key2',\n              name: 'Another attribute2',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n      overlayPlotObject.composition = [\n        {\n          identifier: testTelemetryObject.identifier\n        },\n        {\n          identifier: testTelemetryObject2.identifier\n        }\n      ];\n      overlayPlotObject.configuration.series = [\n        {\n          identifier: testTelemetryObject.identifier,\n          yAxisId: 1\n        },\n        {\n          identifier: testTelemetryObject2.identifier,\n          yAxisId: 3\n        }\n      ];\n      overlayPlotObject.configuration.additionalYAxes = [\n        {\n          label: 'Test Object Label',\n          id: 2\n        },\n        {\n          label: 'Test Object 2 Label',\n          id: 3\n        }\n      ];\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testTelemetryObject);\n        mockComposition.emit('add', testTelemetryObject2);\n\n        return [testTelemetryObject, testTelemetryObject2];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      let viewContainer = document.createElement('div');\n      child.appendChild(viewContainer);\n      const composition = openmct.composition.get(overlayPlotObject);\n      const { destroy } = mount(\n        {\n          components: {\n            Plot\n          },\n          provide: {\n            openmct,\n            domainObject: overlayPlotObject,\n            composition,\n            objectPath: [overlayPlotObject],\n            renderWhenVisible\n          },\n          template: '<plot ref=\"plotComponent\"></plot>'\n        },\n        {\n          element: child\n        }\n      );\n\n      destroyPlot = destroy;\n\n      await telemetryPromise;\n      await nextTick();\n\n      const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);\n      config = configStore.get(configId);\n    });\n\n    it('Renders multiple Y-axis for the telemetry objects', async () => {\n      config.yAxis.set('displayRange', {\n        min: 10,\n        max: 20\n      });\n      await nextTick();\n      let yAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper'\n      );\n      expect(yAxisElement.length).toBe(2);\n    });\n\n    describe('the inspector view', () => {\n      let inspectorComponent;\n      let viewComponentObject;\n      let selection;\n      let destroyPlotOptions;\n      beforeEach(async () => {\n        selection = [\n          [\n            {\n              context: {\n                item: {\n                  id: overlayPlotObject.identifier.key,\n                  identifier: overlayPlotObject.identifier,\n                  type: overlayPlotObject.type,\n                  configuration: overlayPlotObject.configuration,\n                  composition: overlayPlotObject.composition\n                }\n              }\n            }\n          ]\n        ];\n\n        const viewContainer = document.createElement('div');\n        child.appendChild(viewContainer);\n        const { vNode, destroy } = mount(\n          {\n            components: {\n              PlotOptions\n            },\n            provide: {\n              openmct,\n              domainObject: selection[0][0].context.item,\n              path: [selection[0][0].context.item]\n            },\n            template: '<plot-options ref=\"plotOptionsRef\"/>'\n          },\n          {\n            element: viewContainer\n          }\n        );\n        inspectorComponent = vNode.componentInstance;\n        destroyPlotOptions = destroy;\n\n        await nextTick();\n        viewComponentObject = inspectorComponent.$refs.plotOptionsRef;\n      });\n\n      afterEach(() => {\n        destroyPlotOptions();\n        openmct.router.path = null;\n      });\n\n      describe('in edit mode', () => {\n        let editOptionsEl;\n\n        beforeEach(async () => {\n          viewComponentObject.setEditState(true);\n          await nextTick();\n          editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');\n        });\n\n        it('shows multiple yAxis options', () => {\n          const yAxisProperties = editOptionsEl.querySelectorAll(\n            '.js-yaxis-grid-properties .l-inspector-part h2'\n          );\n          expect(yAxisProperties.length).toEqual(2);\n        });\n\n        it('saves yAxis options', () => {\n          //toggle log mode and save\n          config.additionalYAxes[1].set('displayRange', {\n            min: 10,\n            max: 20\n          });\n          const yAxisProperties = editOptionsEl.querySelectorAll('.js-log-mode-input');\n          const clickEvent = createMouseEvent('click');\n          yAxisProperties[1].dispatchEvent(clickEvent);\n\n          expect(config.additionalYAxes[1].get('logMode')).toEqual(true);\n        });\n      });\n    });\n  });\n\n  describe('The overlay plot view with single axes', () => {\n    let testTelemetryObject;\n    let config;\n    let mockComposition;\n    let destroyOverlayPlot;\n\n    afterAll(() => {\n      destroyOverlayPlot();\n      openmct.router.path = null;\n    });\n\n    beforeEach(async () => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      overlayPlotObject.composition = [\n        {\n          identifier: testTelemetryObject.identifier\n        }\n      ];\n      overlayPlotObject.configuration.series = [\n        {\n          identifier: testTelemetryObject.identifier\n        }\n      ];\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testTelemetryObject);\n\n        return [testTelemetryObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n      const composition = openmct.composition.get(overlayPlotObject);\n      const viewContainer = document.createElement('div');\n      child.appendChild(viewContainer);\n      const { destroy } = mount(\n        {\n          components: {\n            Plot\n          },\n          provide: {\n            openmct: openmct,\n            domainObject: overlayPlotObject,\n            composition,\n            objectPath: [overlayPlotObject],\n            renderWhenVisible\n          },\n          template: '<plot ref=\"plotComponent\"></plot>'\n        },\n        {\n          element: viewContainer\n        }\n      );\n\n      destroyOverlayPlot = destroy;\n\n      await telemetryPromise;\n      await nextTick();\n      const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);\n      config = configStore.get(configId);\n    });\n\n    it('Renders single Y-axis for the telemetry object', async () => {\n      config.yAxis.set('displayRange', {\n        min: 10,\n        max: 20\n      });\n      await nextTick();\n      let yAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper'\n      );\n      expect(yAxisElement.length).toBe(1);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/plot/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport PlotViewActions from './actions/ViewActions.js';\nimport PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider.js';\nimport StackedPlotsInspectorViewProvider from './inspector/StackedPlotsInspectorViewProvider.js';\nimport OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy.js';\nimport overlayPlotStylesInterceptor from './overlayPlot/overlayPlotStylesInterceptor.js';\nimport OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider.js';\nimport PlotViewProvider from './PlotViewProvider.js';\nimport StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy.js';\nimport stackedPlotConfigurationInterceptor from './stackedPlot/stackedPlotConfigurationInterceptor.js';\nimport StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.types.addType('telemetry.plot.overlay', {\n      key: 'telemetry.plot.overlay',\n      name: 'Overlay Plot',\n      cssClass: 'icon-plot-overlay',\n      description:\n        'Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.',\n      creatable: true,\n      annotatable: true,\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}}\n          series: [],\n          objectStyles: {}\n        };\n      },\n      priority: 891\n    });\n\n    openmct.objects.addGetInterceptor(overlayPlotStylesInterceptor(openmct));\n\n    openmct.types.addType('telemetry.plot.stacked', {\n      key: 'telemetry.plot.stacked',\n      name: 'Stacked Plot',\n      cssClass: 'icon-plot-stacked',\n      description:\n        'Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.',\n      creatable: true,\n      annotatable: true,\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = {\n          series: [],\n          yAxis: {},\n          xAxis: {},\n          objectStyles: {}\n        };\n      },\n      priority: 890\n    });\n\n    stackedPlotConfigurationInterceptor(openmct);\n\n    openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));\n    openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));\n    openmct.objectViews.addProvider(new PlotViewProvider(openmct));\n\n    openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new StackedPlotsInspectorViewProvider(openmct));\n\n    openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);\n    openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);\n\n    PlotViewActions.forEach((action) => {\n      openmct.actions.register(action);\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\nimport {\n  createMouseEvent,\n  createOpenMct,\n  renderWhenVisible,\n  resetApplicationState,\n  spyOnBuiltins\n} from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport configStore from './configuration/ConfigStore.js';\nimport PlotConfigurationModel from './configuration/PlotConfigurationModel.js';\nimport PlotOptions from './inspector/PlotOptions.vue';\nimport PlotVuePlugin from './plugin.js';\n\nconst TEST_KEY_ID = 'some-other-key';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let mockObjectPath;\n  let telemetrylimitProvider;\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'time-strip',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    const testTelemetry = [\n      {\n        utc: 1,\n        'some-key': 'some-value 1',\n        'some-other-key': 'some-other-value 1'\n      },\n      {\n        utc: 2,\n        'some-key': 'some-value 2',\n        'some-other-key': 'some-other-value 2'\n      },\n      {\n        utc: 3,\n        'some-key': 'some-value 3',\n        'some-other-key': 'some-other-value 3'\n      }\n    ];\n\n    const timeSystem = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 0,\n        end: 4\n      }\n    };\n\n    openmct = createOpenMct(timeSystem);\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(testTelemetry);\n\n      return telemetryPromise;\n    });\n\n    telemetrylimitProvider = jasmine.createSpyObj('telemetrylimitProvider', [\n      'supportsLimits',\n      'getLimits',\n      'getLimitEvaluator'\n    ]);\n    telemetrylimitProvider.supportsLimits.and.returnValue(true);\n    telemetrylimitProvider.getLimits.and.returnValue({\n      limits: function () {\n        return Promise.resolve({\n          WARNING: {\n            low: {\n              cssClass: 'is-limit--lwr is-limit--yellow',\n              'some-key': -0.5\n            },\n            high: {\n              cssClass: 'is-limit--upr is-limit--yellow',\n              'some-key': 0.5\n            }\n          },\n          DISTRESS: {\n            low: {\n              cssClass: 'is-limit--lwr is-limit--red',\n              'some-key': -0.9\n            },\n            high: {\n              cssClass: 'is-limit--upr is-limit--red',\n              'some-key': 0.9\n            }\n          }\n        });\n      }\n    });\n    telemetrylimitProvider.getLimitEvaluator.and.returnValue({\n      evaluate: function () {\n        return {};\n      }\n    });\n    openmct.telemetry.addProvider(telemetrylimitProvider);\n\n    openmct.install(new PlotVuePlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n    document.body.appendChild(element);\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    spyOnBuiltins(['requestAnimationFrame']);\n    window.requestAnimationFrame.and.callFake((callBack) => {\n      callBack();\n    });\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    openmct.time.setTimeSystem('utc', {\n      start: 0,\n      end: 2\n    });\n\n    await nextTick();\n    configStore.deleteAll();\n    return resetApplicationState(openmct);\n  });\n\n  describe('the plot views', () => {\n    it('provides a plot view for objects with telemetry', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'test-object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'other-key',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'yet-another-key',\n              format: 'string',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');\n\n      expect(plotView).toBeDefined();\n    });\n\n    it('does not provide a plot view if the telemetry is entirely non numeric', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'test-object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'other-key',\n              format: 'string',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'yet-another-key',\n              format: 'string',\n              hints: {\n                range: 1\n              }\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');\n\n      expect(plotView).toBeUndefined();\n    });\n\n    it('provides an overlay plot view for objects with telemetry', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'telemetry.plot.overlay',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key'\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay');\n      expect(plotView).toBeDefined();\n    });\n\n    it('provides an inspector view for overlay plots', () => {\n      let selection = [\n        [\n          {\n            context: {\n              item: {\n                id: 'test-object',\n                type: 'telemetry.plot.overlay',\n                telemetry: {\n                  values: [\n                    {\n                      key: 'some-key'\n                    }\n                  ]\n                }\n              }\n            }\n          },\n          {\n            context: {\n              item: {\n                type: 'time-strip'\n              }\n            }\n          }\n        ]\n      ];\n      const applicableInspectorViews = openmct.inspectorViews.get(selection);\n      const plotInspectorView = applicableInspectorViews.find(\n        (view) => view.key === 'plots-inspector'\n      );\n\n      expect(plotInspectorView).toBeDefined();\n    });\n\n    it('provides a stacked plot view for objects with telemetry', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'telemetry.plot.stacked',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key'\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-stacked');\n      expect(plotView).toBeDefined();\n    });\n  });\n\n  describe('The single plot view', () => {\n    let testTelemetryObject;\n    let applicableViews;\n    let plotViewProvider;\n    let plotView;\n\n    beforeEach(() => {\n      openmct.time.timeSystem('utc', {\n        start: 0,\n        end: 4\n      });\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      openmct.router.path = [testTelemetryObject];\n\n      applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');\n      plotView = plotViewProvider.view(testTelemetryObject, []);\n      plotView.show(child, true, { renderWhenVisible });\n\n      return nextTick();\n    });\n\n    afterEach(() => {\n      openmct.router.path = null;\n    });\n\n    it('Makes only one request for telemetry on load', () => {\n      expect(openmct.telemetry.request).toHaveBeenCalledTimes(1);\n    });\n\n    it('Renders a collapsed legend for every telemetry', async () => {\n      await nextTick();\n      let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name');\n      expect(legend.length).toBe(1);\n      expect(legend[0].innerHTML).toEqual('Test Object');\n    });\n\n    it('Renders an expanded legend for every telemetry', async () => {\n      await nextTick();\n      let legendControl = element.querySelector(\n        '.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle'\n      );\n      const clickEvent = createMouseEvent('click');\n\n      legendControl.dispatchEvent(clickEvent);\n      await nextTick();\n\n      let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');\n      expect(legend.length).toBe(6);\n    });\n\n    it('Renders X-axis ticks for the telemetry object', (done) => {\n      const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);\n      const config = configStore.get(configId);\n      config.xAxis.set('displayRange', {\n        min: 0,\n        max: 4\n      });\n\n      nextTick(() => {\n        let xAxisElement = element.querySelectorAll(\n          '.gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper'\n        );\n        expect(xAxisElement.length).toBe(1);\n\n        let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick');\n        expect(ticks.length).toBe(5);\n\n        done();\n      });\n    });\n\n    it('Renders Y-axis options for the telemetry object', async () => {\n      await nextTick();\n      let yAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select'\n      );\n      expect(yAxisElement.length).toBe(1);\n      //Object{name: \"Some attribute\", key: \"some-key\"}, Object{name: \"Another attribute\", key: \"some-other-key\"}\n      let options = yAxisElement[0].querySelectorAll('option');\n      expect(options.length).toBe(2);\n      expect(options[0].value).toBe('Some attribute');\n      expect(options[1].value).toBe('Another attribute');\n    });\n\n    xit('Updates the Y-axis label when changed', () => {\n      const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);\n      const config = configStore.get(configId);\n      const yAxisElement = element.querySelectorAll('.gl-plot-axis-area.gl-plot-y')[0].__vue__;\n      config.yAxis.seriesCollection.models.forEach((plotSeries) => {\n        expect(plotSeries.model.yKey).toBe('some-key');\n      });\n\n      yAxisElement.$emit('y-key-changed', TEST_KEY_ID, 1);\n      config.yAxis.seriesCollection.models.forEach((plotSeries) => {\n        expect(plotSeries.model.yKey).toBe(TEST_KEY_ID);\n      });\n    });\n\n    it('hides the pause and play controls', () => {\n      let pauseEl = element.querySelectorAll('.c-button-set .icon-pause');\n      let playEl = element.querySelectorAll('.c-button-set .icon-arrow-right');\n      expect(pauseEl.length).toBe(0);\n      expect(playEl.length).toBe(0);\n    });\n\n    describe('pause and play controls', () => {\n      beforeEach(() => {\n        openmct.time.setClock('local');\n        openmct.time.setClockOffsets({\n          start: -1000,\n          end: 100\n        });\n\n        return nextTick();\n      });\n\n      it('shows the pause controls', (done) => {\n        nextTick(() => {\n          let pauseEl = element.querySelectorAll('.c-button-set .icon-pause');\n          expect(pauseEl.length).toBe(1);\n          done();\n        });\n      });\n\n      it('shows the play control if plot is paused', (done) => {\n        let pauseEl = element.querySelector('.c-button-set .icon-pause');\n        const clickEvent = createMouseEvent('click');\n\n        pauseEl.dispatchEvent(clickEvent);\n        nextTick(() => {\n          let playEl = element.querySelectorAll('.c-button-set .is-paused');\n          expect(playEl.length).toBe(1);\n          done();\n        });\n      });\n    });\n\n    describe('resume actions on errant click', () => {\n      beforeEach(() => {\n        openmct.time.setClock('local');\n        openmct.time.setClockOffsets({\n          start: -1000,\n          end: 100\n        });\n\n        return nextTick();\n      });\n\n      it('clicking the plot view without movement resumes the plot while active', async () => {\n        const pauseEl = element.querySelectorAll('.c-button-set .icon-pause');\n        // if the pause button is present, the chart is running\n        expect(pauseEl.length).toBe(1);\n\n        // simulate an errant mouse click\n        // the second item is the canvas we need to use\n        const canvas = element.querySelectorAll('canvas')[1];\n        const mouseDownEvent = new MouseEvent('mousedown');\n        const mouseUpEvent = new MouseEvent('mouseup');\n        canvas.dispatchEvent(mouseDownEvent);\n        // mouseup event is bound to the window\n        window.dispatchEvent(mouseUpEvent);\n        await nextTick();\n\n        const pauseElAfterClick = element.querySelectorAll('.c-button-set .icon-pause');\n        console.log('pauseElAfterClick', pauseElAfterClick);\n        expect(pauseElAfterClick.length).toBe(1);\n      });\n\n      it('clicking the plot view without movement leaves the plot paused', async () => {\n        const pauseEl = element.querySelector('.c-button-set .icon-pause');\n        // pause the plot\n        pauseEl.dispatchEvent(createMouseEvent('click'));\n        await nextTick();\n\n        const playEl = element.querySelectorAll('.c-button-set .is-paused');\n        expect(playEl.length).toBe(1);\n\n        // simulate an errant mouse click\n        // the second item is the canvas we need to use\n        const canvas = element.querySelectorAll('canvas')[1];\n        const mouseDownEvent = new MouseEvent('mousedown');\n        const mouseUpEvent = new MouseEvent('mouseup');\n        canvas.dispatchEvent(mouseDownEvent);\n        // mouseup event is bound to the window\n        window.dispatchEvent(mouseUpEvent);\n        await nextTick();\n\n        const playElAfterChartClick = element.querySelectorAll('.c-button-set .is-paused');\n        expect(playElAfterChartClick.length).toBe(1);\n      });\n\n      it('clicking the plot does not request historical data', async () => {\n        expect(openmct.telemetry.request).toHaveBeenCalledTimes(2);\n\n        // simulate an errant mouse click\n        // the second item is the canvas we need to use\n        const canvas = element.querySelectorAll('canvas')[1];\n        const mouseDownEvent = new MouseEvent('mousedown');\n        const mouseUpEvent = new MouseEvent('mouseup');\n        canvas.dispatchEvent(mouseDownEvent);\n        // mouseup event is bound to the window\n        window.dispatchEvent(mouseUpEvent);\n        await nextTick();\n\n        expect(openmct.telemetry.request).toHaveBeenCalledTimes(2);\n      });\n\n      describe('limits', () => {\n        it('lines are not displayed by default', () => {\n          let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line');\n          expect(limitEl.length).toBe(0);\n        });\n\n        it('lines are displayed when configuration is set to true', async () => {\n          await nextTick();\n          const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);\n          const config = configStore.get(configId);\n          config.yAxis.set('displayRange', {\n            min: 0,\n            max: 4\n          });\n          config.series.models[0].set('limitLines', true);\n\n          await nextTick();\n          let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line');\n          expect(limitEl.length).toBe(4);\n        });\n      });\n    });\n\n    describe('controls in time strip view', () => {\n      it('zoom controls are hidden', () => {\n        let pauseEl = element.querySelectorAll('.c-button-set .js-zoom');\n        expect(pauseEl.length).toBe(0);\n      });\n\n      it('pan controls are hidden', () => {\n        let pauseEl = element.querySelectorAll('.c-button-set .js-pan');\n        expect(pauseEl.length).toBe(0);\n      });\n\n      it('pause/play controls are hidden', () => {\n        let pauseEl = element.querySelectorAll('.c-button-set .js-pause');\n        expect(pauseEl.length).toBe(0);\n      });\n    });\n  });\n\n  describe('resizing the plot', () => {\n    let plotContainerResizeObserver;\n    let resizePromiseResolve;\n    let testTelemetryObject;\n    let applicableViews;\n    let plotViewProvider;\n    let plotView;\n    let resizePromise;\n\n    beforeEach(() => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      openmct.router.path = [testTelemetryObject];\n\n      applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');\n      plotView = plotViewProvider.view(testTelemetryObject, []);\n\n      plotView.show(child, true, { renderWhenVisible });\n\n      resizePromise = new Promise((resolve) => {\n        resizePromiseResolve = resolve;\n      });\n\n      const handlePlotResize = _.debounce(() => {\n        resizePromiseResolve(true);\n      }, 600);\n\n      plotContainerResizeObserver = new ResizeObserver(handlePlotResize);\n      plotContainerResizeObserver.observe(\n        plotView.getComponent().$refs.plotComponent.$refs.plotWrapper\n      );\n\n      return nextTick(() => {\n        plotView.getComponent().$refs.plotComponent.$refs.mctPlot.stopFollowingTimeContext();\n        spyOn(\n          plotView.getComponent().$refs.plotComponent.$refs.mctPlot,\n          'loadSeriesData'\n        ).and.callThrough();\n      });\n    });\n\n    afterEach(() => {\n      plotContainerResizeObserver.disconnect();\n      openmct.router.path = null;\n    });\n\n    xit('requests historical data when over the threshold', async () => {\n      await nextTick();\n      element.style.width = '680px';\n      await resizePromise;\n      expect(\n        plotView.getComponent().$refs.plotComponent.$refs.mctPlot.loadSeriesData\n      ).toHaveBeenCalledTimes(1);\n    });\n\n    it('does not request historical data when under the threshold', async () => {\n      element.style.width = '644px';\n      await resizePromise;\n      expect(\n        plotView.getComponent().$refs.plotComponent.$refs.mctPlot.loadSeriesData\n      ).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('the inspector view', () => {\n    let component;\n    let viewComponentObject;\n    let mockComposition;\n    let testTelemetryObject;\n    let selection;\n    let config;\n    beforeEach((done) => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      selection = [\n        [\n          {\n            context: {\n              item: {\n                id: 'test-object',\n                identifier: {\n                  key: 'test-object',\n                  namespace: ''\n                },\n                type: 'telemetry.plot.overlay',\n                configuration: {\n                  series: [\n                    {\n                      identifier: {\n                        key: 'test-object',\n                        namespace: ''\n                      }\n                    }\n                  ]\n                },\n                composition: []\n              }\n            }\n          },\n          {\n            context: {\n              item: {\n                type: 'time-strip',\n                identifier: {\n                  key: 'some-other-key',\n                  namespace: ''\n                }\n              }\n            }\n          }\n        ]\n      ];\n\n      openmct.router.path = [testTelemetryObject];\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testTelemetryObject);\n\n        return [testTelemetryObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);\n      config = new PlotConfigurationModel({\n        id: configId,\n        domainObject: selection[0][0].context.item,\n        openmct: openmct\n      });\n      configStore.add(configId, config);\n\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode } = mount(\n        {\n          components: {\n            PlotOptions\n          },\n          provide: {\n            openmct: openmct,\n            domainObject: selection[0][0].context.item,\n            path: [selection[0][0].context.item, selection[0][1].context.item],\n            renderWhenVisible\n          },\n          template: '<plot-options ref=\"root\"/>'\n        },\n        {\n          element: viewContainer\n        }\n      );\n      component = vNode.componentInstance;\n\n      nextTick(() => {\n        viewComponentObject = component.$refs.root;\n        done();\n      });\n    });\n\n    afterEach(() => {\n      openmct.router.path = null;\n    });\n\n    describe('in view only mode', () => {\n      let browseOptionsEl;\n      let editOptionsEl;\n      beforeEach(() => {\n        browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');\n        editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');\n      });\n\n      it('does not show the edit options', () => {\n        expect(editOptionsEl).toBeNull();\n      });\n\n      it('shows the name', () => {\n        const seriesEl = browseOptionsEl.querySelector('.c-object-label__name');\n        expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);\n      });\n\n      it('shows in collapsed mode', () => {\n        const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');\n        expect(seriesEl.length).toEqual(0);\n      });\n\n      it('shows in expanded mode', () => {\n        let expandControl = browseOptionsEl.querySelector('.c-disclosure-triangle');\n        const clickEvent = createMouseEvent('click');\n        expandControl.dispatchEvent(clickEvent);\n\n        const plotOptionsProperties = browseOptionsEl.querySelectorAll(\n          '.js-plot-options-browse-properties .grid-row'\n        );\n        expect(plotOptionsProperties.length).toEqual(6);\n      });\n    });\n\n    describe('in edit mode', () => {\n      let editOptionsEl;\n      let browseOptionsEl;\n\n      beforeEach((done) => {\n        viewComponentObject.setEditState(true);\n        nextTick(() => {\n          editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');\n          browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');\n          done();\n        });\n      });\n\n      it('does not show the browse options', () => {\n        expect(browseOptionsEl).toBeNull();\n      });\n\n      it('shows the name', () => {\n        const seriesEl = editOptionsEl.querySelector('.c-object-label__name');\n        expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);\n      });\n\n      it('shows in collapsed mode', () => {\n        const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');\n        expect(seriesEl.length).toEqual(0);\n      });\n\n      it('shows in collapsed mode', () => {\n        const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');\n        expect(seriesEl.length).toEqual(0);\n      });\n\n      it('renders expanded', () => {\n        const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle');\n        const clickEvent = createMouseEvent('click');\n        expandControl.dispatchEvent(clickEvent);\n\n        const plotOptionsProperties = editOptionsEl.querySelectorAll(\n          '.js-plot-options-edit-properties .grid-row'\n        );\n        expect(plotOptionsProperties.length).toEqual(8);\n      });\n\n      it('shows yKeyOptions', () => {\n        const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle');\n        const clickEvent = createMouseEvent('click');\n        expandControl.dispatchEvent(clickEvent);\n\n        const plotOptionsProperties = editOptionsEl.querySelectorAll(\n          '.js-plot-options-edit-properties .grid-row'\n        );\n\n        const yKeySelection = plotOptionsProperties[0].querySelector('select');\n        const options = Array.from(yKeySelection.options).map((option) => {\n          return option.value;\n        });\n        expect(options).toEqual([\n          testTelemetryObject.telemetry.values[1].key,\n          testTelemetryObject.telemetry.values[2].key\n        ]);\n      });\n\n      it('shows yAxis options', () => {\n        const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle');\n        const clickEvent = createMouseEvent('click');\n        expandControl.dispatchEvent(clickEvent);\n\n        const yAxisProperties = editOptionsEl.querySelectorAll(\n          'div.grid-properties:first-of-type .l-inspector-part'\n        );\n\n        // TODO better test\n        expect(yAxisProperties.length).toEqual(2);\n      });\n\n      it('renders color palette options', () => {\n        const colorSwatch = editOptionsEl.querySelector('.c-click-swatch');\n        expect(colorSwatch).toBeDefined();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/StackedPlot.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    v-if=\"loaded\"\n    class=\"c-plot c-plot--stacked holder holder-plot has-control-bar u-style-receiver js-style-receiver\"\n    :class=\"[plotLegendExpandedStateClass, plotLegendPositionClass]\"\n    aria-label=\"Stacked Plot Style Target\"\n  >\n    <PlotLegend\n      v-if=\"compositionObjectsConfigLoaded && showLegendsForChildren === false\"\n      :cursor-locked=\"!!lockHighlightPoint\"\n      :highlights=\"highlights\"\n      class=\"js-stacked-plot-legend\"\n      @legend-hover-changed=\"legendHoverChanged\"\n      @expanded=\"updateExpanded\"\n      @position=\"updatePosition\"\n    />\n    <div class=\"l-view-section\">\n      <StackedPlotItem\n        v-for=\"objectWrapper in compositionObjects\"\n        ref=\"stackedPlotItems\"\n        :key=\"objectWrapper.keyString\"\n        class=\"c-plot--stacked-container\"\n        :child-object=\"objectWrapper.object\"\n        :options=\"options\"\n        :grid-lines=\"gridLines\"\n        :color-palette=\"colorPalette\"\n        :cursor-guide=\"cursorGuide\"\n        :show-limit-line-labels=\"showLimitLineLabels\"\n        :hide-legend=\"showLegendsForChildren === false\"\n        @loading-updated=\"loadingUpdated\"\n        @cursor-guide=\"onCursorGuideChange\"\n        @grid-lines=\"onGridLinesChange\"\n        @lock-highlight-point=\"lockHighlightPointUpdated\"\n        @highlights=\"highlightsUpdated\"\n        @config-loaded=\"configLoadedForObject(objectWrapper.keyString)\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { inject } from 'vue';\n\nimport ColorPalette from '@/ui/color/ColorPalette';\n\nimport ImageExporter from '../../../exporters/ImageExporter.js';\nimport { useAlignment } from '../../../ui/composables/alignmentContext.js';\nimport configStore from '../configuration/ConfigStore.js';\nimport PlotConfigurationModel from '../configuration/PlotConfigurationModel.js';\nimport PlotLegend from '../legend/PlotLegend.vue';\nimport eventHelpers from '../lib/eventHelpers.js';\nimport StackedPlotItem from './StackedPlotItem.vue';\n\nexport default {\n  components: {\n    StackedPlotItem,\n    PlotLegend\n  },\n  inject: ['openmct', 'domainObject', 'objectPath', 'renderWhenVisible'],\n  props: {\n    options: {\n      type: Object,\n      default() {\n        return {};\n      }\n    }\n  },\n  setup() {\n    const domainObject = inject('domainObject');\n    const objectPath = inject('objectPath');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData, reset: resetAlignment } = useAlignment(\n      domainObject,\n      objectPath,\n      openmct\n    );\n\n    return { alignmentData, resetAlignment };\n  },\n  data() {\n    return {\n      hideExportButtons: false,\n      cursorGuide: false,\n      gridLines: true,\n      configLoaded: {},\n      compositionObjects: [],\n      loaded: false,\n      lockHighlightPoint: false,\n      highlights: [],\n      showLimitLineLabels: undefined,\n      colorPalette: new ColorPalette(),\n      compositionObjectsConfigLoaded: false,\n      position: 'top',\n      showLegendsForChildren: true,\n      expanded: false\n    };\n  },\n  computed: {\n    plotLegendPositionClass() {\n      if (this.showLegendsForChildren) {\n        return '';\n      }\n\n      return `plot-legend-${this.position}`;\n    },\n    plotLegendExpandedStateClass() {\n      let legendExpandedStateClass = '';\n\n      if (this.showLegendsForChildren !== true && this.expanded) {\n        legendExpandedStateClass = 'plot-legend-expanded';\n      } else if (this.showLegendsForChildren !== true && !this.expanded) {\n        legendExpandedStateClass = 'plot-legend-collapsed';\n      }\n\n      return legendExpandedStateClass;\n    }\n  },\n  beforeUnmount() {\n    this.destroy();\n  },\n  mounted() {\n    eventHelpers.extend(this);\n    //We only need to initialize the stacked plot config for legend properties\n    const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n    this.config = this.getConfig(configId);\n    this.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');\n\n    this.loaded = true;\n    this.imageExporter = new ImageExporter(this.openmct);\n\n    this.composition = this.openmct.composition.get(this.domainObject);\n    this.composition.on('add', this.addChild);\n    this.composition.on('remove', this.removeChild);\n    this.composition.on('reorder', this.compositionReorder);\n    this.composition.load();\n  },\n  methods: {\n    getConfig(configId) {\n      let config = configStore.get(configId);\n      if (!config) {\n        config = new PlotConfigurationModel({\n          id: configId,\n          domainObject: this.domainObject,\n          openmct: this.openmct,\n          callback: (data) => {\n            this.data = data;\n          }\n        });\n        configStore.add(configId, config);\n      }\n\n      return config;\n    },\n    loadingUpdated(loaded) {\n      this.loading = loaded;\n    },\n    configLoadedForObject(childObjIdentifier) {\n      const childObjId = this.openmct.objects.makeKeyString(childObjIdentifier);\n      this.configLoaded[childObjId] = true;\n      this.setConfigLoadedForComposition();\n    },\n    setConfigLoadedForComposition() {\n      this.compositionObjectsConfigLoaded =\n        this.compositionObjects.length &&\n        this.compositionObjects.every((childObject) => {\n          const id = childObject.keyString;\n\n          return this.configLoaded[id] === true;\n        });\n      if (this.compositionObjectsConfigLoaded) {\n        this.listenTo(\n          this.config.legend,\n          'change:showLegendsForChildren',\n          this.updateShowLegendsForChildren,\n          this\n        );\n      }\n    },\n    destroy() {\n      this.resetAlignment();\n      this.composition.off('add', this.addChild);\n      this.composition.off('remove', this.removeChild);\n      this.composition.off('reorder', this.compositionReorder);\n\n      this.stopListening();\n      const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      configStore.deleteStore(configId);\n    },\n\n    addChild(child) {\n      if (this.openmct.objects.isMissing(child)) {\n        console.warn('Missing domain object for stacked plot: ', child);\n        return;\n      }\n\n      const id = this.openmct.objects.makeKeyString(child.identifier);\n\n      this.compositionObjects.push({\n        object: child,\n        keyString: id\n      });\n      this.setConfigLoadedForComposition();\n    },\n\n    removeChild(childIdentifier) {\n      const id = this.openmct.objects.makeKeyString(childIdentifier);\n\n      const childObj = this.compositionObjects.filter((c) => {\n        const identifier = c.keyString;\n\n        return identifier === id;\n      })[0];\n\n      if (childObj) {\n        if (childObj.object.type !== 'telemetry.plot.overlay') {\n          const config = this.getConfig(childObj.keyString);\n          if (config) {\n            config.series.remove(config.series.at(0));\n          }\n        }\n      }\n\n      this.compositionObjects = this.compositionObjects.filter((c) => {\n        const identifier = c.keyString;\n\n        return identifier !== id;\n      });\n\n      const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {\n        return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);\n      });\n      if (configIndex > -1) {\n        const cSeries = this.domainObject.configuration.series.slice();\n        this.openmct.objects.mutate(this.domainObject, 'configuration.series', cSeries);\n      }\n\n      this.setConfigLoadedForComposition();\n    },\n\n    compositionReorder(reorderPlan) {\n      let oldComposition = this.compositionObjects.slice();\n\n      reorderPlan.forEach((reorder) => {\n        this.compositionObjects[reorder.newIndex] = oldComposition[reorder.oldIndex];\n      });\n    },\n\n    resetTelemetry(domainObject) {\n      this.compositionObjects = [];\n    },\n\n    exportJPG(filename) {\n      this.hideExportButtons = true;\n      const plotElement = this.$el;\n      filename = filename ?? `${this.domainObject.name} - stacked-plot`;\n\n      this.imageExporter.exportJPG(plotElement, filename, 'export-plot').finally(\n        function () {\n          this.hideExportButtons = false;\n        }.bind(this)\n      );\n    },\n\n    exportPNG(filename) {\n      this.hideExportButtons = true;\n      const plotElement = this.$el;\n      filename = filename ?? `${this.domainObject.name} - stacked-plot`;\n      this.imageExporter.exportPNG(plotElement, filename, 'export-plot').finally(\n        function () {\n          this.hideExportButtons = false;\n        }.bind(this)\n      );\n    },\n    legendHoverChanged(data) {\n      this.showLimitLineLabels = data;\n    },\n    lockHighlightPointUpdated(data) {\n      this.lockHighlightPoint = data;\n    },\n    updateExpanded(expanded) {\n      this.expanded = expanded;\n    },\n    updatePosition(position) {\n      this.position = position;\n    },\n    updateShowLegendsForChildren(showLegendsForChildren) {\n      this.showLegendsForChildren = showLegendsForChildren;\n    },\n    updateReady(ready) {\n      this.configReady = ready;\n    },\n    highlightsUpdated(data) {\n      this.highlights = data;\n    },\n    onCursorGuideChange(cursorGuide) {\n      this.cursorGuide = cursorGuide === true;\n    },\n    onGridLinesChange(gridLines) {\n      this.gridLines = gridLines === true;\n    },\n    getViewContext() {\n      return {\n        exportPNG: this.exportPNG,\n        exportJPG: this.exportJPG\n      };\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js",
    "content": "export default function StackedPlotCompositionPolicy(openmct) {\n  function hasNumericTelemetry(domainObject) {\n    const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);\n    if (!hasTelemetry) {\n      return false;\n    }\n\n    let metadata = openmct.telemetry.getMetadata(domainObject);\n\n    return metadata.values().length > 0 && hasDomainAndRange(metadata);\n  }\n\n  function hasDomainAndRange(metadata) {\n    return (\n      metadata.valuesForHints(['range']).length > 0 &&\n      metadata.valuesForHints(['domain']).length > 0\n    );\n  }\n\n  return {\n    allow: function (parent, child) {\n      if (\n        parent.type === 'telemetry.plot.stacked' &&\n        child.type !== 'telemetry.plot.overlay' &&\n        hasNumericTelemetry(child) === false\n      ) {\n        return false;\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/StackedPlotItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div :aria-label=\"`Stacked Plot Item ${childObject.name}`\">\n    <Plot\n      ref=\"plotComponent\"\n      :hide-legend=\"hideLegend\"\n      :limit-line-labels=\"showLimitLineLabels\"\n      :grid-lines=\"gridLines\"\n      :cursor-guide=\"cursorGuide\"\n      :options=\"options\"\n      :color-palette=\"colorPalette\"\n      :class=\"isStale && 'is-stale'\"\n      @config-loaded=\"onConfigLoaded\"\n      @lock-highlight-point=\"onLockHighlightPointUpdated\"\n      @highlights=\"onHighlightsUpdated\"\n      @cursor-guide=\"onCursorGuideChange\"\n      @grid-lines=\"onGridLinesChange\"\n    />\n  </div>\n</template>\n<script>\nimport configStore from '@/plugins/plot/configuration/ConfigStore';\nimport PlotConfigurationModel from '@/plugins/plot/configuration/PlotConfigurationModel';\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport Plot from '../PlotView.vue';\nimport conditionalStylesMixin from './mixins/objectStyles-mixin.js';\n\nexport default {\n  components: {\n    Plot\n  },\n  mixins: [conditionalStylesMixin, stalenessMixin],\n  inject: ['openmct', 'domainObject', 'objectPath', 'renderWhenVisible'],\n  provide() {\n    return {\n      openmct: this.openmct,\n      domainObject: this.childObject\n    };\n  },\n  props: {\n    childObject: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    options: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    gridLines: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    cursorGuide: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    showLimitLineLabels: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    colorPalette: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    },\n    hideLegend: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: [\n    'lock-highlight-point',\n    'highlights',\n    'config-loaded',\n    'cursor-guide',\n    'grid-lines',\n    'plot-y-tick-width'\n  ],\n  beforeMount() {\n    // We must do this before mounted to use any series configuration options set at the stacked plot level\n    this.updateView();\n  },\n  mounted() {\n    this.isEditing = this.openmct.editor.isEditing();\n    this.openmct.editor.on('isEditing', this.setEditState);\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n    if (this.composition) {\n      this.composition.off('add', this.subscribeToStaleness);\n      this.composition.off('remove', this.removeSubscription);\n    }\n\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n\n    const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);\n    configStore.deleteStore(configId);\n\n    if (this._destroy) {\n      this._destroy();\n    }\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n\n      if (this.isEditing) {\n        this.setSelection();\n      } else {\n        if (this.removeSelectable) {\n          this.removeSelectable();\n        }\n      }\n    },\n    removeSubscription(identifier) {\n      this.triggerUnsubscribeFromStaleness({\n        identifier\n      });\n    },\n    updateView() {\n      //If this object is not persistable, then package it with it's parent\n      const plotObject = this.getPlotObject();\n\n      if (plotObject === null) {\n        return;\n      }\n\n      if (this.openmct.telemetry.isTelemetryObject(plotObject)) {\n        this.subscribeToStaleness(plotObject);\n      } else {\n        // possibly overlay or other composition based plot\n        this.composition = this.openmct.composition.get(plotObject);\n\n        this.composition.on('add', this.subscribeToStaleness);\n        this.composition.on('remove', this.removeSubscription);\n        this.composition.load();\n      }\n\n      if (this.isEditing) {\n        this.setSelection();\n      }\n    },\n    onLockHighlightPointUpdated() {\n      this.$emit('lock-highlight-point', ...arguments);\n    },\n    onHighlightsUpdated() {\n      this.$emit('highlights', ...arguments);\n    },\n    onConfigLoaded() {\n      this.$emit('config-loaded', ...arguments);\n    },\n    onCursorGuideChange() {\n      this.$emit('cursor-guide', ...arguments);\n    },\n    onGridLinesChange() {\n      this.$emit('grid-lines', ...arguments);\n    },\n    setSelection() {\n      let childContext = {};\n      childContext.item = this.childObject;\n      this.context = childContext;\n      if (this.removeSelectable) {\n        this.removeSelectable();\n      }\n\n      this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);\n    },\n    getPlotObject() {\n      this.checkPlotConfiguration();\n      return this.childObject;\n    },\n    checkPlotConfiguration() {\n      // If the object has its own configuration (like an overlay plot), don't initialize a stacked plot configuration\n      // and instead use its configuration directly.\n      // Otherwise ensure we've got a stacked plot item configuration ready for us.\n      if (\n        !this.openmct.objects.isMissing(this.childObject) &&\n        !this.childObject.configuration?.series\n      ) {\n        this.ensureStackedSeriesConfigInitialization();\n      }\n    },\n    ensureStackedSeriesConfigInitialization() {\n      const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);\n      const existingConfig = configStore.get(configId);\n      if (!existingConfig) {\n        let persistedSeriesConfig = this.domainObject.configuration.series.find((seriesConfig) => {\n          return this.openmct.objects.areIdsEqual(\n            seriesConfig.identifier,\n            this.childObject.identifier\n          );\n        });\n\n        if (!persistedSeriesConfig) {\n          persistedSeriesConfig = {\n            series: {},\n            yAxis: {}\n          };\n        }\n\n        const newConfig = new PlotConfigurationModel({\n          id: configId,\n          domainObject: {\n            ...this.childObject,\n            configuration: {\n              series: [\n                {\n                  identifier: this.childObject.identifier,\n                  ...persistedSeriesConfig.series\n                }\n              ],\n              yAxis: persistedSeriesConfig.yAxis,\n              ...this.childObject.configuration\n            }\n          },\n          openmct: this.openmct,\n          palette: this.colorPalette,\n          callback: (data) => {\n            this.data = data;\n          }\n        });\n        configStore.add(configId, newConfig);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/StackedPlotViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport StackedPlot from './StackedPlot.vue';\n\nexport default function StackedPlotViewProvider(openmct) {\n  function isCompactView(objectPath) {\n    let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');\n\n    return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);\n  }\n\n  return {\n    key: 'plot-stacked',\n    name: 'Stacked Plot',\n    cssClass: 'icon-telemetry',\n    canView(domainObject, objectPath) {\n      return domainObject.type === 'telemetry.plot.stacked';\n    },\n\n    canEdit(domainObject, objectPath) {\n      return domainObject.type === 'telemetry.plot.stacked';\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n      let component = null;\n\n      return {\n        show: function (element, isEditing, { renderWhenVisible }) {\n          let isCompact = isCompactView(objectPath);\n\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                StackedPlot\n              },\n              provide: {\n                openmct,\n                domainObject,\n                objectPath,\n                renderWhenVisible\n              },\n              data() {\n                return {\n                  options: {\n                    compact: isCompact\n                  }\n                };\n              },\n              template: '<stacked-plot ref=\"plotComponent\" :options=\"options\"></stacked-plot>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n          component = vNode.componentInstance;\n        },\n        getViewContext() {\n          if (!component) {\n            return {};\n          }\n\n          return component.$refs.plotComponent.getViewContext();\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport StyleRuleManager from '@/plugins/condition/StyleRuleManager';\nimport { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';\n\nexport default {\n  inject: ['openmct', 'domainObject', 'objectPath'],\n  data() {\n    return {\n      objectStyle: undefined\n    };\n  },\n  mounted() {\n    this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration);\n    this.initObjectStyles();\n  },\n  beforeUnmount() {\n    if (this.stopListeningStyles) {\n      this.stopListeningStyles();\n    }\n\n    if (this.stopListeningFontStyles) {\n      this.stopListeningFontStyles();\n    }\n\n    if (this.styleRuleManager) {\n      this.styleRuleManager.destroy();\n    }\n  },\n  methods: {\n    getObjectStyleForItem(config) {\n      if (config && config.objectStyles) {\n        return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined;\n      } else {\n        return undefined;\n      }\n    },\n    initObjectStyles() {\n      if (!this.styleRuleManager) {\n        this.styleRuleManager = new StyleRuleManager(\n          this.objectStyles,\n          this.openmct,\n          this.updateStyle.bind(this),\n          true\n        );\n      } else {\n        this.styleRuleManager.updateObjectStyleConfig(this.objectStyles);\n      }\n\n      if (this.stopListeningStyles) {\n        this.stopListeningStyles();\n      }\n\n      this.stopListeningStyles = this.openmct.objects.observe(\n        this.childObject,\n        'configuration.objectStyles',\n        (newObjectStyle) => {\n          //Updating styles in the inspector view will trigger this so that the changes are reflected immediately\n          this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);\n        }\n      );\n\n      if (\n        this.childObject &&\n        this.childObject.configuration &&\n        this.childObject.configuration.fontStyle\n      ) {\n        const { fontSize, font } = this.childObject.configuration.fontStyle;\n        this.setFontSize(fontSize);\n        this.setFont(font);\n      }\n\n      this.stopListeningFontStyles = this.openmct.objects.observe(\n        this.childObject,\n        'configuration.fontStyle',\n        (newFontStyle) => {\n          this.setFontSize(newFontStyle.fontSize);\n          this.setFont(newFontStyle.font);\n        }\n      );\n    },\n    getStyleReceiver() {\n      let styleReceiver;\n\n      if (this.$el !== undefined) {\n        styleReceiver =\n          this.$el.querySelector('.js-style-receiver') || this.$el.querySelector(':first-child');\n\n        if (styleReceiver === null) {\n          styleReceiver = undefined;\n        }\n      }\n\n      return styleReceiver;\n    },\n    setFontSize(newSize) {\n      let elemToStyle = this.getStyleReceiver();\n\n      if (elemToStyle !== undefined) {\n        elemToStyle.dataset.fontSize = newSize;\n      }\n    },\n    setFont(newFont) {\n      let elemToStyle = this.getStyleReceiver();\n\n      if (elemToStyle !== undefined) {\n        elemToStyle.dataset.font = newFont;\n      }\n    },\n    updateStyle(styleObj) {\n      const elemToStyle = this.getStyleReceiver();\n\n      if (!styleObj || !elemToStyle) {\n        return;\n      }\n      // handle visibility separately\n      if (styleObj.isStyleInvisible !== undefined) {\n        elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);\n        styleObj.isStyleInvisible = null;\n      }\n\n      Object.entries(styleObj).forEach(([key, value]) => {\n        if (typeof value !== 'string' || !value.includes('__no_value')) {\n          elemToStyle.style[key] = value;\n        } else {\n          elemToStyle.style[key] = ''; // remove the property\n        }\n      });\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\nimport {\n  createMouseEvent,\n  createOpenMct,\n  renderWhenVisible,\n  resetApplicationState,\n  spyOnBuiltins\n} from 'utils/testing';\nimport { nextTick, ref } from 'vue';\n\nimport configStore from '../configuration/ConfigStore.js';\nimport PlotConfigurationModel from '../configuration/PlotConfigurationModel.js';\nimport PlotOptions from '../inspector/PlotOptions.vue';\nimport PlotVuePlugin from '../plugin.js';\nimport StackedPlot from './StackedPlot.vue';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let telemetryPromise;\n  let telemetryPromiseResolve;\n  let mockObjectPath;\n  let stackedPlotObject = {\n    identifier: {\n      namespace: '',\n      key: 'test-plot'\n    },\n    type: 'telemetry.plot.stacked',\n    name: 'Test Stacked Plot',\n    configuration: {\n      series: []\n    }\n  };\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'time-strip',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    const testTelemetry = [\n      {\n        utc: 1,\n        'some-key': 'some-value 1',\n        'some-other-key': 'some-other-value 1'\n      },\n      {\n        utc: 2,\n        'some-key': 'some-value 2',\n        'some-other-key': 'some-other-value 2'\n      },\n      {\n        utc: 3,\n        'some-key': 'some-value 3',\n        'some-other-key': 'some-other-value 3'\n      }\n    ];\n\n    const timeSystem = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 0,\n        end: 4\n      }\n    };\n\n    openmct = createOpenMct(timeSystem);\n\n    telemetryPromise = new Promise((resolve) => {\n      telemetryPromiseResolve = resolve;\n    });\n\n    spyOn(openmct.telemetry, 'request').and.callFake(() => {\n      telemetryPromiseResolve(testTelemetry);\n\n      return telemetryPromise;\n    });\n\n    openmct.install(new PlotVuePlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n    document.body.appendChild(element);\n\n    spyOn(window, 'ResizeObserver').and.returnValue({\n      observe() {},\n      unobserve() {},\n      disconnect() {}\n    });\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    spyOnBuiltins(['requestAnimationFrame']);\n    window.requestAnimationFrame.and.callFake((callBack) => {\n      callBack();\n    });\n\n    openmct.router.path = [stackedPlotObject];\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(async () => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n    configStore.deleteAll();\n    await resetApplicationState(openmct);\n  });\n\n  afterAll(() => {\n    openmct.router.path = null;\n  });\n\n  describe('the plot views', () => {\n    it('provides a stacked plot view for objects with telemetry', () => {\n      const testTelemetryObject = {\n        id: 'test-object',\n        type: 'telemetry.plot.stacked',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key'\n            }\n          ]\n        }\n      };\n\n      const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);\n      let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-stacked');\n      expect(plotView).toBeDefined();\n    });\n  });\n\n  describe('The stacked plot view', () => {\n    let testTelemetryObject;\n    let testTelemetryObject2;\n    let config;\n    let component;\n    let mockCompositionList = [];\n    let plotViewComponentObject;\n    let destroyStackedPlot;\n\n    afterAll(() => {\n      openmct.router.path = null;\n    });\n\n    beforeEach(async () => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        },\n        configuration: {\n          objectStyles: {\n            staticStyle: {\n              style: {\n                backgroundColor: 'rgb(0, 200, 0)',\n                color: '',\n                border: ''\n              }\n            },\n            conditionSetIdentifier: {\n              namespace: '',\n              key: 'testConditionSetId'\n            },\n            selectedConditionId: 'conditionId1',\n            defaultConditionId: 'conditionId1',\n            styles: [\n              {\n                conditionId: 'conditionId1',\n                style: {\n                  backgroundColor: 'rgb(0, 155, 0)',\n                  color: '',\n                  output: '',\n                  border: ''\n                }\n              }\n            ]\n          }\n        }\n      };\n\n      testTelemetryObject2 = {\n        identifier: {\n          namespace: '',\n          key: 'test-object2'\n        },\n        type: 'test-object',\n        name: 'Test Object2',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key2',\n              name: 'Some attribute2',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key2',\n              name: 'Another attribute2',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      stackedPlotObject.composition = [\n        {\n          identifier: testTelemetryObject.identifier\n        }\n      ];\n\n      mockCompositionList = [];\n      spyOn(openmct.composition, 'get').and.callFake((domainObject) => {\n        //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view\n        const numObjects = domainObject.composition?.length ?? 0;\n        const mockComposition = new EventEmitter();\n        mockComposition.load = () => {\n          if (numObjects === 1) {\n            mockComposition.emit('add', testTelemetryObject);\n\n            return [testTelemetryObject];\n          } else if (numObjects === 2) {\n            mockComposition.emit('add', testTelemetryObject);\n            mockComposition.emit('add', testTelemetryObject2);\n\n            return [testTelemetryObject, testTelemetryObject2];\n          } else {\n            return [];\n          }\n        };\n\n        mockCompositionList.push(mockComposition);\n\n        return mockComposition;\n      });\n\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount(\n        {\n          components: {\n            StackedPlot\n          },\n          provide: {\n            openmct,\n            domainObject: stackedPlotObject,\n            objectPath: [stackedPlotObject],\n            renderWhenVisible\n          },\n          template: '<stacked-plot ref=\"stackedPlotRef\"></stacked-plot>'\n        },\n        {\n          element: viewContainer\n        }\n      );\n\n      component = vNode.componentInstance;\n      destroyStackedPlot = destroy;\n\n      await telemetryPromise;\n      await nextTick();\n      plotViewComponentObject = component.$refs.stackedPlotRef;\n      const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);\n      config = configStore.get(configId);\n    });\n\n    afterEach(() => {\n      destroyStackedPlot();\n    });\n\n    it('Renders a collapsed legend for every telemetry', () => {\n      let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name');\n      expect(legend.length).toBe(1);\n      expect(legend[0].innerHTML).toEqual('Test Object');\n    });\n\n    it('Renders an expanded legend for every telemetry', async () => {\n      let legendControl = element.querySelector(\n        '.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle'\n      );\n      const clickEvent = createMouseEvent('click');\n\n      legendControl.dispatchEvent(clickEvent);\n\n      await nextTick();\n\n      let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');\n      expect(legend.length).toBe(6);\n    });\n\n    // disable due to flakiness\n    xit('Renders X-axis ticks for the telemetry object', () => {\n      let xAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper'\n      );\n      expect(xAxisElement.length).toBe(1);\n\n      config.xAxis.set('displayRange', {\n        min: 0,\n        max: 4\n      });\n      let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick');\n      expect(ticks.length).toBe(9);\n    });\n\n    it('Renders Y-axis ticks for the telemetry object', async () => {\n      config.yAxis.set('displayRange', {\n        min: 10,\n        max: 20\n      });\n      await nextTick();\n      let yAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper'\n      );\n      expect(yAxisElement.length).toBe(1);\n      let ticks = yAxisElement[0].querySelectorAll('.gl-plot-tick');\n      expect(ticks.length).toBe(6);\n    });\n\n    it('Renders Y-axis options for the telemetry object', () => {\n      let yAxisElement = element.querySelectorAll(\n        '.gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select'\n      );\n      expect(yAxisElement.length).toBe(1);\n      let options = yAxisElement[0].querySelectorAll('option');\n      expect(options.length).toBe(2);\n      expect(options[0].value).toBe('Some attribute');\n      expect(options[1].value).toBe('Another attribute');\n    });\n\n    it('turns on cursor Guides all telemetry objects', async () => {\n      let cursorGuide = ref(plotViewComponentObject.cursorGuide);\n      expect(cursorGuide.value).toBeFalse();\n      cursorGuide.value = true;\n      await nextTick();\n      let childCursorGuides = element.querySelectorAll('.c-cursor-guide--v');\n      expect(childCursorGuides.length).toBe(1);\n    });\n\n    it('shows grid lines for all telemetry objects', () => {\n      expect(plotViewComponentObject.gridLines).toBeTrue();\n      let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks');\n      let visible = 0;\n      gridLinesContainer.forEach((el) => {\n        if (el.style.display !== 'none') {\n          visible++;\n        }\n      });\n      expect(visible).toBe(2);\n    });\n\n    it('hides grid lines for all telemetry objects', async () => {\n      let gridLines = ref(plotViewComponentObject.gridLines);\n      expect(gridLines.value).toBeTrue();\n      gridLines.value = false;\n      await nextTick();\n      expect(gridLines.value).toBeFalse();\n      let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks');\n      let visible = 0;\n      gridLinesContainer.forEach((el) => {\n        if (el.style.display && el.style.display !== 'none') {\n          visible++;\n        }\n      });\n      expect(visible).toBe(0);\n    });\n\n    xit('plots a new series when a new telemetry object is added', () => {\n      //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach\n      stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2];\n      mockCompositionList[0].emit('add', testTelemetryObject2);\n\n      let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name');\n      expect(legend.length).toBe(2);\n      expect(legend[1].innerHTML).toEqual('Test Object2');\n    });\n\n    it('removes plots from series when a telemetry object is removed', () => {\n      stackedPlotObject.composition = [];\n      mockCompositionList[0].emit('remove', testTelemetryObject.identifier);\n      expect(plotViewComponentObject.compositionObjects.length).toBe(0);\n    });\n\n    it('Changes the label of the y axis when the option changes', () => {\n      let selectEl = element.querySelector('.gl-plot-y-label__select');\n      selectEl.value = 'Another attribute';\n      selectEl.dispatchEvent(new Event('change'));\n\n      expect(config.yAxis.get('label')).toEqual('Another attribute');\n    });\n\n    it('Adds a new point to the plot', () => {\n      let originalLength = config.series.models[0].getSeriesData().length;\n      config.series.models[0].add({\n        utc: 2,\n        'some-key': 1,\n        'some-other-key': 2\n      });\n      const seriesData = config.series.models[0].getSeriesData();\n      expect(seriesData.length).toEqual(originalLength + 1);\n    });\n\n    it('updates the xscale', () => {\n      config.xAxis.set('displayRange', {\n        min: 0,\n        max: 10\n      });\n      expect(\n        plotViewComponentObject.$refs.stackedPlotItems[0].$refs.plotComponent.$refs.mctPlot.xScale.domain()\n      ).toEqual({\n        min: 0,\n        max: 10\n      });\n    });\n\n    it('updates the yscale', () => {\n      const yAxisList = [config.yAxis, ...config.additionalYAxes];\n      yAxisList.forEach((yAxis) => {\n        yAxis.set('displayRange', {\n          min: 10,\n          max: 20\n        });\n      });\n\n      const yAxesScales =\n        plotViewComponentObject.$refs.stackedPlotItems[0].$refs.plotComponent.$refs.mctPlot.yScale;\n      yAxesScales.forEach((yAxisScale) => {\n        expect(yAxisScale.scale.domain()).toEqual({\n          min: 10,\n          max: 20\n        });\n      });\n    });\n\n    it('shows styles for telemetry objects if available', () => {\n      let conditionalStylesContainer = element.querySelectorAll(\n        '.c-plot--stacked-container .js-style-receiver'\n      );\n      let hasStyles = 0;\n      conditionalStylesContainer.forEach((el) => {\n        if (el.style.backgroundColor) {\n          hasStyles++;\n        }\n      });\n      expect(hasStyles).toBe(1);\n    });\n  });\n\n  describe('the stacked plot inspector view', () => {\n    let viewComponentObject;\n    let mockComposition;\n    let testTelemetryObject;\n    let selection;\n    let config;\n    let destroyPlotOptions;\n    beforeEach(async () => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      selection = [\n        [\n          {\n            context: {\n              item: {\n                type: 'telemetry.plot.stacked',\n                identifier: {\n                  key: 'some-stacked-plot',\n                  namespace: ''\n                },\n                configuration: {\n                  series: []\n                }\n              }\n            }\n          }\n        ]\n      ];\n\n      openmct.router.path = [testTelemetryObject];\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testTelemetryObject);\n\n        return [testTelemetryObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);\n      config = new PlotConfigurationModel({\n        id: configId,\n        domainObject: selection[0][0].context.item,\n        openmct: openmct\n      });\n      configStore.add(configId, config);\n\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount(\n        {\n          components: {\n            PlotOptions\n          },\n          provide: {\n            openmct: openmct,\n            domainObject: selection[0][0].context.item,\n            path: [selection[0][0].context.item],\n            renderWhenVisible\n          },\n          template: '<plot-options/>'\n        },\n        {\n          element: viewContainer\n        }\n      );\n      destroyPlotOptions = destroy;\n\n      await nextTick();\n      viewComponentObject = vNode.componentInstance;\n    });\n\n    afterEach(() => {\n      destroyPlotOptions();\n      openmct.router.path = null;\n    });\n\n    describe('in view only mode', () => {\n      let browseOptionsEl;\n      beforeEach(() => {\n        browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');\n      });\n\n      it('shows legend properties', () => {\n        const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');\n        expect(legendPropertiesEl).not.toBeNull();\n      });\n\n      it('does not show series properties', () => {\n        const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');\n        expect(seriesPropertiesEl).toBeNull();\n      });\n\n      it('does not show yaxis properties', () => {\n        const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');\n        expect(yAxisPropertiesEl).toBeNull();\n      });\n    });\n  });\n\n  describe('inspector view of stacked plot child', () => {\n    let viewComponentObject;\n    let mockComposition;\n    let testTelemetryObject;\n    let selection;\n    let config;\n    let destroyPlotOptions;\n    beforeEach(async () => {\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        }\n      };\n\n      selection = [\n        [\n          {\n            context: {\n              item: {\n                id: 'test-object',\n                identifier: {\n                  key: 'test-object',\n                  namespace: ''\n                },\n                type: 'telemetry.plot.overlay',\n                configuration: {\n                  series: [\n                    {\n                      identifier: {\n                        key: 'test-object',\n                        namespace: ''\n                      }\n                    }\n                  ]\n                },\n                composition: []\n              }\n            }\n          },\n          {\n            context: {\n              item: {\n                type: 'telemetry.plot.stacked',\n                identifier: {\n                  key: 'some-stacked-plot',\n                  namespace: ''\n                },\n                configuration: {\n                  series: []\n                }\n              }\n            }\n          }\n        ]\n      ];\n\n      openmct.router.path = [testTelemetryObject];\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        mockComposition.emit('add', testTelemetryObject);\n\n        return [testTelemetryObject];\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);\n      config = new PlotConfigurationModel({\n        id: configId,\n        domainObject: selection[0][0].context.item,\n        openmct: openmct\n      });\n      configStore.add(configId, config);\n\n      let viewContainer = document.createElement('div');\n      child.append(viewContainer);\n      const { vNode, destroy } = mount(\n        {\n          components: {\n            PlotOptions\n          },\n          provide: {\n            openmct: openmct,\n            domainObject: selection[0][0].context.item,\n            path: [selection[0][0].context.item, selection[0][1].context.item],\n            renderWhenVisible\n          },\n          template: '<plot-options />'\n        },\n        {\n          element: viewContainer\n        }\n      );\n      destroyPlotOptions = destroy;\n\n      await nextTick();\n      viewComponentObject = vNode.componentInstance;\n    });\n\n    afterEach(() => {\n      destroyPlotOptions();\n      openmct.router.path = null;\n    });\n\n    describe('in view only mode', () => {\n      let browseOptionsEl;\n      beforeEach(() => {\n        browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');\n      });\n\n      it('hides legend properties', () => {\n        const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');\n        expect(legendPropertiesEl).toBeNull();\n      });\n\n      it('shows series properties', () => {\n        const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');\n        expect(seriesPropertiesEl).not.toBeNull();\n      });\n\n      it('shows yaxis properties', () => {\n        const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');\n        expect(yAxisPropertiesEl).not.toBeNull();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function stackedPlotConfigurationInterceptor(openmct) {\n  openmct.objects.addGetInterceptor({\n    appliesTo: (identifier, domainObject) => {\n      return (\n        domainObject?.type === 'telemetry.plot.stacked' &&\n        (!domainObject.configuration?.series || !domainObject.configuration?.objectStyles)\n      );\n    },\n    invoke: (identifier, domainObject) => {\n      if (!domainObject.configuration) {\n        domainObject.configuration = {};\n      }\n\n      if (!domainObject.configuration.series) {\n        domainObject.configuration.series = [];\n      }\n\n      if (!domainObject.configuration.objectStyles) {\n        domainObject.configuration.objectStyles = {};\n      }\n\n      return domainObject;\n    }\n  });\n}\n"
  },
  {
    "path": "src/plugins/plot/tickUtils.js",
    "content": "import { antisymlog, symlog } from './mathUtils.js';\n\nconst e10 = Math.sqrt(50);\nconst e5 = Math.sqrt(10);\nconst e2 = Math.sqrt(2);\n\n// A complete list of time units and their duration in milliseconds - UTC\nconst TIME_UNITS_UTC = [\n  { unit: 'millisecond', duration: 1 },\n  { unit: 'second', duration: 1000 },\n  { unit: 'minute', duration: 1000 * 60 },\n  { unit: 'hour', duration: 1000 * 60 * 60 },\n  { unit: 'day', duration: 1000 * 60 * 60 * 24 },\n  { unit: 'week', duration: 1000 * 60 * 60 * 24 * 7 },\n  { unit: 'month', duration: 1000 * 60 * 60 * 24 * 30.4375 }, // Average month\n  { unit: 'year', duration: 1000 * 60 * 60 * 24 * 365.25 } // Average year\n];\n\n/**\n * Nicely formatted tick steps from d3-array.\n */\nfunction tickStep(start, stop, count) {\n  const step0 = Math.abs(stop - start) / Math.max(0, count);\n  let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10));\n  const error = step0 / step1;\n  if (error >= e10) {\n    step1 *= 10;\n  } else if (error >= e5) {\n    step1 *= 5;\n  } else if (error >= e2) {\n    step1 *= 2;\n  }\n\n  return stop < start ? -step1 : step1;\n}\n\n/**\n * tickStep for time units - allows for snapping to 15/30 minutes and 6/12 hours, which are common intervals.\n */\nfunction timeTickStep(start, stop, count, unitName) {\n  const step0 = Math.abs(stop - start) / Math.max(0, count);\n  let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10));\n  const error = step0 / step1;\n\n  // For minutes and seconds, allow snapping to 15 and 30\n  if (unitName === 'minute' || unitName === 'second') {\n    // Snap to 1 hour/minute\n    if (error >= 45) {\n      return 60;\n    }\n    // Snap to 30s/30m\n    if (error >= 22.5) {\n      return 30;\n    }\n    // Snap to 15s/15m\n    if (error >= 12.5) {\n      return 15;\n    }\n  }\n\n  // For hours, use to 6 and 12\n  if (unitName === 'hour') {\n    // Snap to 1 day\n    if (error >= 18) {\n      return 24;\n    }\n    if (error >= 9) {\n      return 12;\n    }\n    if (error >= 4.5) {\n      return 6;\n    }\n  }\n\n  // Fallback to standard tickStep that already snaps to 1, 2, 5, 10\n  if (error >= e10) {\n    step1 *= 10;\n  } else if (error >= e5) {\n    step1 *= 5;\n  } else if (error >= e2) {\n    step1 *= 2;\n  }\n\n  return stop < start ? -step1 : step1;\n}\n\n/**\n * Generate time ticks based on a start and stop time, and a desired count of ticks calculated proactively from canvas size\n * @param start beginning timestamp in Ms\n * @param stop  ending timestamp in Ms\n * @param count desired number of ticks\n * @returns {*[]} Array of timestamps in Ms\n */\nexport function getTimeTicks(start, stop, count) {\n  const duration = stop - start;\n  let bestUnit = TIME_UNITS_UTC[0];\n\n  // Find the unit where the duration divided by unit size is closest to our target count.\n  for (const unit of TIME_UNITS_UTC) {\n    const ticksForUnit = duration / unit.duration;\n    if (ticksForUnit <= count) {\n      break;\n    }\n    bestUnit = unit;\n  }\n\n  // Normalize the range to the selected unit to find a \"nice\" step size\n  const startInUnits = start / bestUnit.duration;\n  const stopInUnits = stop / bestUnit.duration;\n\n  // Use specialized time stepping for seconds/minutes/hours\n  const bestStepSize = Math.abs(timeTickStep(startInUnits, stopInUnits, count, bestUnit.unit));\n\n  if (bestUnit.unit === 'month' || bestUnit.unit === 'year') {\n    return generateMonthYearTicks(start, stop, bestUnit.unit, bestStepSize);\n  } else {\n    return generateFixedIntervalTicks(start, stop, bestUnit.duration * bestStepSize);\n  }\n}\n\n// Helper for variable-duration units (months, years)\n/**\n * Generate ticks for month/year intervals - these are variable due to leap years etc.\n * @param start beginning timestamp in Ms\n * @param stop ending timestamp in Ms\n * @param unit 'month' or 'year'\n * @param stepSize number of months/years to step\n * @returns {*[]} Array of timestamps in Ms\n */\nfunction generateMonthYearTicks(start, stop, unit, stepSize) {\n  const resultingTicks = [];\n  let currentDate = new Date(start);\n\n  // Use UTC to avoid DST issues.\n  // Set to the beginning of the interval (e.g., beginning of the month/year)\n  if (unit === 'month') {\n    // currentDate.setDate(1);\n    currentDate.setUTCDate(1);\n    currentDate.setUTCHours(0, 0, 0, 0);\n  } else if (unit === 'year') {\n    // currentDate.setMonth(0, 1);\n    currentDate.setUTCMonth(0, 1);\n    currentDate.setUTCHours(0, 0, 0, 0);\n  }\n\n  while (currentDate.getTime() <= stop) {\n    resultingTicks.push(currentDate.getTime());\n    if (unit === 'month') {\n      // currentDate.setMonth(currentDate.getMonth() + stepSize);\n      currentDate.setUTCMonth(currentDate.getUTCMonth() + stepSize);\n    } else {\n      // unit is 'year'\n      // currentDate.setFullYear(currentDate.getFullYear() + stepSize);\n      currentDate.setUTCFullYear(currentDate.getUTCFullYear() + stepSize);\n    }\n  }\n  return resultingTicks;\n}\n\n// Helper for fixed-duration units (seconds, days)\n/**\n * Generate ticks for fixed-duration intervals (seconds, minutes, hours, etc.)\n * @param start beginning timestamp in Ms\n * @param stop ending timestamp in Ms\n * @param interval duration of each tick in Ms\n * @returns {*[]} Array of timestamps in Ms\n */\nfunction generateFixedIntervalTicks(start, stop, interval) {\n  const fixedIntervalTicks = [];\n  const firstTick = Math.ceil(start / interval) * interval;\n\n  for (let i = firstTick; i <= stop; i += interval) {\n    fixedIntervalTicks.push(i);\n  }\n\n  return fixedIntervalTicks;\n}\n\n/**\n * Find the precision (number of decimals) of a step.  Used to round\n * ticks to precise values.\n */\nfunction getPrecision(step) {\n  const exponential = step.toExponential();\n  const i = exponential.indexOf('e');\n  if (i === -1) {\n    return 0;\n  }\n\n  let precision = Math.max(0, -Number(exponential.slice(i + 1)));\n\n  if (precision > 20) {\n    precision = 20;\n  }\n\n  return precision;\n}\n\nexport function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) {\n  // log()'ed values\n  const mainLogTicks = ticks(start, stop, mainTickCount);\n\n  // original values\n  const mainTicks = mainLogTicks.map((n) => antisymlog(n, 10));\n\n  const result = [];\n\n  let i = 0;\n  for (const logTick of mainLogTicks) {\n    result.push(logTick);\n\n    if (i === mainLogTicks.length - 1) {\n      break;\n    }\n\n    const tick = mainTicks[i];\n    const nextTick = mainTicks[i + 1];\n    const rangeBetweenMainTicks = nextTick - tick;\n\n    const secondaryLogTicks = ticks(\n      tick + rangeBetweenMainTicks / (secondaryTickCount + 1),\n      nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1),\n      secondaryTickCount - 2\n    ).map((n) => symlog(n, 10));\n\n    result.push(...secondaryLogTicks);\n\n    i++;\n  }\n\n  return result;\n}\n\n/**\n * Linear tick generation from d3-array.\n */\nexport function ticks(start, stop, count) {\n  const step = tickStep(start, stop, count);\n  const precision = getPrecision(step);\n\n  return _.range(\n    Math.ceil(start / step) * step,\n    Math.floor(stop / step) * step + step / 2, // inclusive\n    step\n  ).map(function round(tick) {\n    return Number(tick.toFixed(precision));\n  });\n}\n\nexport function commonPrefix(a, b) {\n  const maxLen = Math.min(a.length, b.length);\n  let breakpoint = 0;\n  for (let i = 0; i < maxLen; i++) {\n    if (a[i] !== b[i]) {\n      break;\n    }\n\n    if (a[i] === ' ') {\n      breakpoint = i + 1;\n    }\n  }\n\n  return a.slice(0, breakpoint);\n}\n\nexport function commonSuffix(a, b) {\n  const maxLen = Math.min(a.length, b.length);\n  let breakpoint = 0;\n  for (let i = 0; i <= maxLen; i++) {\n    if (a[a.length - i] !== b[b.length - i]) {\n      break;\n    }\n\n    if ('. '.indexOf(a[a.length - i]) !== -1) {\n      breakpoint = i;\n    }\n  }\n\n  return a.slice(a.length - breakpoint);\n}\n\nexport function getFormattedTicks(newTicks, format) {\n  newTicks = newTicks.map(function (tickValue) {\n    return {\n      value: tickValue,\n      text: format(tickValue)\n    };\n  });\n\n  if (newTicks.length && typeof newTicks[0].text === 'string') {\n    const tickText = newTicks.map(function (t) {\n      return t.text;\n    });\n    const prefix = tickText.reduce(commonPrefix);\n    const suffix = tickText.reduce(commonSuffix);\n    newTicks.forEach(function (t) {\n      t.fullText = t.text;\n\n      if (typeof t.text === 'string') {\n        if (newTicks.length > 1) {\n          if (suffix.length) {\n            t.text = t.text.slice(prefix.length, -suffix.length);\n          } else {\n            t.text = t.text.slice(prefix.length);\n          }\n        }\n      }\n    });\n  }\n\n  return newTicks;\n}\n\n/**\n * Proactively measures text width using a canvas context.\n */\nlet measurementContext;\n\nexport function measureTextWidth(text, font = '12px \"Open Sans\", sans-serif') {\n  if (!measurementContext) {\n    const canvas = document.createElement('canvas');\n    measurementContext = canvas.getContext('2d');\n  }\n  measurementContext.font = font;\n  return measurementContext.measureText(text).width;\n}\n"
  },
  {
    "path": "src/plugins/plugins.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ExampleDataVisualizationSourcePlugin from '../../example/dataVisualization/plugin.js';\nimport EventGeneratorPlugin from '../../example/eventGenerator/plugin.js';\nimport ExampleStaleness from '../../example/exampleStalenessProvider/plugin.js';\nimport ExampleTags from '../../example/exampleTags/plugin.js';\nimport ExampleUser from '../../example/exampleUser/plugin.js';\nimport ExampleFaultSource from '../../example/faultManagement/exampleFaultSource.js';\nimport GeneratorPlugin from '../../example/generator/plugin.js';\nimport ExampleImagery from '../../example/imagery/plugin.js';\nimport AutoflowPlugin from './autoflow/AutoflowTabularPlugin.js';\nimport BarChartPlugin from './charts/bar/plugin.js';\nimport ScatterPlotPlugin from './charts/scatter/plugin.js';\nimport ClearData from './clearData/plugin.js';\nimport Clock from './clock/plugin.js';\nimport DerivedTelemetryPlugin from './comps/plugin.js';\nimport ConditionPlugin from './condition/plugin.js';\nimport ConditionWidgetPlugin from './conditionWidget/plugin.js';\nimport CorrelationTelemetryPlugin from './correlationTelemetryPlugin/plugin.js';\nimport CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';\nimport DefaultRootName from './defaultRootName/plugin.js';\nimport DeviceClassifier from './DeviceClassifier/plugin.js';\nimport DisplayLayoutPlugin from './displayLayout/plugin.js';\nimport EventTimestripPlugin from './events/plugin.js';\nimport FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';\nimport Filters from './filters/plugin.js';\nimport FlexibleLayout from './flexibleLayout/plugin.js';\nimport FolderView from './folderView/plugin.js';\nimport FormActions from './formActions/plugin.js';\nimport GaugePlugin from './gauge/GaugePlugin.js';\nimport GoToOriginalAction from './goToOriginalAction/plugin.js';\nimport Hyperlink from './hyperlink/plugin.js';\nimport ImageryPlugin from './imagery/plugin.js';\nimport InspectorDataVisualization from './inspectorDataVisualization/plugin.js';\nimport InspectorViews from './inspectorViews/plugin.js';\nimport ObjectInterceptors from './interceptors/plugin.js';\nimport ISOTimeFormat from './ISOTimeFormat/plugin.js';\nimport LADTable from './LADTable/plugin.js';\nimport LocalStorage from './localStorage/plugin.js';\nimport LocalTimeSystem from './localTimeSystem/plugin.js';\nimport MyItems from './myItems/plugin.js';\nimport NewFolderAction from './newFolderAction/plugin.js';\nimport { NotebookPlugin, RestrictedNotebookPlugin } from './notebook/plugin.js';\nimport NotificationIndicator from './notificationIndicator/plugin.js';\nimport ObjectMigration from './objectMigration/plugin.js';\nimport OpenInNewTabAction from './openInNewTabAction/plugin.js';\nimport OperatorStatus from './operatorStatus/plugin.js';\nimport PerformanceIndicator from './performanceIndicator/plugin.js';\nimport CouchDBPlugin from './persistence/couch/plugin.js';\nimport PlanLayout from './plan/plugin.js';\nimport PlotPlugin from './plot/plugin.js';\nimport ReloadAction from './reloadAction/plugin.js';\nimport RemoteClock from './remoteClock/plugin.js';\nimport StaticRootPlugin from './staticRootPlugin/plugin.js';\nimport SummaryWidget from './summaryWidget/plugin.js';\nimport Tabs from './tabs/plugin.js';\nimport TelemetryMean from './telemetryMean/plugin.js';\nimport TelemetryTablePlugin from './telemetryTable/plugin.js';\nimport DarkMatter from './themes/darkmatter.js';\nimport Espresso from './themes/espresso.js';\nimport Snow from './themes/snow.js';\nimport TimeConductorPlugin from './timeConductor/plugin.js';\nimport Timeline from './timeline/plugin.js';\nimport TimeList from './timelist/plugin.js';\nimport Timer from './timer/plugin.js';\nimport URLIndicatorPlugin from './URLIndicatorPlugin/URLIndicatorPlugin.js';\nimport URLTimeSettingsSynchronizer from './URLTimeSettingsSynchronizer/plugin.js';\nimport UserIndicator from './userIndicator/plugin.js';\nimport UTCTimeSystem from './utcTimeSystem/plugin.js';\nimport ViewDatumAction from './viewDatumAction/plugin.js';\nimport ViewLargeAction from './viewLargeAction/plugin.js';\nimport WebPagePlugin from './webPage/plugin.js';\n\n/**\n * @type {Object}\n */\nconst plugins = {};\n\nplugins.example = {};\nplugins.example.ExampleUser = ExampleUser;\nplugins.example.ExampleImagery = ExampleImagery;\nplugins.example.ExampleFaultSource = ExampleFaultSource;\nplugins.example.EventGeneratorPlugin = EventGeneratorPlugin;\nplugins.example.ExampleDataVisualizationSourcePlugin = ExampleDataVisualizationSourcePlugin;\nplugins.example.ExampleTags = ExampleTags;\nplugins.example.Generator = () => GeneratorPlugin;\nplugins.example.ExampleStaleness = ExampleStaleness;\n\nplugins.UTCTimeSystem = UTCTimeSystem;\nplugins.LocalTimeSystem = LocalTimeSystem;\nplugins.RemoteClock = RemoteClock;\n\nplugins.MyItems = MyItems;\n\nplugins.StaticRootPlugin = StaticRootPlugin;\n\n/**\n * A tabular view showing the latest values of multiple telemetry points at\n * once. Formatted so that labels and values are aligned.\n *\n * @param {Object} [options] Optional settings to apply to the autoflow\n * tabular view. Currently supports one option, 'type'.\n * @param {string} [options.type] The key of an object type to apply this view\n * to exclusively.\n */\nplugins.AutoflowView = AutoflowPlugin;\n\nplugins.Conductor = TimeConductorPlugin;\n\nplugins.CouchDB = CouchDBPlugin;\n\nplugins.ImageryPlugin = ImageryPlugin;\nplugins.Plot = PlotPlugin;\nplugins.BarChart = BarChartPlugin;\nplugins.ScatterPlot = ScatterPlotPlugin;\nplugins.TelemetryTable = TelemetryTablePlugin;\nplugins.SummaryWidget = SummaryWidget;\nplugins.TelemetryMean = TelemetryMean;\nplugins.URLIndicator = URLIndicatorPlugin;\nplugins.Notebook = NotebookPlugin;\nplugins.RestrictedNotebook = RestrictedNotebookPlugin;\nplugins.DisplayLayout = DisplayLayoutPlugin;\nplugins.FaultManagement = FaultManagementPlugin;\nplugins.FormActions = FormActions;\nplugins.FolderView = FolderView;\nplugins.Tabs = Tabs;\nplugins.FlexibleLayout = FlexibleLayout;\nplugins.LADTable = LADTable;\nplugins.Filters = Filters;\nplugins.ObjectMigration = ObjectMigration;\nplugins.GoToOriginalAction = GoToOriginalAction;\nplugins.OpenInNewTabAction = OpenInNewTabAction;\nplugins.ReloadAction = ReloadAction;\nplugins.ClearData = ClearData;\nplugins.WebPage = WebPagePlugin;\nplugins.DarkmatterTheme = DarkMatter;\nplugins.Espresso = Espresso;\nplugins.Snow = Snow;\nplugins.Condition = ConditionPlugin;\nplugins.ConditionWidget = ConditionWidgetPlugin;\nplugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer;\nplugins.NotificationIndicator = NotificationIndicator;\nplugins.NewFolderAction = NewFolderAction;\nplugins.ISOTimeFormat = ISOTimeFormat;\nplugins.DefaultRootName = DefaultRootName;\nplugins.PlanLayout = PlanLayout;\nplugins.ViewDatumAction = ViewDatumAction;\nplugins.ViewLargeAction = ViewLargeAction;\nplugins.ObjectInterceptors = ObjectInterceptors;\nplugins.PerformanceIndicator = PerformanceIndicator;\nplugins.CouchDBSearchFolder = CouchDBSearchFolder;\nplugins.Timeline = Timeline;\nplugins.Hyperlink = Hyperlink;\nplugins.Clock = Clock;\nplugins.Timer = Timer;\nplugins.DeviceClassifier = DeviceClassifier;\nplugins.UserIndicator = UserIndicator;\nplugins.LocalStorage = LocalStorage;\nplugins.OperatorStatus = OperatorStatus;\nplugins.Gauge = GaugePlugin;\nplugins.Timelist = TimeList;\nplugins.InspectorViews = InspectorViews;\nplugins.InspectorDataVisualization = InspectorDataVisualization;\nplugins.CorrelationTelemetry = CorrelationTelemetryPlugin;\nplugins.DerivedTelemetry = DerivedTelemetryPlugin;\nplugins.EventTimestripPlugin = EventTimestripPlugin;\n\nexport default plugins;\n"
  },
  {
    "path": "src/plugins/reloadAction/ReloadAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst RELOAD_ACTION_KEY = 'reload';\n\nclass ReloadAction {\n  constructor(openmct) {\n    this.name = 'Reload';\n    this.key = RELOAD_ACTION_KEY;\n    this.description = 'Reload this object and its children';\n    this.group = 'action';\n    this.priority = 10;\n    this.cssClass = 'icon-refresh';\n\n    this.openmct = openmct;\n  }\n  invoke(objectPath, view) {\n    const domainObject = objectPath[0];\n    this.openmct.objectViews.emit('reload', domainObject);\n  }\n}\n\nexport { RELOAD_ACTION_KEY };\n\nexport default ReloadAction;\n"
  },
  {
    "path": "src/plugins/reloadAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport ReloadAction from './ReloadAction.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.actions.register(new ReloadAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/remoteClock/RemoteClock.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport DefaultClock from '../../utils/clock/DefaultClock.js';\nimport remoteClockRequestInterceptor from './requestInterceptor.js';\n\n/**\n * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the\n * application based on a time providing telemetry domainObject.\n *\n * @param {openmct} Object Instance of OpenMCT\n * @param {module:openmct.ObjectAPI~Identifier} identifier An object identifier for\n * the time providing telemetry domainObject\n * @constructor\n */\n\nexport default class RemoteClock extends DefaultClock {\n  constructor(openmct, identifier) {\n    super();\n\n    this.key = 'remote-clock';\n\n    this.openmct = openmct;\n    this.identifier = identifier;\n\n    this.name = 'Remote Clock';\n    this.description = 'Provides telemetry based timestamps from a configurable source.';\n\n    this.timeTelemetryObject = undefined;\n    this.parseTime = undefined;\n    this.formatTime = undefined;\n    this.metadata = undefined;\n\n    this.lastTick = 0;\n\n    this.openmct.telemetry.addRequestInterceptor(\n      remoteClockRequestInterceptor(this.openmct, this.identifier, this.#waitForReady.bind(this))\n    );\n\n    this._processDatum = this._processDatum.bind(this);\n  }\n\n  start() {\n    this.openmct.objects.get(this.identifier).then((domainObject) => {\n      // The start method is called when at least one listener registers with the clock.\n      // When the clock is changed, listeners are unregistered from the clock and the stop method is called.\n      // Sometimes, the objects.get call above does not resolve before the stop method is called.\n      // So when we proceed with the clock subscription below, we first need to ensure that there is at least one listener for our clock.\n      if (this.eventNames().length === 0) {\n        return;\n      }\n      this.openmct.time.on('timeSystem', this._timeSystemChange);\n      this.timeTelemetryObject = domainObject;\n      this.metadata = this.openmct.telemetry.getMetadata(domainObject);\n      this._timeSystemChange();\n      this._requestLatest();\n      this._subscribe();\n    });\n  }\n\n  stop() {\n    this.openmct.time.off('timeSystem', this._timeSystemChange);\n    if (this._unsubscribe) {\n      this._unsubscribe();\n    }\n\n    this.removeAllListeners();\n  }\n\n  /**\n   * Will start a subscription to the timeTelemetryObject as well\n   * handle the unsubscribe callback\n   *\n   * @private\n   */\n  _subscribe() {\n    this._unsubscribe = this.openmct.telemetry.subscribe(\n      this.timeTelemetryObject,\n      this._processDatum\n    );\n  }\n\n  /**\n   * Will request the latest data for the timeTelemetryObject\n   *\n   * @private\n   */\n  _requestLatest() {\n    this.openmct.telemetry\n      .request(this.timeTelemetryObject, {\n        size: 1,\n        strategy: 'latest'\n      })\n      .then((data) => {\n        this._processDatum(data[data.length - 1]);\n      });\n  }\n\n  /**\n   * Function to parse the datum from the timeTelemetryObject as well\n   * as check if it's valid, calls \"tick\"\n   *\n   * @private\n   */\n  _processDatum(datum) {\n    let time = this.parseTime(datum);\n\n    if (time > this.lastTick) {\n      this.tick(time);\n    }\n  }\n\n  /**\n   * Callback function for timeSystem change events\n   *\n   * @private\n   */\n  _timeSystemChange() {\n    let timeSystem = this.openmct.time.getTimeSystem();\n    let timeKey = timeSystem.key;\n    let metadataValue = this.metadata.value(timeKey);\n    let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n    this.parseTime = (datum) => {\n      return timeFormatter.parse(datum);\n    };\n\n    this.formatTime = (datum) => {\n      return timeFormatter.format(datum);\n    };\n  }\n\n  /**\n   * Waits for the clock to have a non-default tick value.\n   */\n  #waitForReady() {\n    const waitForInitialTick = (resolve) => {\n      const tickListener = () => {\n        if (this.lastTick > 0) {\n          const offsets = this.openmct.time.getClockOffsets();\n          this.openmct.time.off('tick', tickListener); // Unregister the tick listener\n          resolve({\n            start: this.lastTick + offsets.start,\n            end: this.lastTick + offsets.end\n          });\n        }\n      };\n\n      this.openmct.time.on('tick', tickListener);\n    };\n\n    return new Promise(waitForInitialTick);\n  }\n}\n"
  },
  {
    "path": "src/plugins/remoteClock/RemoteClockSpec.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nconst REMOTE_CLOCK_KEY = 'remote-clock';\nconst TIME_TELEMETRY_ID = {\n  namespace: 'remote',\n  key: 'telemetry'\n};\nconst TIME_VALUE = 12345;\nconst REQ_OPTIONS = {\n  size: 1,\n  strategy: 'latest'\n};\nconst OFFSET_START = -10;\nconst OFFSET_END = 1;\n\ndescribe('the RemoteClock plugin', () => {\n  let openmct;\n  let object = {\n    name: 'remote-telemetry',\n    identifier: TIME_TELEMETRY_ID\n  };\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('once installed', () => {\n    let remoteClock;\n    let boundsCallback;\n    let metadataValue = { some: 'value' };\n    let timeSystem = { key: 'utc' };\n    let metadata = {\n      value: () => metadataValue\n    };\n    let reqDatum = {\n      key: TIME_VALUE\n    };\n\n    let formatter = {\n      parse: (datum) => datum.key\n    };\n\n    let objectPromise;\n    let requestPromise;\n\n    beforeEach(() => {\n      openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));\n\n      let clocks = openmct.time.getAllClocks();\n      remoteClock = clocks.filter((clock) => clock.key === REMOTE_CLOCK_KEY)[0];\n\n      boundsCallback = jasmine.createSpy('boundsCallback');\n      openmct.time.on('bounds', boundsCallback);\n\n      spyOn(remoteClock, '_timeSystemChange').and.callThrough();\n      spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata);\n      spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter);\n      spyOn(openmct.telemetry, 'subscribe').and.callThrough();\n      spyOn(openmct.time, 'on').and.callThrough();\n      spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem);\n      spyOn(metadata, 'value').and.callThrough();\n\n      let requestPromiseResolve;\n      let objectPromiseResolve;\n\n      requestPromise = new Promise((resolve) => {\n        requestPromiseResolve = resolve;\n      });\n      spyOn(openmct.telemetry, 'request').and.callFake(() => {\n        requestPromiseResolve([reqDatum]);\n\n        return requestPromise;\n      });\n\n      objectPromise = new Promise((resolve) => {\n        objectPromiseResolve = resolve;\n      });\n      spyOn(openmct.objects, 'get').and.callFake(() => {\n        objectPromiseResolve(object);\n\n        return objectPromise;\n      });\n\n      openmct.time.clock(REMOTE_CLOCK_KEY, {\n        start: OFFSET_START,\n        end: OFFSET_END\n      });\n    });\n\n    afterEach(() => {\n      openmct.time.setClock('local');\n    });\n\n    it('Does not throw error if time system is changed before remote clock initialized', () => {\n      expect(() => openmct.time.timeSystem('utc')).not.toThrow();\n    });\n\n    describe('once resolved', () => {\n      beforeEach(async () => {\n        await Promise.all([objectPromise, requestPromise]);\n      });\n\n      it('is available and sets up initial values and listeners', () => {\n        expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);\n        expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);\n        expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);\n        expect(remoteClock._timeSystemChange).toHaveBeenCalled();\n      });\n\n      it('will request/store the object based on the identifier passed in', () => {\n        expect(remoteClock.timeTelemetryObject).toEqual(object);\n      });\n\n      it('will request metadata and set up formatters', () => {\n        expect(remoteClock.metadata).toEqual(metadata);\n        expect(metadata.value).toHaveBeenCalled();\n        expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);\n      });\n\n      it('will request the latest datum for the object it received and process the datum returned', () => {\n        expect(openmct.telemetry.request).toHaveBeenCalledWith(\n          remoteClock.timeTelemetryObject,\n          REQ_OPTIONS\n        );\n        expect(boundsCallback).toHaveBeenCalledWith(\n          {\n            start: TIME_VALUE + OFFSET_START,\n            end: TIME_VALUE + OFFSET_END\n          },\n          true\n        );\n      });\n\n      it('will set up subscriptions correctly', () => {\n        expect(remoteClock._unsubscribe).toBeDefined();\n        expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(\n          remoteClock.timeTelemetryObject,\n          remoteClock._processDatum\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/remoteClock/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport RemoteClock from './RemoteClock.js';\n/**\n * Install a clock that uses a configurable telemetry endpoint.\n */\n\nexport default function (identifier) {\n  return function (openmct) {\n    openmct.time.addClock(new RemoteClock(openmct, identifier));\n  };\n}\n"
  },
  {
    "path": "src/plugins/remoteClock/requestInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Intercepts requests to ensure the remote clock is ready.\n *\n * @param {import('../../openmct').OpenMCT} openmct - The OpenMCT instance.\n * @param {import('../../openmct').Identifier} _remoteClockIdentifier - The identifier for the remote clock.\n * @param {Function} waitForBounds - A function that returns a promise resolving to the initial bounds.\n * @returns {Object} The request interceptor.\n */\nfunction remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) {\n  let remoteClockLoaded = false;\n\n  return {\n    /**\n     * Determines if the interceptor applies to the given request.\n     *\n     * @param {Object} _ - Unused parameter.\n     * @param {import('../../api/telemetry/TelemetryAPI').TelemetryRequestOptions} request - The request object.\n     * @returns {boolean} True if the interceptor applies, false otherwise.\n     */\n    appliesTo: (_, request) => {\n      // Get the activeClock from the Global Time Context\n      /** @type {import(\"../../api/time/TimeContext\").default} */\n      const { activeClock } = openmct.time;\n\n      // this type of request does not rely on clock having bounds\n      if (request.strategy === 'latest' && request.timeContext.isRealTime()) {\n        return false;\n      }\n\n      return activeClock?.key === 'remote-clock' && !remoteClockLoaded;\n    },\n    /**\n     * Invokes the interceptor to modify the request.\n     *\n     * @param {Object} request - The request object.\n     * @returns {Promise<Object>} The modified request object.\n     */\n    invoke: async (request) => {\n      const timeContext = request?.timeContext ?? openmct.time;\n\n      // Wait for initial bounds if the request is for real-time data.\n      // Otherwise, use the bounds provided by the request.\n      if (timeContext.isRealTime()) {\n        const { start, end } = await waitForBounds();\n        remoteClockLoaded = true;\n        request.start = start;\n        request.end = end;\n      }\n\n      return request;\n    }\n  };\n}\n\nexport default remoteClockRequestInterceptor;\n"
  },
  {
    "path": "src/plugins/remove/RemoveAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst SPECIAL_MESSAGE_TYPES = ['layout', 'flexible-layout'];\nconst REMOVE_ACTION_KEY = 'remove';\n\nclass RemoveAction {\n  #transaction;\n\n  constructor(openmct) {\n    this.name = 'Remove';\n    this.key = REMOVE_ACTION_KEY;\n    this.description = 'Remove this object from its containing object.';\n    this.cssClass = 'icon-trash';\n    this.group = 'action';\n    this.priority = 1;\n\n    this.openmct = openmct;\n\n    this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable\n    this.#transaction = null;\n  }\n\n  async invoke(objectPath) {\n    const child = objectPath[0];\n    const parent = objectPath[1];\n\n    try {\n      await this.showConfirmDialog(child, parent);\n    } catch (error) {\n      return; // form canceled, exit invoke\n    }\n\n    await this.removeFromComposition(parent, child, objectPath);\n\n    if (this.inNavigationPath(child)) {\n      this.navigateTo(objectPath.slice(1));\n    }\n  }\n\n  showConfirmDialog(child, parent) {\n    let message =\n      'Warning! This action will remove this object. Are you sure you want to continue?';\n\n    if (SPECIAL_MESSAGE_TYPES.includes(parent.type)) {\n      const type = this.openmct.types.get(parent.type);\n      const typeName = type.definition.name;\n\n      message = `Warning! This action will remove this item from the ${typeName}. Are you sure you want to continue?`;\n    }\n\n    return new Promise((resolve, reject) => {\n      const dialog = this.openmct.overlays.dialog({\n        title: `Remove ${child.name}`,\n        iconClass: 'alert',\n        message,\n        buttons: [\n          {\n            label: 'Ok',\n            callback: () => {\n              dialog.dismiss();\n              resolve();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              dialog.dismiss();\n              reject();\n            }\n          }\n        ]\n      });\n    });\n  }\n\n  inNavigationPath(object) {\n    return this.openmct.router.path.some((objectInPath) =>\n      this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)\n    );\n  }\n\n  navigateTo(objectPath) {\n    let urlPath = objectPath\n      .reverse()\n      .map((object) => this.openmct.objects.makeKeyString(object.identifier))\n      .join('/');\n\n    this.openmct.router.navigate('#/browse/' + urlPath);\n  }\n\n  async removeFromComposition(parent, child, objectPath) {\n    this.startTransaction();\n\n    const composition = this.openmct.composition.get(parent);\n    composition.remove(child);\n\n    if (!this.openmct.objects.isObjectPathToALink(child, objectPath)) {\n      this.openmct.objects.mutate(child, 'location', null);\n    }\n\n    if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) {\n      this.openmct.editor.save();\n    }\n\n    await this.saveTransaction();\n  }\n\n  appliesTo(objectPath) {\n    const parent = objectPath[1];\n    const parentType = parent && this.openmct.types.get(parent.type);\n    const child = objectPath[0];\n    const locked = child.locked ? child.locked : parent && parent.locked;\n    const isEditing = this.openmct.editor.isEditing();\n    const isPersistable = this.openmct.objects.isPersistable(child.identifier);\n    const isLink = this.openmct.objects.isObjectPathToALink(child, objectPath);\n\n    if (!isLink && (locked || !isPersistable)) {\n      return false;\n    }\n\n    if (isEditing) {\n      if (this.openmct.router.isNavigatedObject(objectPath)) {\n        return false;\n      }\n    }\n\n    return parentType?.definition.creatable && Array.isArray(parent?.composition);\n  }\n\n  startTransaction() {\n    if (!this.openmct.objects.isTransactionActive()) {\n      this.#transaction = this.openmct.objects.startTransaction();\n    }\n  }\n\n  async saveTransaction() {\n    if (!this.#transaction) {\n      return;\n    }\n\n    await this.#transaction.commit();\n    this.openmct.objects.endTransaction();\n    this.#transaction = null;\n  }\n}\n\nexport { REMOVE_ACTION_KEY };\n\nexport default RemoveAction;\n"
  },
  {
    "path": "src/plugins/remove/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport RemoveAction from './RemoveAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new RemoveAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/remove/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, getMockObjects, resetApplicationState } from 'utils/testing';\n\ndescribe('The Remove Action plugin', () => {\n  let openmct;\n  let removeAction;\n  let childObject;\n  let parentObject;\n\n  // this setups up the app\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    childObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          name: 'Child Folder',\n          identifier: {\n            namespace: '',\n            key: 'child-folder-object'\n          }\n        }\n      }\n    }).folder;\n    parentObject = getMockObjects({\n      objectKeyStrings: ['folder'],\n      overwrite: {\n        folder: {\n          identifier: {\n            namespace: '',\n            key: 'parent-folder-object'\n          },\n          name: 'Parent Folder',\n          composition: [childObject.identifier]\n        }\n      }\n    }).folder;\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    removeAction = openmct.actions._allActions.remove;\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should be defined', () => {\n    expect(removeAction).toBeDefined();\n  });\n\n  describe('when removing an object from a parent composition', () => {\n    beforeEach(() => {\n      spyOn(removeAction, 'removeFromComposition').and.callThrough();\n      spyOn(removeAction, 'inNavigationPath').and.returnValue(false);\n      spyOn(openmct.objects, 'mutate').and.callThrough();\n      spyOn(openmct.objects, 'startTransaction').and.callThrough();\n      spyOn(openmct.objects, 'endTransaction').and.callThrough();\n      removeAction.removeFromComposition(parentObject, childObject);\n    });\n\n    it('removeFromComposition should be called with the parent and child', () => {\n      expect(removeAction.removeFromComposition).toHaveBeenCalled();\n      expect(removeAction.removeFromComposition).toHaveBeenCalledWith(parentObject, childObject);\n    });\n\n    it('it should mutate the parent object', () => {\n      expect(openmct.objects.mutate).toHaveBeenCalled();\n      expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject);\n    });\n\n    it('it should start a transaction', () => {\n      expect(openmct.objects.startTransaction).toHaveBeenCalled();\n    });\n\n    it('it should end the transaction', (done) => {\n      setTimeout(() => {\n        expect(openmct.objects.endTransaction).toHaveBeenCalled();\n        done();\n      }, 100);\n    });\n  });\n\n  describe('when determining the object is applicable', () => {\n    beforeEach(() => {\n      spyOn(removeAction, 'appliesTo').and.callThrough();\n    });\n\n    it('should be true when the parent is creatable and has composition', () => {\n      let applies = removeAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(true);\n    });\n\n    it('should be false when the child is locked and not an alias', () => {\n      childObject.locked = true;\n      childObject.location = 'parent-folder-object';\n      let applies = removeAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(false);\n    });\n\n    it('should be true when the child is locked and IS an alias', () => {\n      childObject.locked = true;\n      childObject.location = 'other-folder-object';\n      let applies = removeAction.appliesTo([childObject, parentObject]);\n      expect(applies).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/staticRootPlugin/README.md",
    "content": "# Static Root Plugin\n\nThis plugin takes an object tree as JSON and exposes it as a non-editable root level tree. This can be useful if you \nhave static non-editable content that you wish to expose, such as a standard set of displays that should not be edited \n(but which can be copied and then modified if desired).\n\nAny object tree in Open MCT can be exported as JSON after installing the \n[Import/Export plugin](../../../platform/import-export/README.md).\n\n## Installation\n``` js\nopenmct.install(openmct.plugins.StaticRootPlugin({ namespace: 'mission', exportUrl: 'data/static-objects.json' }));\n```\n\n## Parameters\nThe StaticRootPlugin takes two parameters:\n1. __namespace__: This should be a name that uniquely identifies this collection of objects.\n2. __exportUrl__: The file that the static tree should be exposed from. This will need to be a path that is reachable by a web \nbrowser, ie not a path on the local file system."
  },
  {
    "path": "src/plugins/staticRootPlugin/StaticModelProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Transforms an import json blob into a object map that can be used to\n * provide objects.  Rewrites root identifier in import data with provided\n * rootIdentifier, and rewrites all child object identifiers so that they\n * exist in the same namespace as the rootIdentifier.\n */\nimport { makeKeyString, parseKeyString, toNewFormat } from 'objectUtils';\n\nclass StaticModelProvider {\n  constructor(importData, rootIdentifier) {\n    this.objectMap = {};\n    this.rewriteModel(importData, rootIdentifier);\n  }\n\n  /**\n   * Standard \"Get\".\n   */\n  get(identifier) {\n    const keyString = makeKeyString(identifier);\n    if (this.objectMap[keyString]) {\n      return this.objectMap[keyString];\n    }\n\n    throw new Error(keyString + ' not found in import models.');\n  }\n\n  parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {\n    Object.keys(objectLeaf).forEach((nodeKey) => {\n      if (idMap.get(nodeKey)) {\n        const newIdentifier = makeKeyString({\n          namespace: newRootNamespace,\n          key: idMap.get(nodeKey)\n        });\n        objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] };\n        delete objectLeaf[nodeKey];\n        objectLeaf[newIdentifier] = this.parseTreeLeaf(\n          newIdentifier,\n          objectLeaf[newIdentifier],\n          idMap,\n          newRootNamespace,\n          oldRootNamespace\n        );\n      } else {\n        objectLeaf[nodeKey] = this.parseTreeLeaf(\n          nodeKey,\n          objectLeaf[nodeKey],\n          idMap,\n          newRootNamespace,\n          oldRootNamespace\n        );\n      }\n    });\n\n    return objectLeaf;\n  }\n\n  parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) {\n    return arrayLeaf.map((leafValue, index) =>\n      this.parseTreeLeaf(null, leafValue, idMap, newRootNamespace, oldRootNamespace)\n    );\n  }\n\n  parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) {\n    if (Array.isArray(branchedLeafValue)) {\n      return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);\n    } else {\n      return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);\n    }\n  }\n\n  parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) {\n    if (leafValue === null || leafValue === undefined) {\n      return leafValue;\n    }\n\n    const hasChild = typeof leafValue === 'object';\n    if (hasChild) {\n      return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace);\n    }\n\n    if (leafKey === 'key') {\n      let mappedLeafValue;\n      if (oldRootNamespace) {\n        mappedLeafValue = idMap.get(\n          makeKeyString({\n            namespace: oldRootNamespace,\n            key: leafValue\n          })\n        );\n      } else {\n        mappedLeafValue = idMap.get(leafValue);\n      }\n\n      return mappedLeafValue ?? leafValue;\n    } else if (leafKey === 'namespace') {\n      // Only rewrite the namespace if it matches the old root namespace.\n      // This is to prevent rewriting namespaces of objects that are not\n      // children of the root object (e.g.: objects from a telemetry dictionary)\n      return leafValue === oldRootNamespace ? newRootNamespace : leafValue;\n    } else if (leafKey === 'location') {\n      const mappedLeafValue = idMap.get(leafValue);\n      if (!mappedLeafValue) {\n        return null;\n      }\n\n      const newLocationIdentifier = makeKeyString({\n        namespace: newRootNamespace,\n        key: mappedLeafValue\n      });\n\n      return newLocationIdentifier;\n    } else {\n      const mappedLeafValue = idMap.get(leafValue);\n      if (mappedLeafValue) {\n        const newIdentifier = makeKeyString({\n          namespace: newRootNamespace,\n          key: mappedLeafValue\n        });\n\n        return newIdentifier;\n      } else {\n        return leafValue;\n      }\n    }\n  }\n\n  rewriteObjectIdentifiers(importData, rootIdentifier) {\n    const { namespace: oldRootNamespace } = parseKeyString(importData.rootId);\n    const { namespace: newRootNamespace } = rootIdentifier;\n    const idMap = new Map();\n    const objectTree = importData.openmct;\n\n    Object.keys(objectTree).forEach((originalId, index) => {\n      let newId = index.toString();\n      if (originalId === importData.rootId) {\n        newId = rootIdentifier.key;\n      }\n\n      idMap.set(originalId, newId);\n    });\n\n    const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace);\n\n    return newTree;\n  }\n\n  /**\n   * Converts all objects in an object make from old format objects to new\n   * format objects.\n   */\n  convertToNewObjects(oldObjectMap) {\n    return Object.keys(oldObjectMap).reduce(function (newObjectMap, key) {\n      newObjectMap[key] = toNewFormat(oldObjectMap[key], key);\n\n      return newObjectMap;\n    }, {});\n  }\n\n  /* Set the root location correctly for a top-level object */\n  setRootLocation(objectMap, rootIdentifier) {\n    objectMap[makeKeyString(rootIdentifier)].location = 'ROOT';\n\n    return objectMap;\n  }\n\n  /**\n   * Takes importData (as provided by the ImportExport plugin) and exposes\n   * an object provider to fetch those objects.\n   */\n  rewriteModel(importData, rootIdentifier) {\n    const oldFormatObjectMap = this.rewriteObjectIdentifiers(importData, rootIdentifier);\n    const newFormatObjectMap = this.convertToNewObjects(oldFormatObjectMap);\n    this.objectMap = this.setRootLocation(newFormatObjectMap, rootIdentifier);\n  }\n}\n\nexport default StaticModelProvider;\n"
  },
  {
    "path": "src/plugins/staticRootPlugin/StaticModelProviderSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport StaticModelProvider from './StaticModelProvider.js';\nimport testStaticDataEmptyNamespace from './test-data/static-provider-test-empty-namespace.json';\nimport testStaticDataFooNamespace from './test-data/static-provider-test-foo-namespace.json';\n\ndescribe('StaticModelProvider', function () {\n  describe('with empty namespace', function () {\n    let staticProvider;\n\n    beforeEach(function () {\n      const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace));\n      staticProvider = new StaticModelProvider(staticData, {\n        namespace: 'my-import',\n        key: 'root'\n      });\n    });\n\n    describe('rootObject', function () {\n      let rootModel;\n\n      beforeEach(function () {\n        rootModel = staticProvider.get({\n          namespace: 'my-import',\n          key: 'root'\n        });\n      });\n\n      it('is located at top level', function () {\n        expect(rootModel.location).toBe('ROOT');\n      });\n\n      it('has remapped identifier', function () {\n        expect(rootModel.identifier).toEqual({\n          namespace: 'my-import',\n          key: 'root'\n        });\n      });\n\n      it('has remapped identifiers in composition', function () {\n        expect(rootModel.composition).toContain({\n          namespace: 'my-import',\n          key: '1'\n        });\n        expect(rootModel.composition).toContain({\n          namespace: 'my-import',\n          key: '2'\n        });\n      });\n    });\n\n    describe('childObjects', function () {\n      let swg;\n      let layout;\n      let fixed;\n\n      beforeEach(function () {\n        swg = staticProvider.get({\n          namespace: 'my-import',\n          key: '1'\n        });\n        layout = staticProvider.get({\n          namespace: 'my-import',\n          key: '2'\n        });\n        fixed = staticProvider.get({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('match expected ordering', function () {\n        // this is a sanity check to make sure the identifiers map in\n        // the correct order.\n        expect(swg.type).toBe('generator');\n        expect(layout.type).toBe('layout');\n        expect(fixed.type).toBe('telemetry.fixed');\n      });\n\n      it('have remapped identifiers', function () {\n        expect(swg.identifier).toEqual({\n          namespace: 'my-import',\n          key: '1'\n        });\n        expect(layout.identifier).toEqual({\n          namespace: 'my-import',\n          key: '2'\n        });\n        expect(fixed.identifier).toEqual({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('have remapped composition', function () {\n        expect(layout.composition).toContain({\n          namespace: 'my-import',\n          key: '1'\n        });\n        expect(layout.composition).toContain({\n          namespace: 'my-import',\n          key: '3'\n        });\n        expect(fixed.composition).toContain({\n          namespace: 'my-import',\n          key: '1'\n        });\n      });\n\n      it('rewrites locations', function () {\n        expect(swg.location).toBe('my-import:root');\n        expect(layout.location).toBe('my-import:root');\n        expect(fixed.location).toBe('my-import:2');\n      });\n\n      it('rewrites matched identifiers in objects', function () {\n        expect(layout.configuration.layout.panels['my-import:1']).toBeDefined();\n        expect(layout.configuration.layout.panels['my-import:3']).toBeDefined();\n        expect(\n          layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0']\n        ).not.toBeDefined();\n        expect(\n          layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d']\n        ).not.toBeDefined();\n        expect(fixed.configuration['fixed-display'].elements[0].id).toBe('my-import:1');\n      });\n    });\n  });\n  describe('with namespace \"foo\"', function () {\n    let staticProvider;\n\n    beforeEach(function () {\n      const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace));\n      staticProvider = new StaticModelProvider(staticData, {\n        namespace: 'my-import',\n        key: 'root'\n      });\n    });\n\n    describe('rootObject', function () {\n      let rootModel;\n\n      beforeEach(function () {\n        rootModel = staticProvider.get({\n          namespace: 'my-import',\n          key: 'root'\n        });\n      });\n\n      it('is located at top level', function () {\n        expect(rootModel.location).toBe('ROOT');\n      });\n\n      it('has remapped identifier', function () {\n        expect(rootModel.identifier).toEqual({\n          namespace: 'my-import',\n          key: 'root'\n        });\n      });\n\n      it('has remapped composition', function () {\n        expect(rootModel.composition).toContain({\n          namespace: 'my-import',\n          key: '1'\n        });\n        expect(rootModel.composition).toContain({\n          namespace: 'my-import',\n          key: '2'\n        });\n      });\n    });\n\n    describe('childObjects', function () {\n      let clock;\n      let layout;\n      let swg;\n      let folder;\n\n      beforeEach(function () {\n        folder = staticProvider.get({\n          namespace: 'my-import',\n          key: 'root'\n        });\n        layout = staticProvider.get({\n          namespace: 'my-import',\n          key: '1'\n        });\n        swg = staticProvider.get({\n          namespace: 'my-import',\n          key: '2'\n        });\n        clock = staticProvider.get({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('match expected ordering', function () {\n        // this is a sanity check to make sure the identifiers map in\n        // the correct order.\n        expect(folder.type).toBe('folder');\n        expect(swg.type).toBe('generator');\n        expect(layout.type).toBe('layout');\n        expect(clock.type).toBe('clock');\n      });\n\n      it('have remapped identifiers', function () {\n        expect(folder.identifier).toEqual({\n          namespace: 'my-import',\n          key: 'root'\n        });\n        expect(layout.identifier).toEqual({\n          namespace: 'my-import',\n          key: '1'\n        });\n        expect(swg.identifier).toEqual({\n          namespace: 'my-import',\n          key: '2'\n        });\n        expect(clock.identifier).toEqual({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('have remapped identifiers in composition', function () {\n        expect(layout.composition).toContain({\n          namespace: 'my-import',\n          key: '2'\n        });\n        expect(layout.composition).toContain({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('layout has remapped identifiers in configuration', function () {\n        const identifiers = layout.configuration.items\n          .map((item) => item.identifier)\n          .filter((identifier) => identifier !== undefined);\n        expect(identifiers).toContain({\n          namespace: 'my-import',\n          key: '2'\n        });\n        expect(identifiers).toContain({\n          namespace: 'my-import',\n          key: '3'\n        });\n      });\n\n      it('rewrites locations', function () {\n        expect(folder.location).toBe('ROOT');\n        expect(swg.location).toBe('my-import:root');\n        expect(layout.location).toBe('my-import:root');\n        expect(clock.location).toBe('my-import:root');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/staticRootPlugin/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport StaticModelProvider from './StaticModelProvider.js';\n\nexport default function StaticRootPlugin(options) {\n  const rootIdentifier = {\n    namespace: options.namespace,\n    key: 'root'\n  };\n\n  let cachedProvider;\n\n  function loadProvider() {\n    return fetch(options.exportUrl)\n      .then(function (response) {\n        return response.json();\n      })\n      .then(function (importData) {\n        cachedProvider = new StaticModelProvider(importData, rootIdentifier);\n\n        return cachedProvider;\n      });\n  }\n\n  function getProvider() {\n    if (!cachedProvider) {\n      cachedProvider = loadProvider();\n    }\n\n    return Promise.resolve(cachedProvider);\n  }\n\n  return function install(openmct) {\n    openmct.objects.addRoot(rootIdentifier);\n    openmct.objects.addProvider(options.namespace, {\n      get: function (identifier) {\n        return getProvider().then(function (provider) {\n          return provider.get(identifier);\n        });\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json",
    "content": "{\n  \"openmct\": {\n    \"a9122832-4b6e-43ea-8219-5359c14c5de8\": {\n      \"composition\": [\n        \"483c00d4-bb1d-4b42-b29a-c58e06b322a0\",\n        \"d2ac3ae4-0af2-49fe-81af-adac09936215\"\n      ],\n      \"name\": \"import-provider-test\",\n      \"type\": \"folder\",\n      \"notes\": null,\n      \"modified\": 1508522673278,\n      \"location\": \"mine\",\n      \"persisted\": 1508522673278\n    },\n    \"483c00d4-bb1d-4b42-b29a-c58e06b322a0\": {\n      \"telemetry\": {\n        \"period\": 10,\n        \"amplitude\": 1,\n        \"offset\": 0,\n        \"dataRateInHz\": 1,\n        \"values\": [\n          {\n            \"key\": \"utc\",\n            \"name\": \"Time\",\n            \"format\": \"utc\",\n            \"hints\": { \"domain\": 1, \"priority\": 0 },\n            \"source\": \"utc\"\n          },\n          {\n            \"key\": \"yesterday\",\n            \"name\": \"Yesterday\",\n            \"format\": \"utc\",\n            \"hints\": { \"domain\": 2, \"priority\": 1 },\n            \"source\": \"yesterday\"\n          },\n          { \"key\": \"sin\", \"name\": \"Sine\", \"hints\": { \"range\": 1, \"priority\": 2 }, \"source\": \"sin\" },\n          {\n            \"key\": \"cos\",\n            \"name\": \"Cosine\",\n            \"hints\": { \"range\": 2, \"priority\": 3 },\n            \"source\": \"cos\"\n          }\n        ]\n      },\n      \"name\": \"SWG-10\",\n      \"type\": \"generator\",\n      \"modified\": 1508522652874,\n      \"location\": \"a9122832-4b6e-43ea-8219-5359c14c5de8\",\n      \"persisted\": 1508522652874\n    },\n    \"d2ac3ae4-0af2-49fe-81af-adac09936215\": {\n      \"composition\": [\n        \"483c00d4-bb1d-4b42-b29a-c58e06b322a0\",\n        \"20273193-f069-49e9-b4f7-b97a87ed755d\"\n      ],\n      \"name\": \"Layout\",\n      \"type\": \"layout\",\n      \"configuration\": {\n        \"layout\": {\n          \"panels\": {\n            \"483c00d4-bb1d-4b42-b29a-c58e06b322a0\": { \"position\": [0, 0], \"dimensions\": [17, 8] },\n            \"20273193-f069-49e9-b4f7-b97a87ed755d\": {\n              \"position\": [0, 8],\n              \"dimensions\": [17, 1],\n              \"hasFrame\": false\n            }\n          }\n        }\n      },\n      \"modified\": 1508522745580,\n      \"location\": \"a9122832-4b6e-43ea-8219-5359c14c5de8\",\n      \"persisted\": 1508522745580\n    },\n    \"20273193-f069-49e9-b4f7-b97a87ed755d\": {\n      \"layoutGrid\": [64, 16],\n      \"composition\": [\"483c00d4-bb1d-4b42-b29a-c58e06b322a0\"],\n      \"name\": \"FP Test\",\n      \"type\": \"telemetry.fixed\",\n      \"configuration\": {\n        \"fixed-display\": {\n          \"elements\": [\n            {\n              \"type\": \"fixed.telemetry\",\n              \"x\": 0,\n              \"y\": 0,\n              \"id\": \"483c00d4-bb1d-4b42-b29a-c58e06b322a0\",\n              \"stroke\": \"transparent\",\n              \"color\": \"\",\n              \"titled\": true,\n              \"width\": 8,\n              \"height\": 2,\n              \"useGrid\": true,\n              \"size\": \"24px\"\n            }\n          ]\n        }\n      },\n      \"modified\": 1508522717619,\n      \"location\": \"d2ac3ae4-0af2-49fe-81af-adac09936215\",\n      \"persisted\": 1508522717619\n    }\n  },\n  \"rootId\": \"a9122832-4b6e-43ea-8219-5359c14c5de8\"\n}\n"
  },
  {
    "path": "src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json",
    "content": "{\n  \"openmct\": {\n    \"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\": {\n      \"identifier\": { \"key\": \"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\", \"namespace\": \"foo\" },\n      \"name\": \"Folder Foo\",\n      \"type\": \"folder\",\n      \"composition\": [\n        { \"key\": \"95729018-86ed-4484-867d-10c63c41c5a1\", \"namespace\": \"foo\" },\n        { \"key\": \"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c\", \"namespace\": \"foo\" },\n        { \"key\": \"3545554b-53c8-467d-a70d-e90d1a120e4a\", \"namespace\": \"foo\" }\n      ],\n      \"modified\": 1681164966705,\n      \"location\": \"foo:mine\",\n      \"created\": 1681164829371,\n      \"persisted\": 1681164966706\n    },\n    \"foo:95729018-86ed-4484-867d-10c63c41c5a1\": {\n      \"identifier\": { \"key\": \"95729018-86ed-4484-867d-10c63c41c5a1\", \"namespace\": \"foo\" },\n      \"name\": \"Display Layout Bar\",\n      \"type\": \"layout\",\n      \"composition\": [\n        { \"key\": \"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c\", \"namespace\": \"foo\" },\n        { \"key\": \"3545554b-53c8-467d-a70d-e90d1a120e4a\", \"namespace\": \"foo\" }\n      ],\n      \"configuration\": {\n        \"items\": [\n          {\n            \"fill\": \"#666666\",\n            \"stroke\": \"\",\n            \"x\": 42,\n            \"y\": 42,\n            \"width\": 20,\n            \"height\": 4,\n            \"type\": \"box-view\",\n            \"id\": \"14505a5d-b846-4504-961f-8c9bcdf19f39\"\n          },\n          {\n            \"identifier\": { \"key\": \"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c\", \"namespace\": \"foo\" },\n            \"x\": 0,\n            \"y\": 0,\n            \"width\": 40,\n            \"height\": 15,\n            \"displayMode\": \"all\",\n            \"value\": \"sin\",\n            \"stroke\": \"\",\n            \"fill\": \"\",\n            \"color\": \"\",\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"telemetry-view\",\n            \"id\": \"05baa95f-2064-4cb0-ad9f-575758491220\"\n          },\n          {\n            \"width\": 40,\n            \"height\": 15,\n            \"x\": 0,\n            \"y\": 15,\n            \"identifier\": { \"key\": \"3545554b-53c8-467d-a70d-e90d1a120e4a\", \"namespace\": \"foo\" },\n            \"hasFrame\": true,\n            \"fontSize\": \"default\",\n            \"font\": \"default\",\n            \"type\": \"subobject-view\",\n            \"id\": \"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5\"\n          }\n        ],\n        \"layoutGrid\": [10, 10],\n        \"objectStyles\": {\n          \"05baa95f-2064-4cb0-ad9f-575758491220\": {\n            \"staticStyle\": {\n              \"style\": {\n                \"border\": \"1px solid #00ff00\",\n                \"backgroundColor\": \"#0000ff\",\n                \"color\": \"#ff00ff\"\n              }\n            }\n          }\n        }\n      },\n      \"modified\": 1681165037189,\n      \"location\": \"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\",\n      \"created\": 1681164838178,\n      \"persisted\": 1681165037190\n    },\n    \"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c\": {\n      \"identifier\": { \"key\": \"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c\", \"namespace\": \"foo\" },\n      \"name\": \"SWG Baz\",\n      \"type\": \"generator\",\n      \"telemetry\": {\n        \"period\": \"20\",\n        \"amplitude\": \"2\",\n        \"offset\": \"5\",\n        \"dataRateInHz\": 1,\n        \"phase\": 0,\n        \"randomness\": 0,\n        \"loadDelay\": 0,\n        \"infinityValues\": false,\n        \"staleness\": false\n      },\n      \"modified\": 1681164910719,\n      \"location\": \"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\",\n      \"created\": 1681164903684,\n      \"persisted\": 1681164910719\n    },\n    \"foo:3545554b-53c8-467d-a70d-e90d1a120e4a\": {\n      \"identifier\": { \"key\": \"3545554b-53c8-467d-a70d-e90d1a120e4a\", \"namespace\": \"foo\" },\n      \"name\": \"Clock Qux\",\n      \"type\": \"clock\",\n      \"configuration\": {\n        \"baseFormat\": \"YYYY/MM/DD hh:mm:ss\",\n        \"use24\": \"clock12\",\n        \"timezone\": \"UTC\"\n      },\n      \"modified\": 1681164989837,\n      \"location\": \"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\",\n      \"created\": 1681164966702,\n      \"persisted\": 1681164989837\n    }\n  },\n  \"rootId\": \"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1\"\n}\n"
  },
  {
    "path": "src/plugins/summaryWidget/README.md",
    "content": "# Summary Widget Plugin\nSummary widgets can be used to provide visual indication of state based on telemetry data. They allow rules to be \ndefined that can then be used to change the appearance of the summary widget element based on data. For example, a \nsummary widget could be defined that is green when a temperature reading is between `0` and `100` centigrade, red when \nit's above `100`, and orange when it's below `0`.\n\n## Installation\n```js\nopenmct.install(openmct.plugins.SummaryWidget());\n```"
  },
  {
    "path": "src/plugins/summaryWidget/SummaryWidgetViewPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Policy determining which views can apply to summary widget.  Disables\n * any view other than normal summary widget view.\n */\nexport default function SummaryWidgetViewPolicy() {}\n\nSummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) {\n  if (domainObject.getModel().type === 'summary-widget') {\n    return view.key === 'summary-widget-viewer';\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function SummaryWidgetsCompositionPolicy(openmct) {\n  this.openmct = openmct;\n}\n\nSummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {\n  const parentType = parent.type;\n\n  if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) {\n    return false;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/plugin.js",
    "content": "import SummaryWidgetMetadataProvider from './src/telemetry/SummaryWidgetMetadataProvider.js';\nimport SummaryWidgetTelemetryProvider from './src/telemetry/SummaryWidgetTelemetryProvider.js';\nimport SummaryWidgetViewProvider from './src/views/SummaryWidgetViewProvider.js';\nimport SummaryWidgetsCompositionPolicy from './SummaryWidgetsCompositionPolicy.js';\n\nexport default function plugin() {\n  const widgetType = {\n    name: 'Summary Widget',\n    description: 'A compact status update for collections of telemetry-producing items',\n    cssClass: 'icon-summary-widget',\n    initialize: function (domainObject) {\n      domainObject.composition = [];\n      domainObject.configuration = {\n        ruleOrder: ['default'],\n        ruleConfigById: {\n          default: {\n            name: 'Default',\n            label: 'Unnamed Rule',\n            message: '',\n            id: 'default',\n            icon: ' ',\n            style: {\n              color: '#ffffff',\n              'background-color': '#38761d',\n              'border-color': 'rgba(0,0,0,0)'\n            },\n            description: 'Default appearance for the widget',\n            conditions: [\n              {\n                object: '',\n                key: '',\n                operation: '',\n                values: []\n              }\n            ],\n            jsCondition: '',\n            trigger: 'any',\n            expanded: 'true'\n          }\n        },\n        testDataConfig: [\n          {\n            object: '',\n            key: '',\n            value: ''\n          }\n        ]\n      };\n      domainObject.openNewTab = 'thisTab';\n      domainObject.telemetry = {};\n    },\n    form: [\n      {\n        key: 'url',\n        name: 'URL',\n        control: 'textfield',\n        required: false,\n        cssClass: 'l-input-lg'\n      },\n      {\n        key: 'openNewTab',\n        name: 'Tab to Open Hyperlink',\n        control: 'select',\n        options: [\n          {\n            value: 'thisTab',\n            name: 'Open in this tab'\n          },\n          {\n            value: 'newTab',\n            name: 'Open in a new tab'\n          }\n        ],\n        cssClass: 'l-inline'\n      }\n    ]\n  };\n\n  return function install(openmct) {\n    openmct.types.addType('summary-widget', widgetType);\n    let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct);\n    openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy));\n    openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct));\n    openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct));\n    openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/conditionTemplate.html",
    "content": "<li class=\"has-local-controls t-condition\">\n  <label class=\"t-condition-context\">when</label>\n  <span class=\"controls\">\n    <span class=\"t-configuration\"> </span>\n    <span class=\"t-value-inputs\"> </span>\n  </span>\n  <span class=\"flex-elem c-local-controls--show-on-hover l-condition-action-buttons-wrapper\">\n    <a class=\"s-icon-button icon-duplicate t-duplicate\" title=\"Duplicate this condition\"></a>\n    <a class=\"s-icon-button icon-trash t-delete\" title=\"Delete this condition\"></a>\n  </span>\n</li>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/input/paletteTemplate.html",
    "content": "<!--a class=\"e-control s-button s-menu-button menu-element\">\n   <span class=\"l-click-area\"></span>\n   <span class=\"t-swatch\"></span>\n   <div class=\"menu l-palette\">\n       <div class=\"l-palette-row l-option-row\">\n           <div class=\"l-palette-item s-palette-item no-selection\"></div>\n           <span class=\"l-palette-item-label\">None</span>\n       </div>\n   </div>\n</a-->\n<div class=\"c-ctrl-wrapper\">\n  <button class=\"c-button--menu c-button--swatched js-button\">\n    <div class=\"c-swatch t-swatch\"></div>\n  </button>\n  <div class=\"c-menu c-palette\">\n    <div class=\"c-palette__item-none\">\n      <div class=\"c-palette__item\"></div>\n    </div>\n    <div class=\"c-palette__items\"></div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/input/selectTemplate.html",
    "content": "<span>\n  <select></select>\n</span>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/ruleImageTemplate.html",
    "content": "<div class=\"holder widget-rules-wrapper\">\n  <div class=\"t-drag-rule-image l-widget-rule s-widget-rule\"></div>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/ruleTemplate.html",
    "content": "<div class=\"c-sw-rule\">\n  <div class=\"c-sw-rule__ui l-compact-form l-widget-rule s-widget-rule has-local-controls\">\n    <div class=\"c-sw-rule__ui__header widget-rule-header\">\n      <div class=\"c-sw-rule__grippy-wrapper\">\n        <div class=\"c-sw-rule__grippy t-grippy local-control local-controls-hidden\"></div>\n      </div>\n      <div\n        class=\"c-disclosure-triangle c-disclosure-triangle--expanded is-enabled js-disclosure\"\n      ></div>\n      <div class=\"t-widget-thumb widget-thumb c-sw c-sw--thumb\">\n        <div class=\"c-sw__icon js-sw__icon\"></div>\n        <div class=\"c-sw__label js-sw__label\"></div>\n      </div>\n      <div class=\"flex-elem rule-title\">Default Title</div>\n      <div class=\"flex-elem rule-description grows\">Rule description goes here</div>\n      <div class=\"flex-elem c-local-controls--show-on-hover l-rule-action-buttons-wrapper\">\n        <a class=\"s-icon-button icon-duplicate t-duplicate\" title=\"Duplicate this rule\"></a>\n        <a class=\"s-icon-button icon-trash t-delete\" title=\"Delete this rule\"></a>\n      </div>\n    </div>\n    <div class=\"widget-rule-content expanded\">\n      <ul>\n        <li>\n          <label>Rule Name:</label>\n          <span class=\"controls\">\n            <input class=\"t-rule-name-input\" type=\"text\" />\n          </span>\n        </li>\n        <li class=\"connects-to-previous\">\n          <label>Label:</label>\n          <span class=\"controls t-label-input\">\n            <input class=\"t-rule-label-input\" type=\"text\" />\n          </span>\n        </li>\n        <li class=\"connects-to-previous\">\n          <label>Message:</label>\n          <span class=\"controls\">\n            <input\n              type=\"text\"\n              class=\"lg s t-rule-message-input\"\n              placeholder=\"Will appear as tooltip when hovering on the widget\"\n            />\n          </span>\n        </li>\n        <li class=\"connects-to-previous\">\n          <label>Style:</label>\n          <span class=\"controls t-style-input\"></span>\n        </li>\n      </ul>\n      <ul class=\"t-widget-rule-config\">\n        <li>\n          <label>Trigger when</label>\n          <span class=\"controls\">\n            <select class=\"t-trigger\">\n              <option value=\"any\">any condition is met</option>\n              <option value=\"all\">all conditions are met</option>\n            </select>\n          </span>\n        </li>\n        <li>\n          <label></label>\n          <span class=\"controls\">\n            <button class=\"c-button add-condition icon-plus\">\n              <span class=\"c-button__label\">Add Condition</span>\n            </button>\n          </span>\n        </li>\n      </ul>\n    </div>\n  </div>\n  <div class=\"t-drag-indicator l-widget-rule s-widget-rule\" style=\"opacity: 0\" hidden></div>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/testDataItemTemplate.html",
    "content": "<div\n  class=\"t-test-data-item l-compact-form has-local-controls l-widget-test-data-item s-widget-test-data-item\"\n>\n  <ul>\n    <li>\n      <label>Set </label>\n      <span class=\"controls\">\n        <span class=\"t-configuration\"></span>\n        <span class=\"equal-to hidden\"> equal to </span>\n        <span class=\"t-value-inputs\"></span>\n      </span>\n      <span\n        class=\"flex-elem c-local-controls--show-on-hover l-widget-test-data-item-action-buttons-wrapper\"\n      >\n        <a class=\"s-icon-button icon-duplicate t-duplicate\" title=\"Duplicate this test value\"></a>\n        <a class=\"s-icon-button icon-trash t-delete\" title=\"Delete this test value\"></a>\n      </span>\n    </li>\n  </ul>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/testDataTemplate.html",
    "content": "<div class=\"flex-accordion-holder\">\n  <div class=\"flex-accordion-holder t-widget-test-data-content w-widget-test-data-content\">\n    <div class=\"l-enable\">\n      <label class=\"checkbox custom\"\n        >Apply Test Values\n        <input type=\"checkbox\" class=\"t-test-data-checkbox\" />\n        <em></em>\n      </label>\n    </div>\n    <div class=\"t-test-data-config w-widget-test-data-items\">\n      <div class=\"holder add-rule-button-wrapper align-right\">\n        <button id=\"addRule\" class=\"c-button c-button--major add-test-condition icon-plus\">\n          <span class=\"c-button__label\">Add Test Value</span>\n        </button>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/res/widgetTemplate.html",
    "content": "<div class=\"c-sw-edit w-summary-widget s-status-no-data\">\n  <a id=\"widget\" class=\"t-summary-widget l-summary-widget s-summary-widget labeled c-sw\">\n    <div id=\"widgetIcon\" class=\"c-sw__icon js-sw__icon\"></div>\n    <div id=\"widgetLabel\" class=\"label widget-label c-sw__label js-sw__label\">\n      Default Static Name\n    </div>\n  </a>\n  <div\n    class=\"js-summary-widget__message c-summary-widget__message c-message c-message--simple message-severity-alert\"\n  >\n    <div class=\"c-summary-widget__text\">\n      You must add at least one telemetry object to edit this widget.\n    </div>\n  </div>\n  <div\n    class=\"c-sw-edit__ui holder l-flex-accordion flex-elem grows widget-edit-holder expanded-widget-test-data expanded-widget-rules\"\n  >\n    <div class=\"c-sw-edit__ui__header\">\n      <span\n        class=\"c-disclosure-triangle c-disclosure-triangle--expanded is-enabled t-view-control-test-data\"\n      ></span>\n      <span class=\"c-sw-edit__ui__header-label\">Test Data Values</span>\n    </div>\n    <div class=\"c-sw-edit__ui__test-data widget-test-data flex-accordion-holder\"></div>\n    <div class=\"c-sw-edit__ui__header\">\n      <span\n        class=\"c-disclosure-triangle c-disclosure-triangle--expanded is-enabled t-view-control-rules\"\n      ></span>\n      <span class=\"c-sw-edit__ui__header-label\">Rules</span>\n    </div>\n    <div class=\"c-sw-editui__rules-wrapper holder widget-rules-wrapper flex-elem expanded\">\n      <div id=\"ruleArea\" class=\"c-sw-editui__rules widget-rules\"></div>\n      <div class=\"holder add-rule-button-wrapper align-right\">\n        <button id=\"addRule\" class=\"c-button c-button--major add-rule-button icon-plus\">\n          <span class=\"c-button__label\">Add Rule</span>\n        </button>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/Condition.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport conditionTemplate from '../res/conditionTemplate.html';\nimport eventHelpers from './eventHelpers.js';\nimport KeySelect from './input/KeySelect.js';\nimport ObjectSelect from './input/ObjectSelect.js';\nimport OperationSelect from './input/OperationSelect.js';\n\n/**\n * Represents an individual condition for a summary widget rule. Manages the\n * associated inputs and view.\n * @param {Object} conditionConfig The configuration for this condition, consisting\n *                                of object, key, operation, and values fields\n * @param {number} index the index of this Condition object in it's parent Rule's data model,\n *                        to be injected into callbacks for removes\n * @param {ConditionManager} conditionManager A ConditionManager instance for populating\n *                                            selects with configuration data\n */\nexport default function Condition(conditionConfig, index, conditionManager) {\n  eventHelpers.extend(this);\n  this.config = conditionConfig;\n  this.index = index;\n  this.conditionManager = conditionManager;\n\n  this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0];\n\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['remove', 'duplicate', 'change'];\n\n  this.deleteButton = this.domElement.querySelector('.t-delete');\n  this.duplicateButton = this.domElement.querySelector('.t-duplicate');\n\n  this.selects = {};\n  this.valueInputs = [];\n\n  const self = this;\n\n  /**\n   * Event handler for a change in one of this conditions' custom selects\n   * @param {string} value The new value of this selects\n   * @param {string} property The property of this condition to modify\n   * @private\n   */\n  function onSelectChange(value, property) {\n    if (property === 'operation') {\n      self.generateValueInputs(value);\n    }\n\n    self.eventEmitter.emit('change', {\n      value: value,\n      property: property,\n      index: self.index\n    });\n  }\n\n  this.handleObjectChange = (value) => onSelectChange(value, 'object');\n  this.handleKeyChange = (value) => onSelectChange(value, 'key');\n\n  /**\n   * Event handler for this conditions value inputs\n   * @param {Event} event The oninput event that triggered this callback\n   * @private\n   */\n  function onValueInput(event) {\n    const elem = event.target;\n    const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value);\n    const inputIndex = self.valueInputs.indexOf(elem);\n\n    self.eventEmitter.emit('change', {\n      value: value,\n      property: 'values[' + inputIndex + ']',\n      index: self.index\n    });\n  }\n\n  this.listenTo(this.deleteButton, 'click', this.remove, this);\n  this.listenTo(this.duplicateButton, 'click', this.duplicate, this);\n\n  this.selects.object = new ObjectSelect(this.config, this.conditionManager, [\n    ['any', 'any telemetry'],\n    ['all', 'all telemetry']\n  ]);\n  this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager);\n  this.selects.operation = new OperationSelect(\n    this.config,\n    this.selects.key,\n    this.conditionManager,\n    function (value) {\n      onSelectChange(value, 'operation');\n    }\n  );\n\n  this.selects.object.on('change', this.handleObjectChange);\n  this.selects.key.on('change', this.handleKeyChange);\n\n  Object.values(this.selects).forEach(function (select) {\n    self.domElement.querySelector('.t-configuration').append(select.getDOM());\n  });\n\n  this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput);\n}\n\nCondition.prototype.getDOM = function (container) {\n  return this.domElement;\n};\n\n/**\n * Register a callback with this condition: supported callbacks are remove, change,\n * duplicate\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nCondition.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  }\n};\n\n/**\n * Hide the appropriate inputs when this is the only condition\n */\nCondition.prototype.hideButtons = function () {\n  this.deleteButton.style.display = 'none';\n};\n\n/**\n * Remove this condition from the configuration. Invokes any registered\n * remove callbacks\n */\nCondition.prototype.remove = function () {\n  this.selects.object.off('change', this.handleObjectChange);\n  this.selects.key.off('change', this.handleKeyChange);\n  this.eventEmitter.emit('remove', this.index);\n  this.destroy();\n};\n\nCondition.prototype.destroy = function () {\n  this.stopListening();\n  Object.values(this.selects).forEach(function (select) {\n    select.destroy();\n  });\n};\n\n/**\n * Make a deep clone of this condition's configuration and invoke any duplicate\n * callbacks with the cloned configuration and this rule's index\n */\nCondition.prototype.duplicate = function () {\n  const sourceCondition = JSON.parse(JSON.stringify(this.config));\n  this.eventEmitter.emit('duplicate', {\n    sourceCondition: sourceCondition,\n    index: this.index\n  });\n};\n\n/**\n * When an operation is selected, create the appropriate value inputs\n * and add them to the view. If an operation is of type enum, create\n * a drop-down menu instead.\n *\n * @param {string} operation The key of currently selected operation\n */\nCondition.prototype.generateValueInputs = function (operation) {\n  const evaluator = this.conditionManager.getEvaluator();\n  const inputArea = this.domElement.querySelector('.t-value-inputs');\n  let inputCount;\n  let inputType;\n  let newInput;\n  let index = 0;\n  let emitChange = false;\n\n  inputArea.innerHTML = '';\n  this.valueInputs = [];\n  this.config.values = this.config.values || [];\n\n  if (evaluator.getInputCount(operation)) {\n    inputCount = evaluator.getInputCount(operation);\n    inputType = evaluator.getInputType(operation);\n\n    while (index < inputCount) {\n      if (inputType === 'select') {\n        const options = this.generateSelectOptions();\n\n        newInput = document.createElement('select');\n        newInput.appendChild(options);\n\n        emitChange = true;\n      } else {\n        const defaultValue = inputType === 'number' ? 0 : '';\n        const value = this.config.values[index] || defaultValue;\n        this.config.values[index] = value;\n\n        newInput = document.createElement('input');\n        newInput.type = `${inputType}`;\n        newInput.value = `${value}`;\n      }\n\n      this.valueInputs.push(newInput);\n      inputArea.appendChild(newInput);\n      index += 1;\n    }\n\n    if (emitChange) {\n      this.eventEmitter.emit('change', {\n        value: Number(newInput[0].options[0].value),\n        property: 'values[0]',\n        index: this.index\n      });\n    }\n  }\n};\n\nCondition.prototype.generateSelectOptions = function () {\n  let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object);\n  let fragment = document.createDocumentFragment();\n\n  telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => {\n    const option = document.createElement('option');\n    option.value = enumeration.value;\n    option.textContent = enumeration.string;\n    fragment.appendChild(option);\n  });\n\n  return fragment;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/ConditionEvaluator.js",
    "content": "/**\n * Responsible for maintaining the possible operations for conditions\n * in this widget, and evaluating the boolean value of conditions passed as\n * input.\n * @constructor\n * @param {Object} subscriptionCache A cache consisting of the latest available\n *                                   data for any telemetry sources in the widget's\n *                                   composition.\n * @param {Object} compositionObjs The current set of composition objects to\n *                                 evaluate for 'any' and 'all' conditions\n */\nexport default function ConditionEvaluator(subscriptionCache, compositionObjs) {\n  this.subscriptionCache = subscriptionCache;\n  this.compositionObjs = compositionObjs;\n\n  this.testCache = {};\n  this.useTestCache = false;\n\n  /**\n   * Maps value types to HTML input field types. These\n   * type of inputs will be generated by conditions expecting this data type\n   */\n  this.inputTypes = {\n    number: 'number',\n    string: 'text',\n    enum: 'select'\n  };\n\n  /**\n   * Functions to validate that the input to an operation is of the type\n   * that it expects, in order to prevent unexpected behavior. Will be\n   * invoked before the corresponding operation is executed\n   */\n  this.inputValidators = {\n    number: this.validateNumberInput,\n    string: this.validateStringInput,\n    enum: this.validateNumberInput\n  };\n\n  /**\n   * A library of operations supported by this rule evaluator. Each operation\n   * consists of the following fields:\n   * operation: a function with boolean return type to be invoked when this\n   *            operation is used. Will be called with an array of inputs\n   *            where input [0] is the telemetry value and input [1..n] are\n   *            any comparison values\n   * text: a human-readable description of this operation to populate selects\n   * appliesTo: an array of identifiers for types that operation may be used on\n   * inputCount: the number of inputs required to get any necessary comparison\n   *             values for the operation\n   * getDescription: A function returning a human-readable shorthand description of\n   *                this operation to populate the 'description' field in the rule header.\n   *                Will be invoked with an array of a condition's comparison values.\n   */\n  this.operations = {\n    equalTo: {\n      operation: function (input) {\n        return input[0] === input[1];\n      },\n      text: 'is equal to',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' == ' + values[0];\n      }\n    },\n    notEqualTo: {\n      operation: function (input) {\n        return input[0] !== input[1];\n      },\n      text: 'is not equal to',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' != ' + values[0];\n      }\n    },\n    greaterThan: {\n      operation: function (input) {\n        return input[0] > input[1];\n      },\n      text: 'is greater than',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' > ' + values[0];\n      }\n    },\n    lessThan: {\n      operation: function (input) {\n        return input[0] < input[1];\n      },\n      text: 'is less than',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' < ' + values[0];\n      }\n    },\n    greaterThanOrEq: {\n      operation: function (input) {\n        return input[0] >= input[1];\n      },\n      text: 'is greater than or equal to',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' >= ' + values[0];\n      }\n    },\n    lessThanOrEq: {\n      operation: function (input) {\n        return input[0] <= input[1];\n      },\n      text: 'is less than or equal to',\n      appliesTo: ['number'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' <= ' + values[0];\n      }\n    },\n    between: {\n      operation: function (input) {\n        return input[0] > input[1] && input[0] < input[2];\n      },\n      text: 'is between',\n      appliesTo: ['number'],\n      inputCount: 2,\n      getDescription: function (values) {\n        return ' between ' + values[0] + ' and ' + values[1];\n      }\n    },\n    notBetween: {\n      operation: function (input) {\n        return input[0] < input[1] || input[0] > input[2];\n      },\n      text: 'is not between',\n      appliesTo: ['number'],\n      inputCount: 2,\n      getDescription: function (values) {\n        return ' not between ' + values[0] + ' and ' + values[1];\n      }\n    },\n    textContains: {\n      operation: function (input) {\n        return input[0] && input[1] && input[0].includes(input[1]);\n      },\n      text: 'text contains',\n      appliesTo: ['string'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' contains ' + values[0];\n      }\n    },\n    textDoesNotContain: {\n      operation: function (input) {\n        return input[0] && input[1] && !input[0].includes(input[1]);\n      },\n      text: 'text does not contain',\n      appliesTo: ['string'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' does not contain ' + values[0];\n      }\n    },\n    textStartsWith: {\n      operation: function (input) {\n        return input[0].startsWith(input[1]);\n      },\n      text: 'text starts with',\n      appliesTo: ['string'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' starts with ' + values[0];\n      }\n    },\n    textEndsWith: {\n      operation: function (input) {\n        return input[0].endsWith(input[1]);\n      },\n      text: 'text ends with',\n      appliesTo: ['string'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' ends with ' + values[0];\n      }\n    },\n    textIsExactly: {\n      operation: function (input) {\n        return input[0] === input[1];\n      },\n      text: 'text is exactly',\n      appliesTo: ['string'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' is exactly ' + values[0];\n      }\n    },\n    isUndefined: {\n      operation: function (input) {\n        return typeof input[0] === 'undefined';\n      },\n      text: 'is undefined',\n      appliesTo: ['string', 'number', 'enum'],\n      inputCount: 0,\n      getDescription: function () {\n        return ' is undefined';\n      }\n    },\n    isDefined: {\n      operation: function (input) {\n        return typeof input[0] !== 'undefined';\n      },\n      text: 'is defined',\n      appliesTo: ['string', 'number', 'enum'],\n      inputCount: 0,\n      getDescription: function () {\n        return ' is defined';\n      }\n    },\n    enumValueIs: {\n      operation: function (input) {\n        return input[0] === input[1];\n      },\n      text: 'is',\n      appliesTo: ['enum'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' == ' + values[0];\n      }\n    },\n    enumValueIsNot: {\n      operation: function (input) {\n        return input[0] !== input[1];\n      },\n      text: 'is not',\n      appliesTo: ['enum'],\n      inputCount: 1,\n      getDescription: function (values) {\n        return ' != ' + values[0];\n      }\n    }\n  };\n}\n\n/**\n * Evaluate the conditions passed in as an argument, and return the boolean\n * value of these conditions. Available evaluation modes are 'any', which will\n * return true if any of the conditions evaluates to true (i.e. logical OR); 'all',\n * which returns true only if all conditions evaluate to true (i.e. logical AND);\n * or 'js', which returns the boolean value of a custom JavaScript conditional.\n * @param {} conditions Either an array of objects with object, key, operation,\n *                      and value fields, or a string representing a JavaScript\n *                      condition.\n * @param {string} mode The key of the mode to use when evaluating the conditions.\n * @return {boolean} The boolean value of the conditions\n */\nConditionEvaluator.prototype.execute = function (conditions, mode) {\n  let active = false;\n  let conditionValue;\n  let conditionDefined = false;\n  const self = this;\n  let firstRuleEvaluated = false;\n  const compositionObjs = this.compositionObjs;\n\n  if (mode === 'js') {\n    active = this.executeJavaScriptCondition(conditions);\n  } else {\n    (conditions || []).forEach(function (condition) {\n      conditionDefined = false;\n      if (condition.object === 'any') {\n        conditionValue = false;\n        Object.keys(compositionObjs).forEach(function (objId) {\n          try {\n            conditionValue =\n              conditionValue ||\n              self.executeCondition(objId, condition.key, condition.operation, condition.values);\n            conditionDefined = true;\n          } catch (e) {\n            //ignore a malformed condition\n          }\n        });\n      } else if (condition.object === 'all') {\n        conditionValue = true;\n        Object.keys(compositionObjs).forEach(function (objId) {\n          try {\n            conditionValue =\n              conditionValue &&\n              self.executeCondition(objId, condition.key, condition.operation, condition.values);\n            conditionDefined = true;\n          } catch (e) {\n            //ignore a malformed condition\n          }\n        });\n      } else {\n        try {\n          conditionValue = self.executeCondition(\n            condition.object,\n            condition.key,\n            condition.operation,\n            condition.values\n          );\n          conditionDefined = true;\n        } catch (e) {\n          //ignore malformed condition\n        }\n      }\n\n      if (conditionDefined) {\n        active = mode === 'all' && !firstRuleEvaluated ? true : active;\n        firstRuleEvaluated = true;\n        if (mode === 'any') {\n          active = active || conditionValue;\n        } else if (mode === 'all') {\n          active = active && conditionValue;\n        }\n      }\n    });\n  }\n\n  return active;\n};\n\n/**\n * Execute a condition defined as an object.\n * @param {string} object The identifier of the telemetry object to retrieve data from\n * @param {string} key The property of the telemetry object\n * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition\n * @param {string} values An array of comparison values to invoke the operation with\n * @return {boolean} The value of this condition\n */\nConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) {\n  const cache = this.useTestCache ? this.testCache : this.subscriptionCache;\n  let telemetryValue;\n  let op;\n  let input;\n  let validator;\n\n  if (cache[object] && typeof cache[object][key] !== 'undefined') {\n    let value = cache[object][key];\n    telemetryValue = [isNaN(Number(value)) ? value : Number(value)];\n  }\n\n  op = this.operations[operation] && this.operations[operation].operation;\n  input = telemetryValue && telemetryValue.concat(values);\n  validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];\n\n  if (op && input && validator) {\n    if (this.operations[operation].appliesTo.length > 1) {\n      return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input);\n    } else {\n      return validator(input) && op(input);\n    }\n  } else {\n    throw new Error('Malformed condition');\n  }\n};\n\n/**\n * A function that returns true only if each value in its input argument is\n * of a numerical type\n * @param {[]} input An array of values\n * @returns {boolean}\n */\nConditionEvaluator.prototype.validateNumberInput = function (input) {\n  let valid = true;\n  input.forEach(function (value) {\n    valid = valid && typeof value === 'number';\n  });\n\n  return valid;\n};\n\n/**\n * A function that returns true only if each value in its input argument is\n * a string\n * @param {[]} input An array of values\n * @returns {boolean}\n */\nConditionEvaluator.prototype.validateStringInput = function (input) {\n  let valid = true;\n  input.forEach(function (value) {\n    valid = valid && typeof value === 'string';\n  });\n\n  return valid;\n};\n\n/**\n * Get the keys of operations supported by this evaluator\n * @return {string[]} An array of the keys of supported operations\n */\nConditionEvaluator.prototype.getOperationKeys = function () {\n  return Object.keys(this.operations);\n};\n\n/**\n * Get the human-readable text corresponding to a given operation\n * @param {string} key The key of the operation\n * @return {string} The text description of the operation\n */\nConditionEvaluator.prototype.getOperationText = function (key) {\n  return this.operations[key].text;\n};\n\n/**\n * Returns true only if the given operation applies to a given type\n * @param {string} key The key of the operation\n * @param {string} type The value type to query\n * @returns {boolean} True if the condition applies, false otherwise\n */\nConditionEvaluator.prototype.operationAppliesTo = function (key, type) {\n  return this.operations[key].appliesTo.includes(type);\n};\n\n/**\n * Return the number of value inputs required by an operation\n * @param {string} key The key of the operation to query\n * @return {number}\n */\nConditionEvaluator.prototype.getInputCount = function (key) {\n  if (this.operations[key]) {\n    return this.operations[key].inputCount;\n  }\n};\n\n/**\n * Return the human-readable shorthand description of the operation for a rule header\n * @param {string} key The key of the operation to query\n * @param {} values An array of values with which to invoke the getDescription function\n *                  of the operation\n * @return {string} A text description of this operation\n */\nConditionEvaluator.prototype.getOperationDescription = function (key, values) {\n  if (this.operations[key]) {\n    return this.operations[key].getDescription(values);\n  }\n};\n\n/**\n * Return the HTML input type associated with a given operation\n * @param {string} key The key of the operation to query\n * @return {string} The key for an HTML5 input type\n */\nConditionEvaluator.prototype.getInputType = function (key) {\n  let type;\n  if (this.operations[key]) {\n    type = this.operations[key].appliesTo[0];\n  }\n\n  if (this.inputTypes[type]) {\n    return this.inputTypes[type];\n  }\n};\n\n/**\n * Returns the HTML input type associated with a value type\n * @param {string} dataType The JavaScript value type\n * @return {string} The key for an HTML5 input type\n */\nConditionEvaluator.prototype.getInputTypeById = function (dataType) {\n  return this.inputTypes[dataType];\n};\n\n/**\n * Set the test data cache used by this rule evaluator\n * @param {Object} testCache A mock cache following the format of the real\n *                           subscription cache\n */\nConditionEvaluator.prototype.setTestDataCache = function (testCache) {\n  this.testCache = testCache;\n};\n\n/**\n * Have this RuleEvaluator pull data values from the provided test cache\n * instead of its actual subscription cache when evaluating. If invoked with true,\n * will use the test cache; otherwise, will use the subscription cache\n * @param {boolean} useTestData Boolean flag\n */\nConditionEvaluator.prototype.useTestData = function (useTestCache) {\n  this.useTestCache = useTestCache;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/ConditionManager.js",
    "content": "import { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\nimport { makeKeyString } from 'objectUtils';\n\nimport ConditionEvaluator from './ConditionEvaluator.js';\n\n/**\n * Provides a centralized content manager for conditions in the summary widget.\n * Loads and caches composition and telemetry subscriptions, and maintains a\n * {ConditionEvaluator} instance to handle evaluation\n * @constructor\n * @param {Object} domainObject the Summary Widget domain object\n * @param {MCT} openmct an MCT instance\n */\nexport default function ConditionManager(domainObject, openmct) {\n  this.domainObject = domainObject;\n  this.openmct = openmct;\n\n  this.composition = this.openmct.composition.get(this.domainObject);\n  this.compositionObjs = {};\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry'];\n\n  this.keywordLabels = {\n    any: 'any Telemetry',\n    all: 'all Telemetry'\n  };\n\n  this.telemetryMetadataById = {\n    any: {},\n    all: {}\n  };\n\n  this.telemetryTypesById = {\n    any: {},\n    all: {}\n  };\n\n  this.subscriptions = {};\n  this.subscriptionCache = {};\n  this.loadComplete = false;\n  this.metadataLoadComplete = false;\n  this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs);\n\n  this.composition.on('add', this.onCompositionAdd, this);\n  this.composition.on('remove', this.onCompositionRemove, this);\n  this.composition.on('load', this.onCompositionLoad, this);\n\n  this.composition.load();\n}\n\n/**\n * Register a callback with this ConditionManager: supported callbacks are add\n * remove, load, metadata, and receiveTelemetry\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nConditionManager.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  } else {\n    throw (\n      event + ' is not a supported callback. Supported callbacks are ' + this.supportedCallbacks\n    );\n  }\n};\n\n/**\n * Given a set of rules, execute the conditions associated with each rule\n * and return the id of the last rule whose conditions evaluate to true\n * @param {string[]} ruleOrder An array of rule IDs indicating what order They\n *                             should be evaluated in\n * @param {Object} rules An object mapping rule IDs to rule configurations\n * @return {string} The ID of the rule to display on the widget\n */\nConditionManager.prototype.executeRules = function (ruleOrder, rules) {\n  const self = this;\n  let activeId = ruleOrder[0];\n  let rule;\n  let conditions;\n\n  ruleOrder.forEach(function (ruleId) {\n    rule = rules[ruleId];\n    conditions = rule.getProperty('conditions');\n    if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) {\n      activeId = ruleId;\n    }\n  });\n\n  return activeId;\n};\n\n/**\n * Adds a field to the list of all available metadata fields in the widget\n * @param {Object} metadatum An object representing a set of telemetry metadata\n */\nConditionManager.prototype.addGlobalMetadata = function (metadatum) {\n  this.telemetryMetadataById.any[metadatum.key] = metadatum;\n  this.telemetryMetadataById.all[metadatum.key] = metadatum;\n};\n\n/**\n * Adds a field to the list of properties for globally available metadata\n * @param {string} key The key for the property this type applies to\n * @param {string} type The type that should be associated with this property\n */\nConditionManager.prototype.addGlobalPropertyType = function (key, type) {\n  this.telemetryTypesById.any[key] = type;\n  this.telemetryTypesById.all[key] = type;\n};\n\n/**\n * Given a telemetry-producing domain object, associate each of it's telemetry\n * fields with a type, parsing from historical data.\n * @param {Object} object a domain object that can produce telemetry\n * @return {Promise} A promise that resolves when a telemetry request\n *                   has completed and types have been parsed\n */\nConditionManager.prototype.parsePropertyTypes = function (object) {\n  const objectId = makeKeyString(object.identifier);\n\n  this.telemetryTypesById[objectId] = {};\n  Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) {\n    let type;\n    if (valueMetadata.enumerations !== undefined) {\n      type = 'enum';\n    } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {\n      type = 'number';\n    } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {\n      type = 'number';\n    } else if (valueMetadata.key === 'name') {\n      type = 'string';\n    } else {\n      type = 'string';\n    }\n\n    this.telemetryTypesById[objectId][valueMetadata.key] = type;\n    this.addGlobalPropertyType(valueMetadata.key, type);\n  }, this);\n};\n\n/**\n * Parse types of telemetry fields from all composition objects; used internally\n * to perform a block types load once initial composition load has completed\n * @return {Promise} A promise that resolves when all metadata has been loaded\n *                   and property types parsed\n */\nConditionManager.prototype.parseAllPropertyTypes = function () {\n  Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this);\n  this.metadataLoadComplete = true;\n  this.eventEmitter.emit('metadata');\n};\n\n/**\n * Invoked when a telemetry subscription yields new data. Updates the LAD\n * cache and invokes any registered receiveTelemetry callbacks\n * @param {string} objId The key associated with the telemetry source\n * @param {datum} datum The new data from the telemetry source\n * @private\n */\nConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) {\n  this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum);\n  this.eventEmitter.emit('receiveTelemetry');\n};\n\nConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) {\n  return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => {\n    normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source];\n\n    return normalizedDatum;\n  }, {});\n};\n\n/**\n * Event handler for an add event in this Summary Widget's composition.\n * Sets up subscription handlers and parses its property types.\n * @param {Object} obj The newly added domain object\n * @private\n */\nConditionManager.prototype.onCompositionAdd = function (obj) {\n  let compositionKeys;\n  const telemetryAPI = this.openmct.telemetry;\n  const objId = makeKeyString(obj.identifier);\n  let telemetryMetadata;\n  const self = this;\n\n  if (telemetryAPI.isTelemetryObject(obj)) {\n    self.compositionObjs[objId] = obj;\n    self.telemetryMetadataById[objId] = {};\n\n    // FIXME: this should just update based on listener.\n    compositionKeys = self.domainObject.composition.map(makeKeyString);\n    if (!compositionKeys.includes(objId)) {\n      self.domainObject.composition.push(obj.identifier);\n    }\n\n    telemetryMetadata = telemetryAPI.getMetadata(obj).values();\n    telemetryMetadata.forEach(function (metaDatum) {\n      self.telemetryMetadataById[objId][metaDatum.key] = metaDatum;\n      self.addGlobalMetadata(metaDatum);\n    });\n\n    self.subscriptionCache[objId] = {};\n    self.subscriptions[objId] = telemetryAPI.subscribe(\n      obj,\n      function (datum) {\n        self.handleSubscriptionCallback(objId, datum);\n      },\n      {}\n    );\n    telemetryAPI\n      .request(obj, {\n        strategy: 'latest',\n        size: 1\n      })\n      .then(function (results) {\n        if (results && results.length) {\n          self.handleSubscriptionCallback(objId, results[results.length - 1]);\n        }\n      });\n\n    /**\n     * if this is the initial load, parsing property types will be postponed\n     * until all composition objects have been loaded\n     */\n    if (self.loadComplete) {\n      self.parsePropertyTypes(obj);\n    }\n\n    self.eventEmitter.emit('add', obj);\n\n    const summaryWidget = document.querySelector('.w-summary-widget');\n    if (summaryWidget) {\n      summaryWidget.classList.remove('s-status-no-data');\n    }\n  }\n};\n\n/**\n * Invoked on a remove event in this Summary Widget's composition. Removes\n * the object from the local composition, and untracks it\n * @param {Object} identifier The identifier of the object to be removed\n * @private\n */\nConditionManager.prototype.onCompositionRemove = function (identifier) {\n  const objectId = makeKeyString(identifier);\n  // FIXME: this should just update by listener.\n  _.remove(this.domainObject.composition, function (id) {\n    return id.key === identifier.key && id.namespace === identifier.namespace;\n  });\n  delete this.compositionObjs[objectId];\n  delete this.subscriptionCache[objectId];\n  this.subscriptions[objectId](); //unsubscribe from telemetry source\n  delete this.subscriptions[objectId];\n  this.eventEmitter.emit('remove', identifier);\n\n  if (_.isEmpty(this.compositionObjs)) {\n    const summaryWidget = document.querySelector('.w-summary-widget');\n    if (summaryWidget) {\n      summaryWidget.classList.add('s-status-no-data');\n    }\n  }\n};\n\n/**\n * Invoked when the Summary Widget's composition finishes its initial load.\n * Invokes any registered load callbacks, does a block load of all metadata,\n * and then invokes any registered metadata load callbacks.\n * @private\n */\nConditionManager.prototype.onCompositionLoad = function () {\n  this.loadComplete = true;\n  this.eventEmitter.emit('load');\n  this.parseAllPropertyTypes();\n};\n\n/**\n * Returns the currently tracked telemetry sources\n * @return {Object} An object mapping object keys to domain objects\n */\nConditionManager.prototype.getComposition = function () {\n  return this.compositionObjs;\n};\n\n/**\n * Get the human-readable name of a domain object from its key\n * @param {string} id The key of the domain object\n * @return {string} The human-readable name of the domain object\n */\nConditionManager.prototype.getObjectName = function (id) {\n  let name;\n\n  if (this.keywordLabels[id]) {\n    name = this.keywordLabels[id];\n  } else if (this.compositionObjs[id]) {\n    name = this.compositionObjs[id].name;\n  }\n\n  return name;\n};\n\n/**\n * Returns the property metadata associated with a given telemetry source\n * @param {string} id The key associated with the domain object\n * @return {Object} Returns an object with fields representing each telemetry field\n */\nConditionManager.prototype.getTelemetryMetadata = function (id) {\n  return this.telemetryMetadataById[id];\n};\n\n/**\n * Returns the type associated with a telemetry data field of a particular domain\n * object\n * @param {string} id The key associated with the domain object\n * @param {string} property The telemetry field key to retrieve the type of\n * @return {string} The type name\n */\nConditionManager.prototype.getTelemetryPropertyType = function (id, property) {\n  if (this.telemetryTypesById[id]) {\n    return this.telemetryTypesById[id][property];\n  }\n};\n\n/**\n * Returns the human-readable name of a telemetry data field of a particular domain\n * object\n * @param {string} id The key associated with the domain object\n * @param {string} property The telemetry field key to retrieve the type of\n * @return {string} The telemetry field name\n */\nConditionManager.prototype.getTelemetryPropertyName = function (id, property) {\n  if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) {\n    return this.telemetryMetadataById[id][property].name;\n  }\n};\n\n/**\n * Returns the {ConditionEvaluator} instance associated with this condition\n * manager\n * @return {ConditionEvaluator}\n */\nConditionManager.prototype.getEvaluator = function () {\n  return this.evaluator;\n};\n\n/**\n * Returns true if the initial composition load has completed\n * @return {boolean}\n */\nConditionManager.prototype.loadCompleted = function () {\n  return this.loadComplete;\n};\n\n/**\n * Returns true if the initial block metadata load has completed\n */\nConditionManager.prototype.metadataLoadCompleted = function () {\n  return this.metadataLoadComplete;\n};\n\n/**\n * Triggers the telemetryReceive callbacks registered to this ConditionManager,\n * used by the {TestDataManager} to force a rule evaluation when test data is\n * enabled\n */\nConditionManager.prototype.triggerTelemetryCallback = function () {\n  this.eventEmitter.emit('receiveTelemetry');\n};\n\n/**\n * Unsubscribe from all registered telemetry sources and unregister all event\n * listeners registered with the Open MCT APIs\n */\nConditionManager.prototype.destroy = function () {\n  Object.values(this.subscriptions).forEach(function (unsubscribeFunction) {\n    unsubscribeFunction();\n  });\n  this.composition.off('add', this.onCompositionAdd, this);\n  this.composition.off('remove', this.onCompositionRemove, this);\n  this.composition.off('load', this.onCompositionLoad, this);\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/Rule.js",
    "content": "import { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport ruleTemplate from '../res/ruleTemplate.html';\nimport Condition from './Condition.js';\nimport eventHelpers from './eventHelpers.js';\nimport ColorPalette from './input/ColorPalette.js';\nimport IconPalette from './input/IconPalette.js';\n\n/**\n * An object representing a summary widget rule. Maintains a set of text\n * and css properties for output, and a set of conditions for configuring\n * when the rule will be applied to the summary widget.\n * @constructor\n * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule\n * @param {Object} domainObject The Summary Widget domain object which contains this rule\n * @param {MCT} openmct An MCT instance\n * @param {ConditionManager} conditionManager A ConditionManager instance\n * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules\n * @param {element} container The DOM element which contains this summary widget\n */\nexport default function Rule(\n  ruleConfig,\n  domainObject,\n  openmct,\n  conditionManager,\n  widgetDnD,\n  container\n) {\n  eventHelpers.extend(this);\n  const self = this;\n  const THUMB_ICON_CLASS = 'c-sw__icon js-sw__icon';\n\n  this.config = ruleConfig;\n  this.domainObject = domainObject;\n  this.openmct = openmct;\n  this.conditionManager = conditionManager;\n  this.widgetDnD = widgetDnD;\n  this.container = container;\n\n  this.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0];\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange'];\n  this.conditions = [];\n  this.dragging = false;\n\n  this.remove = this.remove.bind(this);\n  this.duplicate = this.duplicate.bind(this);\n\n  this.thumbnail = this.domElement.querySelector('.t-widget-thumb');\n  this.thumbnailIcon = this.domElement.querySelector('.js-sw__icon');\n  this.thumbnailLabel = this.domElement.querySelector('.c-sw__label');\n  this.title = this.domElement.querySelector('.rule-title');\n  this.description = this.domElement.querySelector('.rule-description');\n  this.trigger = this.domElement.querySelector('.t-trigger');\n  this.toggleConfigButton = this.domElement.querySelector('.js-disclosure');\n  this.configArea = this.domElement.querySelector('.widget-rule-content');\n  this.grippy = this.domElement.querySelector('.t-grippy');\n  this.conditionArea = this.domElement.querySelector('.t-widget-rule-config');\n  this.jsConditionArea = this.domElement.querySelector('.t-rule-js-condition-input-holder');\n  this.deleteButton = this.domElement.querySelector('.t-delete');\n  this.duplicateButton = this.domElement.querySelector('.t-duplicate');\n  this.addConditionButton = this.domElement.querySelector('.add-condition');\n\n  /**\n   * The text inputs for this rule: any input included in this object will\n   * have the appropriate event handlers registered to it, and it's corresponding\n   * field in the domain object will be updated with its value\n   */\n\n  this.textInputs = {\n    name: this.domElement.querySelector('.t-rule-name-input'),\n    label: this.domElement.querySelector('.t-rule-label-input'),\n    message: this.domElement.querySelector('.t-rule-message-input'),\n    jsCondition: this.domElement.querySelector('.t-rule-js-condition-input')\n  };\n\n  this.iconInput = new IconPalette('', container);\n  this.colorInputs = {\n    'background-color': new ColorPalette('icon-paint-bucket', container),\n    'border-color': new ColorPalette('icon-line-horz', container),\n    color: new ColorPalette('icon-font', container)\n  };\n\n  this.colorInputs.color.toggleNullOption();\n\n  /**\n   * An onchange event handler method for this rule's icon palettes\n   * @param {string} icon The css class name corresponding to this icon\n   * @private\n   */\n  function onIconInput(icon) {\n    self.config.icon = icon;\n    self.updateDomainObject('icon', icon);\n    self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`;\n    self.eventEmitter.emit('change');\n  }\n\n  /**\n   * An onchange event handler method for this rule's color palettes palettes\n   * @param {string} color The color selected in the palette\n   * @param {string} property The css property which this color corresponds to\n   * @private\n   */\n  function onColorInput(color, property) {\n    self.config.style[property] = color;\n    self.thumbnail.style[property] = color;\n    self.eventEmitter.emit('change');\n  }\n\n  /**\n   * Parse input text from textbox to prevent HTML Injection\n   * @param {string} msg The text to be Parsed\n   * @private\n   */\n  function encodeMsg(msg) {\n    const div = document.createElement('div');\n    div.innerText = msg;\n\n    return div.innerText;\n  }\n\n  /**\n   * An onchange event handler method for this rule's trigger key\n   * @param {event} event The change event from this rule's select element\n   * @private\n   */\n  function onTriggerInput(event) {\n    const elem = event.target;\n    self.config.trigger = encodeMsg(elem.value);\n    self.generateDescription();\n    self.updateDomainObject();\n    self.refreshConditions();\n    self.eventEmitter.emit('conditionChange');\n  }\n\n  /**\n   * An onchange event handler method for this rule's text inputs\n   * @param {element} elem The input element that generated the event\n   * @param {string} inputKey The field of this rule's configuration to update\n   * @private\n   */\n  function onTextInput(elem, inputKey) {\n    const text = encodeMsg(elem.value);\n    self.config[inputKey] = text;\n    self.updateDomainObject();\n    if (inputKey === 'name') {\n      self.title.innerText = text;\n    } else if (inputKey === 'label') {\n      self.thumbnailLabel.innerText = text;\n    }\n\n    self.eventEmitter.emit('change');\n  }\n\n  /**\n   * An onchange event handler for a mousedown event that initiates a drag gesture\n   * @param {event} event A mouseup event that was registered on this rule's grippy\n   * @private\n   */\n  function onDragStart(event) {\n    document.querySelectorAll('.t-drag-indicator').forEach((indicator) => {\n      const ruleHeader = self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true);\n      indicator.textContent = '';\n      indicator.appendChild(ruleHeader);\n    });\n    self.widgetDnD.setDragImage(\n      self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true)\n    );\n    self.widgetDnD.dragStart(self.config.id);\n    self.domElement.style.display = 'none';\n  }\n\n  /**\n   * Show or hide this rule's configuration properties\n   * @private\n   */\n  function toggleConfig() {\n    if (self.configArea.classList.contains('expanded')) {\n      self.configArea.classList.remove('expanded');\n    } else {\n      self.configArea.classList.add('expanded');\n    }\n\n    if (self.toggleConfigButton.classList.contains('c-disclosure-triangle--expanded')) {\n      self.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded');\n    } else {\n      self.toggleConfigButton.classList.add('c-disclosure-triangle--expanded');\n    }\n\n    self.config.expanded = !self.config.expanded;\n  }\n\n  const labelInput = this.domElement.querySelector('.t-rule-label-input');\n  labelInput.parentNode.insertBefore(this.iconInput.getDOM(), labelInput);\n  this.iconInput.set(self.config.icon);\n  this.iconInput.on('change', function (value) {\n    onIconInput(value);\n  });\n\n  // Initialize thumbs when first loading\n  this.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + self.config.icon}`;\n  this.thumbnailLabel.innerText = self.config.label;\n\n  Object.keys(this.colorInputs).forEach(function (inputKey) {\n    const input = self.colorInputs[inputKey];\n\n    input.set(self.config.style[inputKey]);\n    onColorInput(self.config.style[inputKey], inputKey);\n\n    input.on('change', function (value) {\n      onColorInput(value, inputKey);\n      self.updateDomainObject();\n    });\n\n    self.domElement.querySelector('.t-style-input').append(input.getDOM());\n  });\n\n  Object.keys(this.textInputs).forEach(function (inputKey) {\n    if (self.textInputs[inputKey]) {\n      self.textInputs[inputKey].value = self.config[inputKey] || '';\n      self.listenTo(self.textInputs[inputKey], 'input', function () {\n        // eslint-disable-next-line no-invalid-this\n        onTextInput(this, inputKey);\n      });\n    }\n  });\n\n  this.listenTo(this.deleteButton, 'click', this.remove);\n  this.listenTo(this.duplicateButton, 'click', this.duplicate);\n  this.listenTo(this.addConditionButton, 'click', function () {\n    self.initCondition();\n  });\n  this.listenTo(this.toggleConfigButton, 'click', toggleConfig);\n  this.listenTo(this.trigger, 'change', onTriggerInput);\n\n  this.title.innerText = self.config.name;\n  this.description.innerText = self.config.description;\n  this.trigger.value = self.config.trigger;\n\n  this.listenTo(this.grippy, 'mousedown', onDragStart);\n  this.widgetDnD.on(\n    'drop',\n    function () {\n      // eslint-disable-next-line no-invalid-this\n      this.domElement.show();\n      document.querySelector('.t-drag-indicator').style.display = 'none';\n    },\n    this\n  );\n\n  if (!this.conditionManager.loadCompleted()) {\n    this.config.expanded = false;\n  }\n\n  if (!this.config.expanded) {\n    this.configArea.classList.remove('expanded');\n    this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded');\n  }\n\n  if (this.domainObject.configuration.ruleOrder.length === 2) {\n    this.domElement.querySelector('.t-grippy').style.display = 'none';\n  }\n\n  this.refreshConditions();\n\n  //if this is the default rule, hide elements that don't apply\n  if (this.config.id === 'default') {\n    this.domElement.querySelector('.t-delete').style.display = 'none';\n    this.domElement.querySelector('.t-widget-rule-config').style.display = 'none';\n    this.domElement.querySelector('.t-grippy').style.display = 'none';\n  }\n}\n\n/**\n * Return the DOM element representing this rule\n * @return {Element} A DOM element\n */\nRule.prototype.getDOM = function () {\n  return this.domElement;\n};\n\n/**\n * Unregister any event handlers registered with external sources\n */\nRule.prototype.destroy = function () {\n  Object.values(this.colorInputs).forEach(function (palette) {\n    palette.destroy();\n  });\n  this.iconInput.destroy();\n  this.stopListening();\n  this.conditions.forEach(function (condition) {\n    condition.destroy();\n  });\n};\n\n/**\n * Register a callback with this rule: supported callbacks are remove, change,\n * conditionChange, and duplicate\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nRule.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  }\n};\n\n/**\n * An event handler for when a condition's configuration is modified\n * @param {} value\n * @param {string} property The path in the configuration to updateDomainObject\n * @param {number} index The index of the condition that initiated this change\n */\nRule.prototype.onConditionChange = function (event) {\n  _.set(this.config.conditions[event.index], event.property, event.value);\n  this.generateDescription();\n  this.updateDomainObject();\n  this.eventEmitter.emit('conditionChange');\n};\n\n/**\n * During a rule drag event, show the placeholder element after this rule\n */\nRule.prototype.showDragIndicator = function () {\n  document.querySelector('.t-drag-indicator').style.display = 'none';\n  this.domElement.querySelector('.t-drag-indicator').style.display = '';\n};\n\n/**\n * Mutate the domain object with this rule's local configuration\n */\nRule.prototype.updateDomainObject = function () {\n  this.openmct.objects.mutate(\n    this.domainObject,\n    'configuration.ruleConfigById.' + this.config.id,\n    this.config\n  );\n};\n\n/**\n * Get a property of this rule by key\n * @param {string} prop They property key of this rule to get\n * @return {} The queried property\n */\nRule.prototype.getProperty = function (prop) {\n  return this.config[prop];\n};\n\n/**\n * Remove this rule from the domain object's configuration and invoke any\n * registered remove callbacks\n */\nRule.prototype.remove = function () {\n  const ruleOrder = this.domainObject.configuration.ruleOrder;\n  const ruleConfigById = this.domainObject.configuration.ruleConfigById;\n  const self = this;\n\n  ruleConfigById[self.config.id] = undefined;\n  _.remove(ruleOrder, function (ruleId) {\n    return ruleId === self.config.id;\n  });\n\n  this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById);\n  this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder);\n  this.destroy();\n  this.eventEmitter.emit('remove');\n};\n\n/**\n * Makes a deep clone of this rule's configuration, and calls the duplicate event\n * callback with the cloned configuration as an argument if one has been registered\n */\nRule.prototype.duplicate = function () {\n  const sourceRule = JSON.parse(JSON.stringify(this.config));\n  sourceRule.expanded = true;\n  this.eventEmitter.emit('duplicate', sourceRule);\n};\n\n/**\n * Initialize a new condition. If called with the sourceConfig and sourceIndex arguments,\n * will insert a new condition with the provided configuration after the sourceIndex\n * index. Otherwise, initializes a new blank rule and inserts it at the end\n * of the list.\n * @param {Object} [config] The configuration to initialize this rule from,\n *                          consisting of sourceCondition and index fields\n */\nRule.prototype.initCondition = function (config) {\n  const ruleConfigById = this.domainObject.configuration.ruleConfigById;\n  let newConfig;\n  const sourceIndex = config && config.index;\n  const defaultConfig = {\n    object: '',\n    key: '',\n    operation: '',\n    values: []\n  };\n\n  newConfig = config !== undefined ? config.sourceCondition : defaultConfig;\n  if (sourceIndex !== undefined) {\n    ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig);\n  } else {\n    ruleConfigById[this.config.id].conditions.push(newConfig);\n  }\n\n  this.domainObject.configuration.ruleConfigById = ruleConfigById;\n  this.updateDomainObject();\n  this.refreshConditions();\n  this.generateDescription();\n};\n\n/**\n * Build {Condition} objects from configuration and rebuild associated view\n */\nRule.prototype.refreshConditions = function () {\n  const self = this;\n  let $condition = null;\n  let loopCnt = 0;\n  const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and ';\n\n  self.conditions = [];\n\n  this.domElement.querySelectorAll('.t-condition').forEach((condition) => {\n    condition.remove();\n  });\n\n  this.config.conditions.forEach(function (condition, index) {\n    const newCondition = new Condition(condition, index, self.conditionManager);\n    newCondition.on('remove', self.removeCondition, self);\n    newCondition.on('duplicate', self.initCondition, self);\n    newCondition.on('change', self.onConditionChange, self);\n    self.conditions.push(newCondition);\n  });\n\n  if (this.config.trigger === 'js') {\n    if (this.jsConditionArea) {\n      this.jsConditionArea.style.display = '';\n    }\n\n    this.addConditionButton.style.display = 'none';\n  } else {\n    if (this.jsConditionArea) {\n      this.jsConditionArea.style.display = 'none';\n    }\n\n    this.addConditionButton.style.display = '';\n    self.conditions.forEach(function (condition) {\n      $condition = condition.getDOM();\n      const lastOfType = self.conditionArea.querySelector('li:last-of-type');\n      lastOfType.parentNode.insertBefore($condition, lastOfType);\n      if (loopCnt > 0) {\n        $condition.querySelector('.t-condition-context').innerText = triggerContextStr + ' when';\n      }\n\n      loopCnt++;\n    });\n  }\n\n  if (self.conditions.length === 1) {\n    self.conditions[0].hideButtons();\n  }\n};\n\n/**\n * Remove a condition from this rule's configuration at the given index\n * @param {number} removeIndex The index of the condition to remove\n */\nRule.prototype.removeCondition = function (removeIndex) {\n  const ruleConfigById = this.domainObject.configuration.ruleConfigById;\n  const conditions = ruleConfigById[this.config.id].conditions;\n\n  _.remove(conditions, function (condition, index) {\n    return index === removeIndex;\n  });\n\n  this.domainObject.configuration.ruleConfigById[this.config.id] = this.config;\n  this.updateDomainObject();\n  this.refreshConditions();\n  this.generateDescription();\n  this.eventEmitter.emit('conditionChange');\n};\n\n/**\n * Build a human-readable description from this rule's conditions\n */\nRule.prototype.generateDescription = function () {\n  let description = '';\n  const manager = this.conditionManager;\n  const evaluator = manager.getEvaluator();\n  let name;\n  let property;\n  let operation;\n  const self = this;\n\n  if (this.config.conditions && this.config.id !== 'default') {\n    if (self.config.trigger === 'js') {\n      description = 'when a custom JavaScript condition evaluates to true';\n    } else {\n      this.config.conditions.forEach(function (condition, index) {\n        name = manager.getObjectName(condition.object);\n        property = manager.getTelemetryPropertyName(condition.object, condition.key);\n        operation = evaluator.getOperationDescription(condition.operation, condition.values);\n        if (name || property || operation) {\n          description +=\n            'when ' +\n            (name ? name + \"'s \" : '') +\n            (property ? property + ' ' : '') +\n            (operation ? operation + ' ' : '') +\n            (self.config.trigger === 'any' ? ' OR ' : ' AND ');\n        }\n      });\n    }\n  }\n\n  if (description.endsWith('OR ')) {\n    description = description.substring(0, description.length - 3);\n  }\n\n  if (description.endsWith('AND ')) {\n    description = description.substring(0, description.length - 4);\n  }\n\n  description = description === '' ? this.config.description : description;\n  this.description.innerText = self.config.description;\n  this.config.description = description;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/SummaryWidget.js",
    "content": "import * as urlSanitizeLib from '@braintree/sanitize-url';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport widgetTemplate from '../res/widgetTemplate.html';\nimport ConditionManager from './ConditionManager.js';\nimport eventHelpers from './eventHelpers.js';\nimport Rule from './Rule.js';\nimport TestDataManager from './TestDataManager.js';\nimport WidgetDnD from './WidgetDnD.js';\n\n//default css configuration for new rules\nconst DEFAULT_PROPS = {\n  color: '#cccccc',\n  'background-color': '#666666',\n  'border-color': 'rgba(0,0,0,0)'\n};\n\n/**\n * A Summary Widget object, which allows a user to configure rules based\n * on telemetry producing domain objects, and update a compact display\n * accordingly.\n * @constructor\n * @param {Object} domainObject The domain Object represented by this Widget\n * @param {MCT} openmct An MCT instance\n */\nexport default function SummaryWidget(domainObject, openmct) {\n  eventHelpers.extend(this);\n\n  this.domainObject = domainObject;\n  this.openmct = openmct;\n\n  this.domainObject.configuration = this.domainObject.configuration || {};\n  this.domainObject.configuration.ruleConfigById =\n    this.domainObject.configuration.ruleConfigById || {};\n  this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || [\n    'default'\n  ];\n  this.domainObject.configuration.testDataConfig = this.domainObject.configuration\n    .testDataConfig || [\n    {\n      object: '',\n      key: '',\n      value: ''\n    }\n  ];\n\n  this.activeId = 'default';\n  this.rulesById = {};\n  this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0];\n  this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules');\n  this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data');\n\n  this.widgetButton = this.domElement.querySelector(':scope > #widget');\n\n  this.editing = false;\n  this.container = '';\n  this.editListenerUnsubscribe = () => {};\n\n  this.outerWrapper = this.domElement.querySelector('.widget-edit-holder');\n  this.ruleArea = this.domElement.querySelector('#ruleArea');\n  this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper');\n\n  this.testDataArea = this.domElement.querySelector('.widget-test-data');\n  this.addRuleButton = this.domElement.querySelector('#addRule');\n\n  this.conditionManager = new ConditionManager(this.domainObject, this.openmct);\n  this.testDataManager = new TestDataManager(\n    this.domainObject,\n    this.conditionManager,\n    this.openmct\n  );\n\n  this.watchForChanges = this.watchForChanges.bind(this);\n  this.show = this.show.bind(this);\n  this.destroy = this.destroy.bind(this);\n  this.addRule = this.addRule.bind(this);\n\n  this.addHyperlink(domainObject.url, domainObject.openNewTab);\n  this.watchForChanges(openmct, domainObject);\n\n  const self = this;\n\n  /**\n   * Toggles the configuration area for test data in the view\n   * @private\n   */\n  function toggleTestData() {\n    if (self.outerWrapper.classList.contains('expanded-widget-test-data')) {\n      self.outerWrapper.classList.remove('expanded-widget-test-data');\n    } else {\n      self.outerWrapper.classList.add('expanded-widget-test-data');\n    }\n\n    if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) {\n      self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded');\n    } else {\n      self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded');\n    }\n  }\n\n  this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);\n\n  /**\n   * Toggles the configuration area for rules in the view\n   * @private\n   */\n  function toggleRules() {\n    templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules');\n    templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded');\n  }\n\n  this.listenTo(this.toggleRulesControl, 'click', toggleRules);\n}\n\n/**\n * adds or removes href to widget button and adds or removes openInNewTab\n * @param {string} url String that denotes the url to be opened\n * @param {string} openNewTab String that denotes wether to open link in new tab or not\n */\nSummaryWidget.prototype.addHyperlink = function (url, openNewTab) {\n  if (url) {\n    this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url);\n  } else {\n    this.widgetButton.removeAttribute('href');\n  }\n\n  if (openNewTab === 'newTab') {\n    this.widgetButton.target = '_blank';\n  } else {\n    this.widgetButton.removeAttribute('target');\n  }\n};\n\n/**\n * adds a listener to the object to watch for any changes made by user\n * only executes if changes are observed\n * @param {openmct} Object Instance of OpenMCT\n * @param {domainObject} Object instance of this object\n */\nSummaryWidget.prototype.watchForChanges = function (openmct, domainObject) {\n  this.watchForChangesUnsubscribe = openmct.objects.observe(\n    domainObject,\n    '*',\n    function (newDomainObject) {\n      if (\n        newDomainObject.url !== this.domainObject.url ||\n        newDomainObject.openNewTab !== this.domainObject.openNewTab\n      ) {\n        this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab);\n      }\n    }.bind(this)\n  );\n};\n\n/**\n * Builds the Summary Widget's DOM, performs other necessary setup, and attaches\n * this Summary Widget's view to the supplied container.\n * @param {element} container The DOM element that will contain this Summary\n *                            Widget's view.\n */\nSummaryWidget.prototype.show = function (container) {\n  const self = this;\n  this.container = container;\n  this.container.append(this.domElement);\n  this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM());\n  this.widgetDnD = new WidgetDnD(\n    this.domElement,\n    this.domainObject.configuration.ruleOrder,\n    this.rulesById\n  );\n  this.initRule('default', 'Default');\n  this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {\n    if (ruleId !== 'default') {\n      self.initRule(ruleId);\n    }\n  });\n  this.refreshRules();\n  this.updateWidget();\n\n  this.listenTo(this.addRuleButton, 'click', this.addRule);\n  this.conditionManager.on('receiveTelemetry', this.executeRules, this);\n  this.widgetDnD.on('drop', this.reorder, this);\n};\n\n/**\n * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry,\n * and clean up event handlers\n */\nSummaryWidget.prototype.destroy = function (container) {\n  this.editListenerUnsubscribe();\n  this.conditionManager.destroy();\n  this.testDataManager.destroy();\n  this.widgetDnD.destroy();\n  this.watchForChangesUnsubscribe();\n  Object.values(this.rulesById).forEach(function (rule) {\n    rule.destroy();\n  });\n\n  this.stopListening();\n};\n\n/**\n * Update the view from the current rule configuration and order\n */\nSummaryWidget.prototype.refreshRules = function () {\n  const self = this;\n  const ruleOrder = self.domainObject.configuration.ruleOrder;\n  const rules = self.rulesById;\n  self.ruleArea.innerHTML = '';\n  Object.values(ruleOrder).forEach(function (ruleId) {\n    self.ruleArea.append(rules[ruleId].getDOM());\n  });\n\n  this.executeRules();\n  this.addOrRemoveDragIndicator();\n};\n\nSummaryWidget.prototype.addOrRemoveDragIndicator = function () {\n  const rules = this.domainObject.configuration.ruleOrder;\n  const rulesById = this.rulesById;\n\n  rules.forEach(function (ruleKey, index, array) {\n    if (array.length > 2 && index > 0) {\n      rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = '';\n    } else {\n      rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none';\n    }\n  });\n};\n\n/**\n * Update the widget's appearance from the configuration of the active rule\n */\nSummaryWidget.prototype.updateWidget = function () {\n  const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';\n  const activeRule = this.rulesById[this.activeId];\n\n  this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style'));\n  this.domElement.querySelector('#widget').title = activeRule.getProperty('message');\n  this.domElement.querySelector('#widgetLabel').textContent = activeRule.getProperty('label');\n  this.domElement.querySelector('#widgetIcon').classList =\n    WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon');\n};\n\n/**\n * Get the active rule and update the Widget's appearance.\n */\nSummaryWidget.prototype.executeRules = function () {\n  this.activeId = this.conditionManager.executeRules(\n    this.domainObject.configuration.ruleOrder,\n    this.rulesById\n  );\n  this.updateWidget();\n};\n\n/**\n * Add a new rule to this widget\n */\nSummaryWidget.prototype.addRule = function () {\n  let ruleCount = 0;\n  let ruleId;\n  const ruleOrder = this.domainObject.configuration.ruleOrder;\n\n  while (Object.keys(this.rulesById).includes('rule' + ruleCount)) {\n    ruleCount++;\n  }\n\n  ruleId = 'rule' + ruleCount;\n  ruleOrder.push(ruleId);\n  this.domainObject.configuration.ruleOrder = ruleOrder;\n\n  this.initRule(ruleId, 'Rule');\n  this.updateDomainObject();\n  this.refreshRules();\n};\n\n/**\n * Duplicate an existing widget rule from its configuration and splice it in\n * after the rule it duplicates\n * @param {Object} sourceConfig The configuration properties of the rule to be\n *                              instantiated\n */\nSummaryWidget.prototype.duplicateRule = function (sourceConfig) {\n  let ruleCount = 0;\n  let ruleId;\n  const sourceRuleId = sourceConfig.id;\n  const ruleOrder = this.domainObject.configuration.ruleOrder;\n  const ruleIds = Object.keys(this.rulesById);\n\n  while (ruleIds.includes('rule' + ruleCount)) {\n    ruleCount = ++ruleCount;\n  }\n\n  ruleId = 'rule' + ruleCount;\n  sourceConfig.id = ruleId;\n  sourceConfig.name += ' Copy';\n  ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId);\n  this.domainObject.configuration.ruleOrder = ruleOrder;\n  this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig;\n  this.initRule(ruleId, sourceConfig.name);\n  this.updateDomainObject();\n  this.refreshRules();\n};\n\n/**\n * Initialize a new rule from a default configuration, or build a {Rule} object\n * from it if already exists\n * @param {string} ruleId An key to be used to identify this ruleId, or the key\n                          of the rule to be instantiated\n  * @param {string} ruleName The initial human-readable name of this rule\n  */\nSummaryWidget.prototype.initRule = function (ruleId, ruleName) {\n  let ruleConfig;\n  const styleObj = {};\n\n  Object.assign(styleObj, DEFAULT_PROPS);\n  if (!this.domainObject.configuration.ruleConfigById[ruleId]) {\n    this.domainObject.configuration.ruleConfigById[ruleId] = {\n      name: ruleName || 'Rule',\n      label: 'Unnamed Rule',\n      message: '',\n      id: ruleId,\n      icon: ' ',\n      style: styleObj,\n      description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule',\n      conditions: [\n        {\n          object: '',\n          key: '',\n          operation: '',\n          values: []\n        }\n      ],\n      jsCondition: '',\n      trigger: 'any',\n      expanded: 'true'\n    };\n  }\n\n  ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];\n  this.rulesById[ruleId] = new Rule(\n    ruleConfig,\n    this.domainObject,\n    this.openmct,\n    this.conditionManager,\n    this.widgetDnD,\n    this.container\n  );\n  this.rulesById[ruleId].on('remove', this.refreshRules, this);\n  this.rulesById[ruleId].on('duplicate', this.duplicateRule, this);\n  this.rulesById[ruleId].on('change', this.updateWidget, this);\n  this.rulesById[ruleId].on('conditionChange', this.executeRules, this);\n};\n\n/**\n * Given two ruleIds, move the source rule after the target rule and update\n * the view.\n * @param {Object} event An event object representing this drop with draggingId\n *                       and dropTarget fields\n */\nSummaryWidget.prototype.reorder = function (event) {\n  const ruleOrder = this.domainObject.configuration.ruleOrder;\n  const sourceIndex = ruleOrder.indexOf(event.draggingId);\n  let targetIndex;\n\n  if (event.draggingId !== event.dropTarget) {\n    ruleOrder.splice(sourceIndex, 1);\n    targetIndex = ruleOrder.indexOf(event.dropTarget);\n    ruleOrder.splice(targetIndex + 1, 0, event.draggingId);\n    this.domainObject.configuration.ruleOrder = ruleOrder;\n    this.updateDomainObject();\n  }\n\n  this.refreshRules();\n};\n\n/**\n * Apply a list of css properties to an element\n * @param {element} elem The DOM element to which the rules will be applied\n * @param {Object} style an object representing the style\n */\nSummaryWidget.prototype.applyStyle = function (elem, style) {\n  Object.keys(style).forEach(function (propId) {\n    elem.style[propId] = style[propId];\n  });\n};\n\n/**\n * Mutate this domain object's configuration with the current local configuration\n */\nSummaryWidget.prototype.updateDomainObject = function () {\n  this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration);\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/TestDataItem.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport itemTemplate from '../res/testDataItemTemplate.html';\nimport eventHelpers from './eventHelpers.js';\nimport KeySelect from './input/KeySelect.js';\nimport ObjectSelect from './input/ObjectSelect.js';\n\n/**\n * An object representing a single mock telemetry value\n * @param {Object} itemConfig the configuration for this item, consisting of\n *                            object, key, and value fields\n * @param {number} index the index of this TestDataItem object in the data\n *                 model of its parent {TestDataManager} o be injected into callbacks\n *                 for removes\n * @param {ConditionManager} conditionManager a conditionManager instance\n *                           for populating selects with configuration data\n * @constructor\n */\nexport default function TestDataItem(itemConfig, index, conditionManager) {\n  eventHelpers.extend(this);\n  this.config = itemConfig;\n  this.index = index;\n  this.conditionManager = conditionManager;\n\n  this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0];\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['remove', 'duplicate', 'change'];\n\n  this.deleteButton = this.domElement.querySelector('.t-delete');\n  this.duplicateButton = this.domElement.querySelector('.t-duplicate');\n\n  this.selects = {};\n  this.valueInputs = [];\n\n  this.remove = this.remove.bind(this);\n  this.duplicate = this.duplicate.bind(this);\n\n  const self = this;\n\n  /**\n   * A change event handler for this item's select inputs, which also invokes\n   * change callbacks registered with this item\n   * @param {string} value The new value of this select item\n   * @param {string} property The property of this item to modify\n   * @private\n   */\n  function onSelectChange(value, property) {\n    if (property === 'key') {\n      self.generateValueInput(value);\n    }\n\n    self.eventEmitter.emit('change', {\n      value: value,\n      property: property,\n      index: self.index\n    });\n  }\n\n  /**\n   * An input event handler for this item's value field. Invokes any change\n   * callbacks associated with this item\n   * @param {Event} event The input event that initiated this callback\n   * @private\n   */\n  function onValueInput(event) {\n    const elem = event.target;\n    const value = isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber;\n\n    if (elem.tagName.toUpperCase() === 'INPUT') {\n      self.eventEmitter.emit('change', {\n        value: value,\n        property: 'value',\n        index: self.index\n      });\n    }\n  }\n\n  this.listenTo(this.deleteButton, 'click', this.remove);\n  this.listenTo(this.duplicateButton, 'click', this.duplicate);\n\n  this.selects.object = new ObjectSelect(this.config, this.conditionManager);\n  this.selects.key = new KeySelect(\n    this.config,\n    this.selects.object,\n    this.conditionManager,\n    function (value) {\n      onSelectChange(value, 'key');\n    }\n  );\n\n  this.selects.object.on('change', function (value) {\n    onSelectChange(value, 'object');\n  });\n\n  Object.values(this.selects).forEach(function (select) {\n    self.domElement.querySelector('.t-configuration').append(select.getDOM());\n  });\n  this.listenTo(this.domElement, 'input', onValueInput);\n}\n\n/**\n * Gets the DOM associated with this element's view\n * @return {Element}\n */\nTestDataItem.prototype.getDOM = function (container) {\n  return this.domElement;\n};\n\n/**\n * Register a callback with this item: supported callbacks are remove, change,\n * and duplicate\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nTestDataItem.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  }\n};\n\n/**\n * Implement \"off\" to complete event emitter interface.\n */\nTestDataItem.prototype.off = function (event, callback, context) {\n  this.eventEmitter.off(event, callback, context);\n};\n\n/**\n * Hide the appropriate inputs when this is the only item\n */\nTestDataItem.prototype.hideButtons = function () {\n  this.deleteButton.style.display = 'none';\n};\n\n/**\n * Remove this item from the configuration. Invokes any registered\n * remove callbacks\n */\nTestDataItem.prototype.remove = function () {\n  const self = this;\n  this.eventEmitter.emit('remove', self.index);\n  this.stopListening();\n\n  Object.values(this.selects).forEach(function (select) {\n    select.destroy();\n  });\n};\n\n/**\n * Makes a deep clone of this item's configuration, and invokes any registered\n * duplicate callbacks with the cloned configuration as an argument\n */\nTestDataItem.prototype.duplicate = function () {\n  const sourceItem = JSON.parse(JSON.stringify(this.config));\n  const self = this;\n\n  this.eventEmitter.emit('duplicate', {\n    sourceItem: sourceItem,\n    index: self.index\n  });\n};\n\n/**\n * When a telemetry property key is selected, create the appropriate value input\n * and add it to the view\n * @param {string} key The key of currently selected telemetry property\n */\nTestDataItem.prototype.generateValueInput = function (key) {\n  const evaluator = this.conditionManager.getEvaluator();\n  const inputArea = this.domElement.querySelector('.t-value-inputs');\n  const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key);\n  const inputType = evaluator.getInputTypeById(dataType);\n\n  inputArea.innerHTML = '';\n  if (inputType) {\n    if (!this.config.value) {\n      this.config.value = inputType === 'number' ? 0 : '';\n    }\n\n    const newInput = document.createElement('input');\n    newInput.type = `${inputType}`;\n    newInput.value = `${this.config.value}`;\n\n    this.valueInput = newInput;\n    inputArea.append(this.valueInput);\n  }\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/TestDataManager.js",
    "content": "import _ from 'lodash';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport testDataTemplate from '../res/testDataTemplate.html';\nimport eventHelpers from './eventHelpers.js';\nimport TestDataItem from './TestDataItem.js';\n\n/**\n * Controls the input and usage of test data in the summary widget.\n * @constructor\n * @param {Object} domainObject The summary widget domain object\n * @param {ConditionManager} conditionManager A conditionManager instance\n * @param {MCT} openmct and MCT instance\n */\nexport default function TestDataManager(domainObject, conditionManager, openmct) {\n  eventHelpers.extend(this);\n  const self = this;\n\n  this.domainObject = domainObject;\n  this.manager = conditionManager;\n  this.openmct = openmct;\n\n  this.evaluator = this.manager.getEvaluator();\n  this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0];\n  this.config = this.domainObject.configuration.testDataConfig;\n  this.testCache = {};\n\n  this.itemArea = this.domElement.querySelector('.t-test-data-config');\n  this.addItemButton = this.domElement.querySelector('.add-test-condition');\n  this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox');\n\n  /**\n   * Toggles whether the associated {ConditionEvaluator} uses the actual\n   * subscription cache or the test data cache\n   * @param {Event} event The change event that triggered this callback\n   * @private\n   */\n  function toggleTestData(event) {\n    const elem = event.target;\n    self.evaluator.useTestData(elem.checked);\n    self.updateTestCache();\n  }\n\n  this.listenTo(this.addItemButton, 'click', function () {\n    self.initItem();\n  });\n  this.listenTo(this.testDataInput, 'change', toggleTestData);\n\n  this.evaluator.setTestDataCache(this.testCache);\n  this.evaluator.useTestData(false);\n\n  this.refreshItems();\n}\n\n/**\n * Get the DOM element representing this test data manager in the view\n */\nTestDataManager.prototype.getDOM = function () {\n  return this.domElement;\n};\n\n/**\n * Initialize a new test data item, either from a source configuration, or with\n * the default empty configuration\n * @param {Object} [config] An object with sourceItem and index fields to instantiate\n *                          this rule from, optional\n */\nTestDataManager.prototype.initItem = function (config) {\n  const sourceIndex = config && config.index;\n  const defaultItem = {\n    object: '',\n    key: '',\n    value: ''\n  };\n  let newItem;\n\n  newItem = config !== undefined ? config.sourceItem : defaultItem;\n  if (sourceIndex !== undefined) {\n    this.config.splice(sourceIndex + 1, 0, newItem);\n  } else {\n    this.config.push(newItem);\n  }\n\n  this.updateDomainObject();\n  this.refreshItems();\n};\n\n/**\n * Remove an item from this TestDataManager at the given index\n * @param {number} removeIndex The index of the item to remove\n */\nTestDataManager.prototype.removeItem = function (removeIndex) {\n  _.remove(this.config, function (item, index) {\n    return index === removeIndex;\n  });\n  this.updateDomainObject();\n  this.refreshItems();\n};\n\n/**\n * Change event handler for the test data items which compose this\n * test data generator\n * @param {Object} event An object representing this event, with value, property,\n *                       and index fields\n */\nTestDataManager.prototype.onItemChange = function (event) {\n  this.config[event.index][event.property] = event.value;\n  this.updateDomainObject();\n  this.updateTestCache();\n};\n\n/**\n * Builds the test cache from the current item configuration, and passes\n * the new test cache to the associated {ConditionEvaluator} instance\n */\nTestDataManager.prototype.updateTestCache = function () {\n  this.generateTestCache();\n  this.evaluator.setTestDataCache(this.testCache);\n  this.manager.triggerTelemetryCallback();\n};\n\n/**\n * Instantiate {TestDataItem} objects from the current configuration, and\n * update the view accordingly\n */\nTestDataManager.prototype.refreshItems = function () {\n  const self = this;\n  if (this.items) {\n    this.items.forEach(function (item) {\n      this.stopListening(item);\n    }, this);\n  }\n\n  self.items = [];\n\n  this.domElement.querySelectorAll('.t-test-data-item').forEach((item) => {\n    item.remove();\n  });\n\n  this.config.forEach(function (item, index) {\n    const newItem = new TestDataItem(item, index, self.manager);\n    self.listenTo(newItem, 'remove', self.removeItem, self);\n    self.listenTo(newItem, 'duplicate', self.initItem, self);\n    self.listenTo(newItem, 'change', self.onItemChange, self);\n    self.items.push(newItem);\n  });\n\n  self.items.forEach(function (item) {\n    self.itemArea.prepend(item.getDOM());\n  });\n\n  if (self.items.length === 1) {\n    self.items[0].hideButtons();\n  }\n\n  this.updateTestCache();\n};\n\n/**\n * Builds a test data cache in the format of a telemetry subscription cache\n * as expected by a {ConditionEvaluator}\n */\nTestDataManager.prototype.generateTestCache = function () {\n  let testCache = this.testCache;\n  const manager = this.manager;\n  const compositionObjs = manager.getComposition();\n  let metadata;\n\n  testCache = {};\n  Object.keys(compositionObjs).forEach(function (id) {\n    testCache[id] = {};\n    metadata = manager.getTelemetryMetadata(id);\n    Object.keys(metadata).forEach(function (key) {\n      testCache[id][key] = '';\n    });\n  });\n  this.config.forEach(function (item) {\n    if (testCache[item.object]) {\n      testCache[item.object][item.key] = item.value;\n    }\n  });\n\n  this.testCache = testCache;\n};\n\n/**\n * Update the domain object configuration associated with this test data manager\n */\nTestDataManager.prototype.updateDomainObject = function () {\n  this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config);\n};\n\nTestDataManager.prototype.destroy = function () {\n  this.stopListening();\n  this.items.forEach(function (item) {\n    item.remove();\n  });\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/WidgetDnD.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nimport * as templateHelpers from '../../../utils/template/templateHelpers.js';\nimport ruleImageTemplate from '../res/ruleImageTemplate.html';\n\n/**\n * Manages the Sortable List interface for reordering rules by drag and drop\n * @param {Element} container The DOM element that contains this Summary Widget's view\n * @param {string[]} ruleOrder An array of rule IDs representing the current rule order\n * @param {Object} rulesById An object mapping rule IDs to rule configurations\n */\nexport default function WidgetDnD(container, ruleOrder, rulesById) {\n  this.container = container;\n  this.ruleOrder = ruleOrder;\n  this.rulesById = rulesById;\n\n  this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0];\n  this.image = this.imageContainer.querySelector('.t-drag-rule-image');\n  this.draggingId = '';\n  this.draggingRulePrevious = '';\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['drop'];\n\n  this.drag = this.drag.bind(this);\n  this.drop = this.drop.bind(this);\n\n  this.container.addEventListener('mousemove', this.drag);\n  document.addEventListener('mouseup', this.drop);\n  this.container.parentNode.insertBefore(this.imageContainer, this.container);\n  this.imageContainer.style.display = 'none';\n}\n\n/**\n * Remove event listeners registered to elements external to the widget\n */\nWidgetDnD.prototype.destroy = function () {\n  this.container.removeEventListener('mousemove', this.drag);\n  document.removeEventListener('mouseup', this.drop);\n};\n\n/**\n * Register a callback with this WidgetDnD: supported callback is drop\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nWidgetDnD.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  }\n};\n\n/**\n * Sets the image for the dragged element to the given DOM element\n * @param {Element} image The HTML element to set as the drap image\n */\nWidgetDnD.prototype.setDragImage = function (image) {\n  this.image.html(image);\n};\n\n/**\n   * Calculate where this rule has been dragged relative to the other rules\n   * @param {Event} event The mousemove or mouseup event that triggered this\n                          event handler\n    * @return {string} The ID of the rule whose drag indicator should be displayed\n    */\nWidgetDnD.prototype.getDropLocation = function (event) {\n  const ruleOrder = this.ruleOrder;\n  const rulesById = this.rulesById;\n  const draggingId = this.draggingId;\n  let offset;\n  let y;\n  let height;\n  const dropY = event.pageY;\n  let target = '';\n\n  ruleOrder.forEach(function (ruleId, index) {\n    const ruleDOM = rulesById[ruleId].getDOM();\n    offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth);\n    y = offset.top;\n    height = offset.height;\n    if (index === 0) {\n      if (dropY < y + (7 * height) / 3) {\n        target = ruleId;\n      }\n    } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) {\n      if (y + height / 3 < dropY) {\n        target = ruleId;\n      }\n    } else {\n      if (y + height / 3 < dropY && dropY < y + (7 * height) / 3) {\n        target = ruleId;\n      }\n    }\n  });\n\n  return target;\n};\n\n/**\n * Called by a {Rule} instance that initiates a drag gesture\n * @param {string} ruleId The identifier of the rule which is being dragged\n */\nWidgetDnD.prototype.dragStart = function (ruleId) {\n  const ruleOrder = this.ruleOrder;\n  this.draggingId = ruleId;\n  this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1];\n  this.rulesById[this.draggingRulePrevious].showDragIndicator();\n  this.imageContainer.show();\n  this.imageContainer.offset({\n    top: event.pageY - this.image.height() / 2,\n    left: event.pageX - this.image.querySelector('.t-grippy').style.width\n  });\n};\n\n/**\n * An event handler for a mousemove event, once a rule has begun a drag gesture\n * @param {Event} event The mousemove event that triggered this callback\n */\nWidgetDnD.prototype.drag = function (event) {\n  let dragTarget;\n  if (this.draggingId && this.draggingId !== '') {\n    event.preventDefault();\n    dragTarget = this.getDropLocation(event);\n    this.imageContainer.offset({\n      top: event.pageY - this.image.height() / 2,\n      left: event.pageX - this.image.querySelector('.t-grippy').style.width\n    });\n    if (this.rulesById[dragTarget]) {\n      this.rulesById[dragTarget].showDragIndicator();\n    } else {\n      this.rulesById[this.draggingRulePrevious].showDragIndicator();\n    }\n  }\n};\n\n/**\n * Handles the mouseup event that corresponds to the user dropping the rule\n * in its final location. Invokes any registered drop callbacks with the dragged\n * rule's ID and the ID of the target rule that the dragged rule should be\n * inserted after\n * @param {Event} event The mouseup event that triggered this callback\n */\nWidgetDnD.prototype.drop = function (event) {\n  let dropTarget = this.getDropLocation(event);\n  const draggingId = this.draggingId;\n\n  if (this.draggingId && this.draggingId !== '') {\n    if (!this.rulesById[dropTarget]) {\n      dropTarget = this.draggingId;\n    }\n\n    this.eventEmitter.emit('drop', {\n      draggingId: draggingId,\n      dropTarget: dropTarget\n    });\n    this.draggingId = '';\n    this.draggingRulePrevious = '';\n    this.imageContainer.hide();\n  }\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/eventHelpers.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst helperFunctions = {\n  listenTo(object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    const listener = {\n      object: object,\n      event: event,\n      callback: callback,\n      context: context,\n      _cb: context ? callback.bind(context) : callback\n    };\n    if (object.$watch && event.indexOf('change:') === 0) {\n      const scopePath = event.replace('change:', '');\n      listener.unlisten = object.$watch(scopePath, listener._cb, true);\n    } else if (object.$on) {\n      listener.unlisten = object.$on(event, listener._cb);\n    } else if (object.addEventListener) {\n      object.addEventListener(event, listener._cb);\n    } else {\n      object.on(event, listener._cb, listener.context);\n    }\n\n    this._listeningTo.push(listener);\n  },\n\n  stopListening(object, event, callback, context) {\n    if (!this._listeningTo) {\n      this._listeningTo = [];\n    }\n\n    this._listeningTo\n      .filter(function (listener) {\n        if (object && object !== listener.object) {\n          return false;\n        }\n\n        if (event && event !== listener.event) {\n          return false;\n        }\n\n        if (callback && callback !== listener.callback) {\n          return false;\n        }\n\n        if (context && context !== listener.context) {\n          return false;\n        }\n\n        return true;\n      })\n      .map(function (listener) {\n        if (listener.unlisten) {\n          listener.unlisten();\n        } else if (listener.object.removeEventListener) {\n          listener.object.removeEventListener(listener.event, listener._cb);\n        } else {\n          listener.object.off(listener.event, listener._cb, listener.context);\n        }\n\n        return listener;\n      })\n      .forEach(function (listener) {\n        this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);\n      }, this);\n  },\n\n  extend: function (object) {\n    object.listenTo = helperFunctions.listenTo;\n    object.stopListening = helperFunctions.stopListening;\n  }\n};\n\nexport default helperFunctions;\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/ColorPalette.js",
    "content": "import Palette from './Palette.js';\n\n// The colors that will be used to instantiate this palette if none are provided\nconst DEFAULT_COLORS = [\n  '#000000',\n  '#434343',\n  '#666666',\n  '#999999',\n  '#b7b7b7',\n  '#cccccc',\n  '#d9d9d9',\n  '#efefef',\n  '#f3f3f3',\n  '#ffffff',\n  '#980000',\n  '#ff0000',\n  '#ff9900',\n  '#ffff00',\n  '#00ff00',\n  '#00ffff',\n  '#4a86e8',\n  '#0000ff',\n  '#9900ff',\n  '#ff00ff',\n  '#e6b8af',\n  '#f4cccc',\n  '#fce5cd',\n  '#fff2cc',\n  '#d9ead3',\n  '#d0e0e3',\n  '#c9daf8',\n  '#cfe2f3',\n  '#d9d2e9',\n  '#ead1dc',\n  '#dd7e6b',\n  '#dd7e6b',\n  '#f9cb9c',\n  '#ffe599',\n  '#b6d7a8',\n  '#a2c4c9',\n  '#a4c2f4',\n  '#9fc5e8',\n  '#b4a7d6',\n  '#d5a6bd',\n  '#cc4125',\n  '#e06666',\n  '#f6b26b',\n  '#ffd966',\n  '#93c47d',\n  '#76a5af',\n  '#6d9eeb',\n  '#6fa8dc',\n  '#8e7cc3',\n  '#c27ba0',\n  '#a61c00',\n  '#cc0000',\n  '#e69138',\n  '#f1c232',\n  '#6aa84f',\n  '#45818e',\n  '#3c78d8',\n  '#3d85c6',\n  '#674ea7',\n  '#a64d79',\n  '#85200c',\n  '#990000',\n  '#b45f06',\n  '#bf9000',\n  '#38761d',\n  '#134f5c',\n  '#1155cc',\n  '#0b5394',\n  '#351c75',\n  '#741b47',\n  '#5b0f00',\n  '#660000',\n  '#783f04',\n  '#7f6000',\n  '#274e13',\n  '#0c343d',\n  '#1c4587',\n  '#073763',\n  '#20124d',\n  '#4c1130'\n];\n\n/**\n * Instantiates a new Open MCT Color Palette input\n * @constructor\n * @param {string} cssClass The class name of the icon which should be applied\n *                          to this palette\n * @param {Element} container The view that contains this palette\n * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette\n */\nexport default function ColorPalette(cssClass, container, colors) {\n  this.colors = colors || DEFAULT_COLORS;\n  this.palette = new Palette(cssClass, container, this.colors);\n\n  this.palette.setNullOption('rgba(0,0,0,0)');\n\n  const domElement = this.palette.getDOM();\n  const self = this;\n\n  domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');\n  domElement.querySelector('.t-swatch').classList.add('color-swatch');\n  domElement.querySelector('.c-palette').classList.add('c-palette--color');\n\n  domElement.querySelectorAll('.c-palette__item').forEach((item) => {\n    item.style.backgroundColor = item.dataset.item;\n  });\n\n  /**\n   * Update this palette's current selection indicator with the style\n   * of the currently selected item\n   * @private\n   */\n  function updateSwatch() {\n    const color = self.palette.getCurrent();\n    domElement.querySelector('.color-swatch').style.backgroundColor = color;\n  }\n\n  this.palette.on('change', updateSwatch);\n\n  return this.palette;\n}\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/IconPalette.js",
    "content": "import Palette from './Palette.js';\n\n//The icons that will be used to instantiate this palette if none are provided\nconst DEFAULT_ICONS = [\n  'icon-alert-rect',\n  'icon-alert-triangle',\n  'icon-arrow-down',\n  'icon-arrow-left',\n  'icon-arrow-right',\n  'icon-arrow-double-up',\n  'icon-arrow-tall-up',\n  'icon-arrow-tall-down',\n  'icon-arrow-double-down',\n  'icon-arrow-up',\n  'icon-asterisk',\n  'icon-bell',\n  'icon-check',\n  'icon-eye-open',\n  'icon-gear',\n  'icon-hourglass',\n  'icon-info',\n  'icon-link',\n  'icon-lock',\n  'icon-people',\n  'icon-person',\n  'icon-plus',\n  'icon-trash',\n  'icon-x'\n];\n\n/**\n * Instantiates a new Open MCT Icon Palette input\n * @constructor\n * @param {string} cssClass The class name of the icon which should be applied\n *                          to this palette\n * @param {Element} container The view that contains this palette\n * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette\n */\nexport default function IconPalette(cssClass, container, icons) {\n  this.icons = icons || DEFAULT_ICONS;\n  this.palette = new Palette(cssClass, container, this.icons);\n\n  this.palette.setNullOption('');\n  this.oldIcon = this.palette.current || '';\n\n  const domElement = this.palette.getDOM();\n  const self = this;\n\n  domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');\n  domElement.querySelector('.t-swatch').classList.add('icon-swatch');\n  domElement.querySelector('.c-palette').classList.add('c-palette--icon');\n\n  domElement.querySelectorAll('.c-palette-item').forEach((item) => {\n    item.classList.add(item.dataset.item);\n  });\n\n  /**\n   * Update this palette's current selection indicator with the style\n   * of the currently selected item\n   * @private\n   */\n  function updateSwatch() {\n    if (self.oldIcon) {\n      domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon);\n    }\n\n    domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent());\n    self.oldIcon = self.palette.getCurrent();\n  }\n\n  this.palette.on('change', updateSwatch);\n\n  return this.palette;\n}\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/KeySelect.js",
    "content": "import Select from './Select.js';\n\n/**\n * Create a {Select} element whose composition is dynamically updated with\n * the telemetry fields of a particular domain object\n * @constructor\n * @param {Object} config The current state of this select. Must have object\n *                        and key fields\n * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which\n *                                    this KeySelect should listen to for change\n *                                    events\n * @param {ConditionManager} manager A ConditionManager instance from which\n *                                   to receive telemetry metadata\n * @param {function} changeCallback A change event callback to register with this\n *                                  select on initialization\n */\nconst NULLVALUE = '- Select Field -';\n\nexport default function KeySelect(config, objectSelect, manager, changeCallback) {\n  const self = this;\n\n  this.config = config;\n  this.objectSelect = objectSelect;\n  this.manager = manager;\n\n  this.select = new Select();\n  this.select.hide();\n  this.select.addOption('', NULLVALUE);\n  if (changeCallback) {\n    this.select.on('change', changeCallback);\n  }\n\n  /**\n   * Change event handler for the {ObjectSelect} to which this KeySelect instance\n   * is linked. Loads the new object's metadata and updates its select element's\n   * composition.\n   * @param {Object} key The key identifying the newly selected domain object\n   * @private\n   */\n  function onObjectChange(key) {\n    const selected = self.manager.metadataLoadCompleted()\n      ? self.select.getSelected()\n      : self.config.key;\n    self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {};\n    self.generateOptions();\n    self.select.setSelected(selected);\n  }\n\n  /**\n   * Event handler for the initial metadata load event from the associated\n   * ConditionManager. Retrieves metadata from the manager and populates\n   * the select element.\n   * @private\n   */\n  function onMetadataLoad() {\n    if (self.manager.getTelemetryMetadata(self.config.object)) {\n      self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object);\n      self.generateOptions();\n    }\n\n    self.select.setSelected(self.config.key);\n  }\n\n  if (self.manager.metadataLoadCompleted()) {\n    onMetadataLoad();\n  }\n\n  this.objectSelect.on('change', onObjectChange, this);\n  this.manager.on('metadata', onMetadataLoad);\n\n  return this.select;\n}\n\n/**\n * Populate this select with options based on its current composition\n */\nKeySelect.prototype.generateOptions = function () {\n  const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) {\n    return [metaDatum[0], metaDatum[1].name];\n  });\n  items.splice(0, 0, ['', NULLVALUE]);\n  this.select.setOptions(items);\n\n  if (this.select.options.length < 2) {\n    this.select.hide();\n  } else if (this.select.options.length > 1) {\n    this.select.show();\n  }\n};\n\nKeySelect.prototype.destroy = function () {\n  this.objectSelect.destroy();\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/ObjectSelect.js",
    "content": "import { makeKeyString } from 'objectUtils';\n\nimport Select from './Select.js';\n\n/**\n * Create a {Select} element whose composition is dynamically updated with\n * the current composition of the Summary Widget\n * @constructor\n * @param {Object} config The current state of this select. Must have an\n *                        object field\n * @param {ConditionManager} manager A ConditionManager instance from which\n *                                   to receive the current composition status\n * @param {string[][]} baseOptions A set of [value, label] keyword pairs to\n *                                 display regardless of the composition state\n */\nexport default function ObjectSelect(config, manager, baseOptions) {\n  const self = this;\n\n  this.config = config;\n  this.manager = manager;\n\n  this.select = new Select();\n  this.baseOptions = [['', '- Select Telemetry -']];\n  if (baseOptions) {\n    this.baseOptions = this.baseOptions.concat(baseOptions);\n  }\n\n  this.baseOptions.forEach(function (option) {\n    self.select.addOption(option[0], option[1]);\n  });\n\n  this.compositionObjs = this.manager.getComposition();\n  self.generateOptions();\n\n  /**\n   * Add a new composition object to this select when a composition added\n   * is detected on the Summary Widget\n   * @param {Object} obj The newly added domain object\n   * @private\n   */\n  function onCompositionAdd(obj) {\n    self.select.addOption(makeKeyString(obj.identifier), obj.name);\n  }\n\n  /**\n   * Refresh the composition of this select when a domain object is removed\n   * from the Summary Widget's composition\n   * @private\n   */\n  function onCompositionRemove() {\n    const selected = self.select.getSelected();\n    self.generateOptions();\n    self.select.setSelected(selected);\n  }\n\n  /**\n   * Defer setting the selected state on initial load until load is complete\n   * @private\n   */\n  function onCompositionLoad() {\n    self.select.setSelected(self.config.object);\n  }\n\n  this.manager.on('add', onCompositionAdd);\n  this.manager.on('remove', onCompositionRemove);\n  this.manager.on('load', onCompositionLoad);\n\n  if (this.manager.loadCompleted()) {\n    onCompositionLoad();\n  }\n\n  return this.select;\n}\n\n/**\n * Populate this select with options based on its current composition\n */\nObjectSelect.prototype.generateOptions = function () {\n  const items = Object.values(this.compositionObjs).map(function (obj) {\n    return [makeKeyString(obj.identifier), obj.name];\n  });\n  this.baseOptions.forEach(function (option, index) {\n    items.splice(index, 0, option);\n  });\n  this.select.setOptions(items);\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/OperationSelect.js",
    "content": "import eventHelpers from '../eventHelpers.js';\nimport Select from './Select.js';\n\n/**\n * Create a {Select} element whose composition is dynamically updated with\n * the operations applying to a particular telemetry property\n * @constructor\n * @param {Object} config The current state of this select. Must have object,\n *                        key, and operation fields\n * @param {KeySelect} keySelect The linked Key Select instance to which\n *                              this OperationSelect should listen to for change\n *                              events\n * @param {ConditionManager} manager A ConditionManager instance from which\n *                                   to receive telemetry metadata\n * @param {function} changeCallback A change event callback to register with this\n *                                  select on initialization\n */\nconst NULLVALUE = '- Select Comparison -';\n\nexport default function OperationSelect(config, keySelect, manager, changeCallback) {\n  eventHelpers.extend(this);\n  const self = this;\n\n  this.config = config;\n  this.keySelect = keySelect;\n  this.manager = manager;\n\n  this.operationKeys = [];\n  this.evaluator = this.manager.getEvaluator();\n  this.loadComplete = false;\n\n  this.select = new Select();\n  this.select.hide();\n  this.select.addOption('', NULLVALUE);\n  if (changeCallback) {\n    this.listenTo(this.select, 'change', changeCallback);\n  }\n\n  /**\n   * Change event handler for the {KeySelect} to which this OperationSelect instance\n   * is linked. Loads the operations applicable to the given telemetry property and updates\n   * its select element's composition\n   * @param {Object} key The key identifying the newly selected property\n   * @private\n   */\n  function onKeyChange(key) {\n    const selected = self.config.operation;\n    if (self.manager.metadataLoadCompleted()) {\n      self.loadOptions(key);\n      self.generateOptions();\n      self.select.setSelected(selected);\n    }\n  }\n\n  /**\n   * Event handler for the initial metadata load event from the associated\n   * ConditionManager. Retrieves telemetry property types and updates the\n   * select\n   * @private\n   */\n  function onMetadataLoad() {\n    if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) {\n      self.loadOptions(self.config.key);\n      self.generateOptions();\n    }\n\n    self.select.setSelected(self.config.operation);\n  }\n\n  this.keySelect.on('change', onKeyChange);\n  this.manager.on('metadata', onMetadataLoad);\n\n  if (this.manager.metadataLoadCompleted()) {\n    onMetadataLoad();\n  }\n\n  return this.select;\n}\n\n/**\n * Populate this select with options based on its current composition\n */\nOperationSelect.prototype.generateOptions = function () {\n  const self = this;\n  const items = this.operationKeys.map(function (operation) {\n    return [operation, self.evaluator.getOperationText(operation)];\n  });\n  items.splice(0, 0, ['', NULLVALUE]);\n  this.select.setOptions(items);\n\n  if (this.select.options.length < 2) {\n    this.select.hide();\n  } else {\n    this.select.show();\n  }\n};\n\n/**\n * Retrieve the data type associated with a given telemetry property and\n * the applicable operations from the {ConditionEvaluator}\n * @param {string} key The telemetry property to load operations for\n */\nOperationSelect.prototype.loadOptions = function (key) {\n  const self = this;\n  const operations = self.evaluator.getOperationKeys();\n  let type;\n\n  type = self.manager.getTelemetryPropertyType(self.config.object, key);\n\n  if (type !== undefined) {\n    self.operationKeys = operations.filter(function (operation) {\n      return self.evaluator.operationAppliesTo(operation, type);\n    });\n  }\n};\n\nOperationSelect.prototype.destroy = function () {\n  this.stopListening();\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/Palette.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nimport * as templateHelpers from '../../../../utils/template/templateHelpers.js';\nimport paletteTemplate from '../../res/input/paletteTemplate.html';\nimport eventHelpers from '../eventHelpers.js';\n\n/**\n * Instantiates a new Open MCT Color Palette input\n * @constructor\n * @param {string} cssClass The class name of the icon which should be applied\n *                          to this palette\n * @param {Element} container The view that contains this palette\n * @param {string[]} items A list of data items that will be associated with each\n *                         palette item in the view; how this data is represented is\n *                         up to the descendent class\n */\nexport default function Palette(cssClass, container, items) {\n  eventHelpers.extend(this);\n\n  const self = this;\n\n  this.cssClass = cssClass;\n  this.items = items;\n  this.container = container;\n\n  this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0];\n\n  this.itemElements = {\n    nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item')\n  };\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['change'];\n  this.value = this.items[0];\n  this.nullOption = ' ';\n  this.button = this.domElement.querySelector('.js-button');\n  this.menu = this.domElement.querySelector('.c-menu');\n\n  this.hideMenu = this.hideMenu.bind(this);\n\n  if (this.cssClass) {\n    self.button.classList.add(this.cssClass);\n  }\n\n  self.setNullOption(this.nullOption);\n\n  self.items.forEach(function (item) {\n    const itemElement = document.createElement('div');\n    itemElement.className = 'c-palette__item ' + item;\n    itemElement.setAttribute('data-item', item);\n\n    self.itemElements[item] = itemElement;\n    self.domElement.querySelector('.c-palette__items').appendChild(itemElement);\n  });\n\n  self.domElement.querySelector('.c-menu').style.display = 'none';\n\n  this.listenTo(window.document, 'click', this.hideMenu);\n  this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) {\n    event.stopPropagation();\n    self.container.querySelector('.c-menu').style.display = 'none';\n    self.domElement.querySelector('.c-menu').style.display = '';\n  });\n\n  /**\n   * Event handler for selection of an individual palette item. Sets the\n   * currently selected element to be the one associated with that item's data\n   * @param {Event} event the click event that initiated this callback\n   * @private\n   */\n  function handleItemClick(event) {\n    const elem = event.currentTarget;\n    const item = elem.dataset.item;\n    self.set(item);\n    self.domElement.querySelector('.c-menu').style.display = 'none';\n  }\n\n  self.domElement.querySelectorAll('.c-palette__item').forEach((item) => {\n    this.listenTo(item, 'click', handleItemClick);\n  });\n}\n\n/**\n * Get the DOM element representing this palette in the view\n */\nPalette.prototype.getDOM = function () {\n  return this.domElement;\n};\n\n/**\n * Clean up any event listeners registered to DOM elements external to the widget\n */\nPalette.prototype.destroy = function () {\n  this.stopListening();\n};\n\nPalette.prototype.hideMenu = function () {\n  this.domElement.querySelector('.c-menu').style.display = 'none';\n};\n\n/**\n * Register a callback with this palette: supported callback is change\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nPalette.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  } else {\n    throw new Error('Unsupported event type: ' + event);\n  }\n};\n\n/**\n * Get the currently selected value of this palette\n * @return {string} The selected value\n */\nPalette.prototype.getCurrent = function () {\n  return this.value;\n};\n\n/**\n * Set the selected value of this palette; if the item doesn't exist in the\n * palette's data model, the selected value will not change. Invokes any\n * change callbacks associated with this palette.\n * @param {string} item The key of the item to set as selected\n */\nPalette.prototype.set = function (item) {\n  const self = this;\n  if (this.items.includes(item) || item === this.nullOption) {\n    this.value = item;\n    if (item === this.nullOption) {\n      this.updateSelected('nullOption');\n    } else {\n      this.updateSelected(item);\n    }\n  }\n\n  this.eventEmitter.emit('change', self.value);\n};\n\n/**\n * Update the view associated with the currently selected item\n */\nPalette.prototype.updateSelected = function (item) {\n  this.domElement.querySelectorAll('.c-palette__item').forEach((paletteItem) => {\n    if (paletteItem.classList.contains('is-selected')) {\n      paletteItem.classList.remove('is-selected');\n    }\n  });\n  this.itemElements[item].classList.add('is-selected');\n  if (item === 'nullOption') {\n    this.domElement.querySelector('.t-swatch').classList.add('no-selection');\n  } else {\n    this.domElement.querySelector('.t-swatch').classList.remove('no-selection');\n  }\n};\n\n/**\n * set the property to be used for the 'no selection' item. If not set, this\n * defaults to a single space\n * @param {string} item The key to use as the 'no selection' item\n */\nPalette.prototype.setNullOption = function (item) {\n  this.nullOption = item;\n  this.itemElements.nullOption.data = { item: item };\n};\n\n/**\n * Hides the 'no selection' option to be hidden in the view if it doesn't apply\n */\nPalette.prototype.toggleNullOption = function () {\n  const elem = this.domElement.querySelector('.c-palette__item-none');\n\n  if (elem.style.display === 'none') {\n    this.domElement.querySelector('.c-palette__item-none').style.display = 'flex';\n  } else {\n    this.domElement.querySelector('.c-palette__item-none').style.display = 'none';\n  }\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/input/Select.js",
    "content": "import { EventEmitter } from 'eventemitter3';\n\nimport * as templateHelpers from '../../../../utils/template/templateHelpers.js';\nimport selectTemplate from '../../res/input/selectTemplate.html';\nimport eventHelpers from '../eventHelpers.js';\n\n/**\n * Wraps an HTML select element, and provides methods for dynamically altering\n * its composition from the data model\n * @constructor\n */\nexport default function Select() {\n  eventHelpers.extend(this);\n\n  const self = this;\n\n  this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0];\n\n  this.options = [];\n  this.eventEmitter = new EventEmitter();\n  this.supportedCallbacks = ['change'];\n\n  this.populate();\n\n  /**\n   * Event handler for the wrapped select element. Also invokes any change\n   * callbacks registered with this select with the new value\n   * @param {Event} event The change event that triggered this callback\n   * @private\n   */\n  function onChange(event) {\n    const elem = event.target;\n    const value = self.options[elem.selectedIndex];\n\n    self.eventEmitter.emit('change', value[0]);\n  }\n\n  this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this);\n}\n\n/**\n * Get the DOM element representing this Select in the view\n * @return {Element}\n */\nSelect.prototype.getDOM = function () {\n  return this.domElement;\n};\n\n/**\n * Register a callback with this select: supported callback is change\n * @param {string} event The key for the event to listen to\n * @param {function} callback The function that this rule will invoke on this event\n * @param {Object} context A reference to a scope to use as the context for\n *                         context for the callback function\n */\nSelect.prototype.on = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.on(event, callback, context || this);\n  } else {\n    throw new Error('Unsupported event type' + event);\n  }\n};\n\n/**\n * Unregister a callback from this select.\n * @param {string} event The key for the event to stop listening to\n * @param {function} callback The function to unregister\n * @param {Object} context A reference to a scope to use as the context for the callback function\n */\nSelect.prototype.off = function (event, callback, context) {\n  if (this.supportedCallbacks.includes(event)) {\n    this.eventEmitter.off(event, callback, context || this);\n  } else {\n    throw new Error('Unsupported event type: ' + event);\n  }\n};\n\n/**\n * Update the select element in the view from the current state of the data\n * model\n */\nSelect.prototype.populate = function () {\n  const self = this;\n  let selectedIndex = 0;\n\n  selectedIndex = this.domElement.querySelector('select').selectedIndex;\n\n  this.domElement.querySelector('select').innerHTML = '';\n\n  self.options.forEach(function (option) {\n    const optionElement = document.createElement('option');\n    optionElement.value = option[0];\n    optionElement.innerText = `+ ${option[1]}`;\n\n    self.domElement.querySelector('select').appendChild(optionElement);\n  });\n\n  this.domElement.querySelector('select').selectedIndex = selectedIndex;\n};\n\n/**\n * Add a single option to this select\n * @param {string} value The value for the new option\n * @param {string} label The human-readable text for the new option\n */\nSelect.prototype.addOption = function (value, label) {\n  this.options.push([value, label]);\n  this.populate();\n};\n\n/**\n * Set the available options for this select. Replaces any existing options\n * @param {string[][]} options An array of [value, label] pairs to display\n */\nSelect.prototype.setOptions = function (options) {\n  this.options = options;\n  this.populate();\n};\n\n/**\n * Sets the currently selected element an invokes any registered change\n * callbacks with the new value. If the value doesn't exist in this select's\n * model, its state will not change.\n * @param {string} value The value to set as the selected option\n */\nSelect.prototype.setSelected = function (value) {\n  let selectedIndex = 0;\n  let selectedOption;\n\n  this.options.forEach(function (option, index) {\n    if (option[0] === value) {\n      selectedIndex = index;\n    }\n  });\n  this.domElement.querySelector('select').selectedIndex = selectedIndex;\n\n  selectedOption = this.options[selectedIndex];\n  this.eventEmitter.emit('change', selectedOption[0]);\n};\n\n/**\n * Get the value of the currently selected item\n * @return {string}\n */\nSelect.prototype.getSelected = function () {\n  return this.domElement.querySelector('select').value;\n};\n\nSelect.prototype.hide = function () {\n  this.domElement.classList.add('hidden');\n  if (this.domElement.querySelector('.equal-to')) {\n    this.domElement.querySelector('.equal-to').classList.add('hidden');\n  }\n};\n\nSelect.prototype.show = function () {\n  this.domElement.classList.remove('hidden');\n  if (this.domElement.querySelector('.equal-to')) {\n    this.domElement.querySelector('.equal-to').classList.remove('hidden');\n  }\n};\n\nSelect.prototype.destroy = function () {\n  this.stopListening();\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { makeKeyString } from 'objectUtils';\n\nimport SummaryWidgetEvaluator from './SummaryWidgetEvaluator.js';\n\nexport default function EvaluatorPool(openmct) {\n  this.openmct = openmct;\n  this.byObjectId = {};\n  this.byEvaluator = new WeakMap();\n}\n\nEvaluatorPool.prototype.get = function (domainObject) {\n  const objectId = makeKeyString(domainObject.identifier);\n  let poolEntry = this.byObjectId[objectId];\n  if (!poolEntry) {\n    poolEntry = {\n      leases: 0,\n      objectId: objectId,\n      evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct)\n    };\n    this.byEvaluator.set(poolEntry.evaluator, poolEntry);\n    this.byObjectId[objectId] = poolEntry;\n  }\n\n  poolEntry.leases += 1;\n\n  return poolEntry.evaluator;\n};\n\nEvaluatorPool.prototype.release = function (evaluator) {\n  const poolEntry = this.byEvaluator.get(evaluator);\n  poolEntry.leases -= 1;\n  if (poolEntry.leases === 0) {\n    evaluator.destroy();\n    this.byEvaluator.delete(evaluator);\n    delete this.byObjectId[poolEntry.objectId];\n  }\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport EvaluatorPool from './EvaluatorPool.js';\n\ndescribe('EvaluatorPool', function () {\n  let pool;\n  let openmct;\n  let objectA;\n  let objectB;\n\n  beforeEach(function () {\n    openmct = {\n      composition: jasmine.createSpyObj('compositionAPI', ['get']),\n      objects: jasmine.createSpyObj('objectAPI', ['observe'])\n    };\n    openmct.composition.get.and.callFake(function () {\n      const compositionCollection = jasmine.createSpyObj('compositionCollection', [\n        'load',\n        'on',\n        'off'\n      ]);\n      compositionCollection.load.and.returnValue(Promise.resolve());\n\n      return compositionCollection;\n    });\n    openmct.objects.observe.and.callFake(function () {\n      return function () {};\n    });\n    pool = new EvaluatorPool(openmct);\n    objectA = {\n      identifier: {\n        namespace: 'someNamespace',\n        key: 'someKey'\n      },\n      configuration: {\n        ruleOrder: []\n      }\n    };\n    objectB = {\n      identifier: {\n        namespace: 'otherNamespace',\n        key: 'otherKey'\n      },\n      configuration: {\n        ruleOrder: []\n      }\n    };\n  });\n\n  it('returns new evaluators for different objects', function () {\n    const evaluatorA = pool.get(objectA);\n    const evaluatorB = pool.get(objectB);\n    expect(evaluatorA).not.toBe(evaluatorB);\n  });\n\n  it('returns the same evaluator for the same object', function () {\n    const evaluatorA = pool.get(objectA);\n    const evaluatorB = pool.get(objectA);\n    expect(evaluatorA).toBe(evaluatorB);\n\n    const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA)));\n    expect(evaluatorA).toBe(evaluatorC);\n  });\n\n  it('returns new evaluator when old is released', function () {\n    const evaluatorA = pool.get(objectA);\n    const evaluatorB = pool.get(objectA);\n    expect(evaluatorA).toBe(evaluatorB);\n    pool.release(evaluatorA);\n    pool.release(evaluatorB);\n    const evaluatorC = pool.get(objectA);\n    expect(evaluatorA).not.toBe(evaluatorC);\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport OPERATIONS from './operations.js';\n\nexport default function SummaryWidgetCondition(definition) {\n  this.object = definition.object;\n  this.key = definition.key;\n  this.values = definition.values;\n  if (!definition.operation) {\n    // TODO: better handling for default rule.\n    this.evaluate = function () {\n      return true;\n    };\n  } else {\n    this.comparator = OPERATIONS[definition.operation].operation;\n  }\n}\n\nSummaryWidgetCondition.prototype.evaluate = function (telemetryState) {\n  const stateKeys = Object.keys(telemetryState);\n  let state;\n  let result;\n  let i;\n\n  if (this.object === 'any') {\n    for (i = 0; i < stateKeys.length; i++) {\n      state = telemetryState[stateKeys[i]];\n      result = this.evaluateState(state);\n      if (result) {\n        return true;\n      }\n    }\n\n    return false;\n  } else if (this.object === 'all') {\n    for (i = 0; i < stateKeys.length; i++) {\n      state = telemetryState[stateKeys[i]];\n      result = this.evaluateState(state);\n      if (!result) {\n        return false;\n      }\n    }\n\n    return true;\n  } else {\n    return this.evaluateState(telemetryState[this.object]);\n  }\n};\n\nSummaryWidgetCondition.prototype.evaluateState = function (state) {\n  const testValues = [state.formats[this.key].parse(state.lastDatum)].concat(this.values);\n\n  return this.comparator(testValues);\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetCondition from './SummaryWidgetCondition.js';\n\ndescribe('SummaryWidgetCondition', function () {\n  let condition;\n  let telemetryState;\n\n  beforeEach(function () {\n    // Format map intentionally uses different keys than those present\n    // in datum, which serves to verify conditions use format map to get\n    // data.\n    const formatMap = {\n      adjusted: {\n        parse: function (datum) {\n          return datum.value + 10;\n        }\n      },\n      raw: {\n        parse: function (datum) {\n          return datum.value;\n        }\n      }\n    };\n\n    telemetryState = {\n      objectId: {\n        formats: formatMap,\n        lastDatum: {}\n      },\n      otherObjectId: {\n        formats: formatMap,\n        lastDatum: {}\n      }\n    };\n  });\n\n  it('can evaluate if a single object matches', function () {\n    condition = new SummaryWidgetCondition({\n      object: 'objectId',\n      key: 'raw',\n      operation: 'greaterThan',\n      values: [10]\n    });\n    telemetryState.objectId.lastDatum.value = 5;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('can evaluate if a single object matches (alternate keys)', function () {\n    condition = new SummaryWidgetCondition({\n      object: 'objectId',\n      key: 'adjusted',\n      operation: 'greaterThan',\n      values: [10]\n    });\n    telemetryState.objectId.lastDatum.value = -5;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 5;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('can evaluate \"if all objects match\"', function () {\n    condition = new SummaryWidgetCondition({\n      object: 'all',\n      key: 'raw',\n      operation: 'greaterThan',\n      values: [10]\n    });\n    telemetryState.objectId.lastDatum.value = 0;\n    telemetryState.otherObjectId.lastDatum.value = 0;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 0;\n    telemetryState.otherObjectId.lastDatum.value = 15;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 0;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 15;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('can evaluate \"if any object matches\"', function () {\n    condition = new SummaryWidgetCondition({\n      object: 'any',\n      key: 'raw',\n      operation: 'greaterThan',\n      values: [10]\n    });\n    telemetryState.objectId.lastDatum.value = 0;\n    telemetryState.otherObjectId.lastDatum.value = 0;\n    expect(condition.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 0;\n    telemetryState.otherObjectId.lastDatum.value = 15;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 0;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 15;\n    expect(condition.evaluate(telemetryState)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport _ from 'lodash';\nimport { makeKeyString } from 'objectUtils';\n\nimport eventHelpers from '../eventHelpers.js';\nimport SummaryWidgetRule from './SummaryWidgetRule.js';\n\n/**\n * evaluates rules defined in a summary widget against either lad or\n * realtime state.\n *\n */\nexport default function SummaryWidgetEvaluator(domainObject, openmct) {\n  this.openmct = openmct;\n  this.baseState = {};\n\n  this.updateRules(domainObject);\n  this.removeObserver = openmct.objects.observe(domainObject, '*', this.updateRules.bind(this));\n\n  const composition = openmct.composition.get(domainObject);\n\n  this.listenTo(composition, 'add', this.addChild, this);\n  this.listenTo(composition, 'remove', this.removeChild, this);\n\n  this.loadPromise = composition.load();\n}\n\neventHelpers.extend(SummaryWidgetEvaluator.prototype);\n\n/**\n * Subscribes to realtime telemetry for the given summary widget.\n */\nSummaryWidgetEvaluator.prototype.subscribe = function (callback) {\n  let active = true;\n  let unsubscribes = [];\n\n  this.getBaseStateClone().then(\n    function (realtimeStates) {\n      if (!active) {\n        return;\n      }\n\n      const updateCallback = function () {\n        const datum = this.evaluateState(realtimeStates, this.openmct.time.getTimeSystem().key);\n        if (datum) {\n          callback(datum);\n        }\n      }.bind(this);\n\n      /* eslint-disable you-dont-need-lodash-underscore/map */\n      unsubscribes = _.map(realtimeStates, this.subscribeToObjectState.bind(this, updateCallback));\n      /* eslint-enable you-dont-need-lodash-underscore/map */\n    }.bind(this)\n  );\n\n  return function () {\n    active = false;\n    unsubscribes.forEach(function (unsubscribe) {\n      unsubscribe();\n    });\n  };\n};\n\n/**\n * Returns a promise for a telemetry datum obtained by evaluating the\n * current lad data.\n */\nSummaryWidgetEvaluator.prototype.requestLatest = function (options) {\n  return this.getBaseStateClone()\n    .then(\n      function (ladState) {\n        const promises = Object.values(ladState).map(\n          this.updateObjectStateFromLAD.bind(this, options)\n        );\n\n        return Promise.all(promises).then(function () {\n          return ladState;\n        });\n      }.bind(this)\n    )\n    .then(\n      function (ladStates) {\n        return this.evaluateState(ladStates, options.domain);\n      }.bind(this)\n    );\n};\n\nSummaryWidgetEvaluator.prototype.updateRules = function (domainObject) {\n  this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) {\n    return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]);\n  });\n};\n\nSummaryWidgetEvaluator.prototype.addChild = function (childObject) {\n  const childId = makeKeyString(childObject.identifier);\n  const metadata = this.openmct.telemetry.getMetadata(childObject);\n  const formats = this.openmct.telemetry.getFormatMap(metadata);\n\n  this.baseState[childId] = {\n    id: childId,\n    domainObject: childObject,\n    metadata: metadata,\n    formats: formats\n  };\n};\n\nSummaryWidgetEvaluator.prototype.removeChild = function (childObject) {\n  const childId = makeKeyString(childObject.identifier);\n  delete this.baseState[childId];\n};\n\nSummaryWidgetEvaluator.prototype.load = function () {\n  return this.loadPromise;\n};\n\n/**\n * Return a promise for a 2-deep clone of the base state object: object\n * states are shallow cloned, and then assembled and returned as a new base\n * state.  Allows object states to be mutated while sharing telemetry\n * metadata and formats.\n */\nSummaryWidgetEvaluator.prototype.getBaseStateClone = function () {\n  return this.load().then(\n    function () {\n      /* eslint-disable you-dont-need-lodash-underscore/values */\n      return _(this.baseState).values().map(_.clone).keyBy('id').value();\n      /* eslint-enable you-dont-need-lodash-underscore/values */\n    }.bind(this)\n  );\n};\n\n/**\n * Subscribes to realtime updates for a given objectState, and invokes\n * the supplied callback when objectState has been updated.  Returns\n * a function to unsubscribe.\n * @private.\n */\nSummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) {\n  return this.openmct.telemetry.subscribe(\n    objectState.domainObject,\n    function (datum) {\n      objectState.lastDatum = datum;\n      objectState.timestamps = this.getTimestamps(objectState.id, datum);\n      callback();\n    }.bind(this)\n  );\n};\n\n/**\n * Given an object state, will return a promise that is resolved when the\n * object state has been updated from the LAD.\n * @private.\n */\nSummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) {\n  options = Object.assign({}, options, {\n    strategy: 'latest',\n    size: 1\n  });\n\n  return this.openmct.telemetry.request(objectState.domainObject, options).then(\n    function (results) {\n      objectState.lastDatum = results[results.length - 1];\n      objectState.timestamps = this.getTimestamps(objectState.id, objectState.lastDatum);\n    }.bind(this)\n  );\n};\n\n/**\n * Returns an object containing all domain values in a datum.\n * @private.\n */\nSummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) {\n  const timestampedDatum = {};\n  this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) {\n    timestampedDatum[timeSystem.key] = this.baseState[childId].formats[timeSystem.key].parse(datum);\n  }, this);\n\n  return timestampedDatum;\n};\n\n/**\n * Given a base datum(containing timestamps) and rule index, adds values\n * from the matching rule.\n * @private\n */\nSummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) {\n  const rule = this.rules[ruleIndex];\n\n  baseDatum.ruleLabel = rule.label;\n  baseDatum.ruleName = rule.name;\n  baseDatum.message = rule.message;\n  baseDatum.ruleIndex = ruleIndex;\n  baseDatum.backgroundColor = rule.style['background-color'];\n  baseDatum.textColor = rule.style.color;\n  baseDatum.borderColor = rule.style['border-color'];\n  baseDatum.icon = rule.icon;\n\n  return baseDatum;\n};\n\n/**\n * Evaluate a `state` object and return a summary widget telemetry datum.\n * Datum timestamps will be taken from the \"latest\" datum in the `state`\n * where \"latest\" is the datum with the largest value for the given\n * `timestampKey`.\n * @private.\n */\nSummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) {\n  const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) {\n    return itDoes && state[k].lastDatum;\n  }, true);\n  if (!hasRequiredData) {\n    return;\n  }\n\n  let i;\n  for (i = this.rules.length - 1; i > 0; i--) {\n    if (this.rules[i].evaluate(state, false)) {\n      break;\n    }\n  }\n\n  /* eslint-disable you-dont-need-lodash-underscore/map */\n  let latestTimestamp = _(state).map('timestamps').sortBy(timestampKey).last();\n  /* eslint-enable you-dont-need-lodash-underscore/map */\n\n  if (!latestTimestamp) {\n    latestTimestamp = {};\n  }\n\n  const baseDatum = _.clone(latestTimestamp);\n\n  return this.makeDatumFromRule(i, baseDatum);\n};\n\n/**\n * remove all listeners and clean up any resources.\n */\nSummaryWidgetEvaluator.prototype.destroy = function () {\n  this.stopListening();\n  this.removeObserver();\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function SummaryWidgetMetadataProvider(openmct) {\n  this.openmct = openmct;\n}\n\nSummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) {\n  return domainObject.type === 'summary-widget';\n};\n\nSummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) {\n  return this.openmct.time.getAllTimeSystems().map(function (ts, i) {\n    return {\n      key: ts.key,\n      name: ts.name,\n      format: ts.timeFormat,\n      hints: {\n        domain: i\n      }\n    };\n  });\n};\n\nSummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) {\n  const ruleOrder = domainObject.configuration.ruleOrder || [];\n  const enumerations = ruleOrder\n    .filter(function (ruleId) {\n      return Boolean(domainObject.configuration.ruleConfigById[ruleId]);\n    })\n    .map(function (ruleId, ruleIndex) {\n      return {\n        string: domainObject.configuration.ruleConfigById[ruleId].label,\n        value: ruleIndex\n      };\n    });\n\n  const metadata = {\n    // Generally safe assumption is that we have one domain per timeSystem.\n    values: this.getDomains().concat([\n      {\n        name: 'State',\n        key: 'state',\n        source: 'ruleIndex',\n        format: 'enum',\n        enumerations: enumerations,\n        hints: {\n          range: 1\n        }\n      },\n      {\n        name: 'Rule Label',\n        key: 'ruleLabel',\n        format: 'string'\n      },\n      {\n        name: 'Rule Name',\n        key: 'ruleName',\n        format: 'string'\n      },\n      {\n        name: 'Message',\n        key: 'message',\n        format: 'string'\n      },\n      {\n        name: 'Background Color',\n        key: 'backgroundColor',\n        format: 'string'\n      },\n      {\n        name: 'Text Color',\n        key: 'textColor',\n        format: 'string'\n      },\n      {\n        name: 'Border Color',\n        key: 'borderColor',\n        format: 'string'\n      },\n      {\n        name: 'Display Icon',\n        key: 'icon',\n        format: 'string'\n      }\n    ])\n  };\n\n  return metadata;\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetCondition from './SummaryWidgetCondition.js';\n\nexport default function SummaryWidgetRule(definition) {\n  this.name = definition.name;\n  this.label = definition.label;\n  this.id = definition.id;\n  this.icon = definition.icon;\n  this.style = definition.style;\n  this.message = definition.message;\n  this.description = definition.description;\n  this.conditions = definition.conditions.map(function (cDefinition) {\n    return new SummaryWidgetCondition(cDefinition);\n  });\n  this.trigger = definition.trigger;\n}\n\n/**\n * Evaluate the given rule against a telemetryState and return true if it\n * matches.\n */\nSummaryWidgetRule.prototype.evaluate = function (telemetryState) {\n  let i;\n  let result;\n\n  if (this.trigger === 'all') {\n    for (i = 0; i < this.conditions.length; i++) {\n      result = this.conditions[i].evaluate(telemetryState);\n      if (!result) {\n        return false;\n      }\n    }\n\n    return true;\n  } else if (this.trigger === 'any') {\n    for (i = 0; i < this.conditions.length; i++) {\n      result = this.conditions[i].evaluate(telemetryState);\n      if (result) {\n        return true;\n      }\n    }\n\n    return false;\n  } else {\n    throw new Error('Invalid rule trigger: ' + this.trigger);\n  }\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetRule from './SummaryWidgetRule.js';\n\ndescribe('SummaryWidgetRule', function () {\n  let rule;\n  let telemetryState;\n\n  beforeEach(function () {\n    const formatMap = {\n      raw: {\n        parse: function (datum) {\n          return datum.value;\n        }\n      }\n    };\n\n    telemetryState = {\n      objectId: {\n        formats: formatMap,\n        lastDatum: {}\n      },\n      otherObjectId: {\n        formats: formatMap,\n        lastDatum: {}\n      }\n    };\n  });\n\n  it('allows single condition rules with any', function () {\n    rule = new SummaryWidgetRule({\n      trigger: 'any',\n      conditions: [\n        {\n          object: 'objectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [10]\n        }\n      ]\n    });\n\n    telemetryState.objectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('allows single condition rules with all', function () {\n    rule = new SummaryWidgetRule({\n      trigger: 'all',\n      conditions: [\n        {\n          object: 'objectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [10]\n        }\n      ]\n    });\n\n    telemetryState.objectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('can combine multiple conditions with all', function () {\n    rule = new SummaryWidgetRule({\n      trigger: 'all',\n      conditions: [\n        {\n          object: 'objectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [10]\n        },\n        {\n          object: 'otherObjectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [20]\n        }\n      ]\n    });\n\n    telemetryState.objectId.lastDatum.value = 5;\n    telemetryState.otherObjectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 5;\n    telemetryState.otherObjectId.lastDatum.value = 25;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 25;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n  });\n\n  it('can combine multiple conditions with any', function () {\n    rule = new SummaryWidgetRule({\n      trigger: 'any',\n      conditions: [\n        {\n          object: 'objectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [10]\n        },\n        {\n          object: 'otherObjectId',\n          key: 'raw',\n          operation: 'greaterThan',\n          values: [20]\n        }\n      ]\n    });\n\n    telemetryState.objectId.lastDatum.value = 5;\n    telemetryState.otherObjectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(false);\n    telemetryState.objectId.lastDatum.value = 5;\n    telemetryState.otherObjectId.lastDatum.value = 25;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 5;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n    telemetryState.objectId.lastDatum.value = 15;\n    telemetryState.otherObjectId.lastDatum.value = 25;\n    expect(rule.evaluate(telemetryState)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport EvaluatorPool from './EvaluatorPool.js';\n\nexport default function SummaryWidgetTelemetryProvider(openmct) {\n  this.pool = new EvaluatorPool(openmct);\n}\n\nSummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) {\n  return domainObject.type === 'summary-widget';\n};\n\nSummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) {\n  if (options.strategy !== 'latest' && options.size !== 1) {\n    return Promise.resolve([]);\n  }\n\n  const evaluator = this.pool.get(domainObject);\n\n  return evaluator.requestLatest(options).then(\n    function (latestDatum) {\n      this.pool.release(evaluator);\n\n      return latestDatum ? [latestDatum] : [];\n    }.bind(this)\n  );\n};\n\nSummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) {\n  return domainObject.type === 'summary-widget';\n};\n\nSummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) {\n  const evaluator = this.pool.get(domainObject);\n  const unsubscribe = evaluator.subscribe(callback);\n\n  return function () {\n    this.pool.release(evaluator);\n    unsubscribe();\n  }.bind(this);\n};\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetTelemetryProvider from './SummaryWidgetTelemetryProvider.js';\n\nxdescribe('SummaryWidgetTelemetryProvider', function () {\n  let telemObjectA;\n  let telemObjectB;\n  let summaryWidgetObject;\n  let openmct;\n  let telemUnsubscribes;\n  let unobserver;\n  let composition;\n  let telemetryProvider;\n  let loader;\n\n  beforeEach(function () {\n    telemObjectA = {\n      identifier: {\n        namespace: 'a',\n        key: 'telem'\n      }\n    };\n    telemObjectB = {\n      identifier: {\n        namespace: 'b',\n        key: 'telem'\n      }\n    };\n    summaryWidgetObject = {\n      name: 'Summary Widget',\n      type: 'summary-widget',\n      identifier: {\n        namespace: 'base',\n        key: 'widgetId'\n      },\n      composition: ['a:telem', 'b:telem'],\n      configuration: {\n        ruleOrder: ['default', 'rule0', 'rule1'],\n        ruleConfigById: {\n          default: {\n            name: 'safe',\n            label: \"Don't Worry\",\n            message: \"It's Ok\",\n            id: 'default',\n            icon: 'a-ok',\n            style: {\n              color: '#ffffff',\n              'background-color': '#38761d',\n              'border-color': 'rgba(0,0,0,0)'\n            },\n            conditions: [\n              {\n                object: '',\n                key: '',\n                operation: '',\n                values: []\n              }\n            ],\n            trigger: 'any'\n          },\n          rule0: {\n            name: 'A High',\n            label: 'Start Worrying',\n            message: 'A is a little high...',\n            id: 'rule0',\n            icon: 'a-high',\n            style: {\n              color: '#000000',\n              'background-color': '#ffff00',\n              'border-color': 'rgba(1,1,0,0)'\n            },\n            conditions: [\n              {\n                object: 'a:telem',\n                key: 'measurement',\n                operation: 'greaterThan',\n                values: [50]\n              }\n            ],\n            trigger: 'any'\n          },\n          rule1: {\n            name: 'B Low',\n            label: 'WORRY!',\n            message: 'B is Low',\n            id: 'rule1',\n            icon: 'b-low',\n            style: {\n              color: '#ff00ff',\n              'background-color': '#ff0000',\n              'border-color': 'rgba(1,0,0,0)'\n            },\n            conditions: [\n              {\n                object: 'b:telem',\n                key: 'measurement',\n                operation: 'lessThan',\n                values: [10]\n              }\n            ],\n            trigger: 'any'\n          }\n        }\n      }\n    };\n    openmct = {\n      objects: jasmine.createSpyObj('objectAPI', ['get', 'observe']),\n      telemetry: jasmine.createSpyObj('telemetryAPI', [\n        'getMetadata',\n        'getFormatMap',\n        'request',\n        'subscribe',\n        'addProvider'\n      ]),\n      composition: jasmine.createSpyObj('compositionAPI', ['get']),\n      time: jasmine.createSpyObj('timeAPI', ['getAllTimeSystems', 'timeSystem'])\n    };\n\n    openmct.time.getAllTimeSystems.and.returnValue([{ key: 'timestamp' }]);\n    openmct.time.timeSystem.and.returnValue({ key: 'timestamp' });\n\n    unobserver = jasmine.createSpy('unobserver');\n    openmct.objects.observe.and.returnValue(unobserver);\n\n    composition = jasmine.createSpyObj('compositionCollection', ['on', 'off', 'load']);\n\n    function notify(eventName, a, b) {\n      composition.on.calls\n        .all()\n        .filter(function (c) {\n          return c.args[0] === eventName;\n        })\n        .forEach(function (c) {\n          if (c.args[2]) {\n            // listener w/ context.\n            c.args[1].call(c.args[2], a, b);\n          } else {\n            // listener w/o context.\n            c.args[1](a, b);\n          }\n        });\n    }\n\n    loader = {};\n    loader.promise = new Promise(function (resolve, reject) {\n      loader.resolve = resolve;\n      loader.reject = reject;\n    });\n\n    composition.load.and.callFake(function () {\n      setTimeout(function () {\n        notify('add', telemObjectA);\n        setTimeout(function () {\n          notify('add', telemObjectB);\n          setTimeout(function () {\n            loader.resolve();\n          });\n        });\n      });\n\n      return loader.promise;\n    });\n    openmct.composition.get.and.returnValue(composition);\n\n    telemUnsubscribes = [];\n    openmct.telemetry.subscribe.and.callFake(function () {\n      const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length);\n      telemUnsubscribes.push(unsubscriber);\n\n      return unsubscriber;\n    });\n\n    openmct.telemetry.getMetadata.and.callFake(function (object) {\n      return {\n        name: 'fake metadata manager',\n        object: object,\n        keys: ['timestamp', 'measurement']\n      };\n    });\n\n    openmct.telemetry.getFormatMap.and.callFake(function (metadata) {\n      expect(metadata.name).toBe('fake metadata manager');\n\n      return {\n        metadata: metadata,\n        timestamp: {\n          parse: function (datum) {\n            return datum.t;\n          }\n        },\n        measurement: {\n          parse: function (datum) {\n            return datum.m;\n          }\n        }\n      };\n    });\n    telemetryProvider = new SummaryWidgetTelemetryProvider(openmct);\n  });\n\n  it('supports subscription for summary widgets', function () {\n    expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)).toBe(true);\n  });\n\n  it('supports requests for summary widgets', function () {\n    expect(telemetryProvider.supportsRequest(summaryWidgetObject)).toBe(true);\n  });\n\n  it('does not support other requests or subscriptions', function () {\n    expect(telemetryProvider.supportsSubscribe(telemObjectA)).toBe(false);\n    expect(telemetryProvider.supportsRequest(telemObjectA)).toBe(false);\n  });\n\n  it('Returns no results for basic requests', function () {\n    return telemetryProvider.request(summaryWidgetObject, {}).then(function (result) {\n      expect(result).toEqual([]);\n    });\n  });\n\n  it('provides realtime telemetry', function () {\n    const callback = jasmine.createSpy('callback');\n    telemetryProvider.subscribe(summaryWidgetObject, callback);\n\n    return loader.promise\n      .then(function () {\n        return new Promise(function (resolve) {\n          setTimeout(resolve);\n        });\n      })\n      .then(function () {\n        expect(openmct.telemetry.subscribe.calls.count()).toBe(2);\n        expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(\n          telemObjectA,\n          jasmine.any(Function)\n        );\n        expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(\n          telemObjectB,\n          jasmine.any(Function)\n        );\n\n        const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1];\n        const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1];\n\n        aCallback({\n          t: 123,\n          m: 25\n        });\n        expect(callback).not.toHaveBeenCalled();\n        bCallback({\n          t: 123,\n          m: 25\n        });\n        expect(callback).toHaveBeenCalledWith({\n          timestamp: 123,\n          ruleLabel: \"Don't Worry\",\n          ruleName: 'safe',\n          message: \"It's Ok\",\n          ruleIndex: 0,\n          backgroundColor: '#38761d',\n          textColor: '#ffffff',\n          borderColor: 'rgba(0,0,0,0)',\n          icon: 'a-ok'\n        });\n\n        aCallback({\n          t: 140,\n          m: 55\n        });\n        expect(callback).toHaveBeenCalledWith({\n          timestamp: 140,\n          ruleLabel: 'Start Worrying',\n          ruleName: 'A High',\n          message: 'A is a little high...',\n          ruleIndex: 1,\n          backgroundColor: '#ffff00',\n          textColor: '#000000',\n          borderColor: 'rgba(1,1,0,0)',\n          icon: 'a-high'\n        });\n\n        bCallback({\n          t: 140,\n          m: -10\n        });\n        expect(callback).toHaveBeenCalledWith({\n          timestamp: 140,\n          ruleLabel: 'WORRY!',\n          ruleName: 'B Low',\n          message: 'B is Low',\n          ruleIndex: 2,\n          backgroundColor: '#ff0000',\n          textColor: '#ff00ff',\n          borderColor: 'rgba(1,0,0,0)',\n          icon: 'b-low'\n        });\n\n        aCallback({\n          t: 160,\n          m: 25\n        });\n        expect(callback).toHaveBeenCalledWith({\n          timestamp: 160,\n          ruleLabel: 'WORRY!',\n          ruleName: 'B Low',\n          message: 'B is Low',\n          ruleIndex: 2,\n          backgroundColor: '#ff0000',\n          textColor: '#ff00ff',\n          borderColor: 'rgba(1,0,0,0)',\n          icon: 'b-low'\n        });\n\n        bCallback({\n          t: 160,\n          m: 25\n        });\n        expect(callback).toHaveBeenCalledWith({\n          timestamp: 160,\n          ruleLabel: \"Don't Worry\",\n          ruleName: 'safe',\n          message: \"It's Ok\",\n          ruleIndex: 0,\n          backgroundColor: '#38761d',\n          textColor: '#ffffff',\n          borderColor: 'rgba(0,0,0,0)',\n          icon: 'a-ok'\n        });\n      });\n  });\n\n  describe('providing lad telemetry', function () {\n    let responseDatums;\n    let resultsShouldBe;\n\n    beforeEach(function () {\n      openmct.telemetry.request.and.callFake(function (rObj, options) {\n        expect(rObj).toEqual(jasmine.any(Object));\n        expect(options).toEqual({\n          size: 1,\n          strategy: 'latest',\n          domain: 'timestamp'\n        });\n        expect(responseDatums[rObj.identifier.namespace]).toBeDefined();\n\n        return Promise.resolve([responseDatums[rObj.identifier.namespace]]);\n      });\n      responseDatums = {};\n\n      resultsShouldBe = function (results) {\n        return telemetryProvider\n          .request(summaryWidgetObject, {\n            size: 1,\n            strategy: 'latest',\n            domain: 'timestamp'\n          })\n          .then(function (r) {\n            expect(r).toEqual(results);\n          });\n      };\n    });\n\n    it('returns default when no rule matches', function () {\n      responseDatums = {\n        a: {\n          t: 122,\n          m: 25\n        },\n        b: {\n          t: 111,\n          m: 25\n        }\n      };\n\n      return resultsShouldBe([\n        {\n          timestamp: 122,\n          ruleLabel: \"Don't Worry\",\n          ruleName: 'safe',\n          message: \"It's Ok\",\n          ruleIndex: 0,\n          backgroundColor: '#38761d',\n          textColor: '#ffffff',\n          borderColor: 'rgba(0,0,0,0)',\n          icon: 'a-ok'\n        }\n      ]);\n    });\n\n    it('returns highest priority when multiple match', function () {\n      responseDatums = {\n        a: {\n          t: 131,\n          m: 55\n        },\n        b: {\n          t: 139,\n          m: 5\n        }\n      };\n\n      return resultsShouldBe([\n        {\n          timestamp: 139,\n          ruleLabel: 'WORRY!',\n          ruleName: 'B Low',\n          message: 'B is Low',\n          ruleIndex: 2,\n          backgroundColor: '#ff0000',\n          textColor: '#ff00ff',\n          borderColor: 'rgba(1,0,0,0)',\n          icon: 'b-low'\n        }\n      ]);\n    });\n\n    it('returns matching rule', function () {\n      responseDatums = {\n        a: {\n          t: 144,\n          m: 55\n        },\n        b: {\n          t: 141,\n          m: 15\n        }\n      };\n\n      return resultsShouldBe([\n        {\n          timestamp: 144,\n          ruleLabel: 'Start Worrying',\n          ruleName: 'A High',\n          message: 'A is a little high...',\n          ruleIndex: 1,\n          backgroundColor: '#ffff00',\n          textColor: '#000000',\n          borderColor: 'rgba(1,1,0,0)',\n          icon: 'a-high'\n        }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/telemetry/operations.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst OPERATIONS = {\n  equalTo: {\n    operation: function (input) {\n      return input[0] === input[1];\n    },\n    text: 'is equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' == ' + values[0];\n    }\n  },\n  notEqualTo: {\n    operation: function (input) {\n      return input[0] !== input[1];\n    },\n    text: 'is not equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' != ' + values[0];\n    }\n  },\n  greaterThan: {\n    operation: function (input) {\n      return input[0] > input[1];\n    },\n    text: 'is greater than',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' > ' + values[0];\n    }\n  },\n  lessThan: {\n    operation: function (input) {\n      return input[0] < input[1];\n    },\n    text: 'is less than',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' < ' + values[0];\n    }\n  },\n  greaterThanOrEq: {\n    operation: function (input) {\n      return input[0] >= input[1];\n    },\n    text: 'is greater than or equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' >= ' + values[0];\n    }\n  },\n  lessThanOrEq: {\n    operation: function (input) {\n      return input[0] <= input[1];\n    },\n    text: 'is less than or equal to',\n    appliesTo: ['number'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' <= ' + values[0];\n    }\n  },\n  between: {\n    operation: function (input) {\n      return input[0] > input[1] && input[0] < input[2];\n    },\n    text: 'is between',\n    appliesTo: ['number'],\n    inputCount: 2,\n    getDescription: function (values) {\n      return ' between ' + values[0] + ' and ' + values[1];\n    }\n  },\n  notBetween: {\n    operation: function (input) {\n      return input[0] < input[1] || input[0] > input[2];\n    },\n    text: 'is not between',\n    appliesTo: ['number'],\n    inputCount: 2,\n    getDescription: function (values) {\n      return ' not between ' + values[0] + ' and ' + values[1];\n    }\n  },\n  textContains: {\n    operation: function (input) {\n      return input[0] && input[1] && input[0].includes(input[1]);\n    },\n    text: 'text contains',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' contains ' + values[0];\n    }\n  },\n  textDoesNotContain: {\n    operation: function (input) {\n      return input[0] && input[1] && !input[0].includes(input[1]);\n    },\n    text: 'text does not contain',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' does not contain ' + values[0];\n    }\n  },\n  textStartsWith: {\n    operation: function (input) {\n      return input[0].startsWith(input[1]);\n    },\n    text: 'text starts with',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' starts with ' + values[0];\n    }\n  },\n  textEndsWith: {\n    operation: function (input) {\n      return input[0].endsWith(input[1]);\n    },\n    text: 'text ends with',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' ends with ' + values[0];\n    }\n  },\n  textIsExactly: {\n    operation: function (input) {\n      return input[0] === input[1];\n    },\n    text: 'text is exactly',\n    appliesTo: ['string'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' is exactly ' + values[0];\n    }\n  },\n  isUndefined: {\n    operation: function (input) {\n      return typeof input[0] === 'undefined';\n    },\n    text: 'is undefined',\n    appliesTo: ['string', 'number', 'enum'],\n    inputCount: 0,\n    getDescription: function () {\n      return ' is undefined';\n    }\n  },\n  isDefined: {\n    operation: function (input) {\n      return typeof input[0] !== 'undefined';\n    },\n    text: 'is defined',\n    appliesTo: ['string', 'number', 'enum'],\n    inputCount: 0,\n    getDescription: function () {\n      return ' is defined';\n    }\n  },\n  enumValueIs: {\n    operation: function (input) {\n      return input[0] === input[1];\n    },\n    text: 'is',\n    appliesTo: ['enum'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' == ' + values[0];\n    }\n  },\n  enumValueIsNot: {\n    operation: function (input) {\n      return input[0] !== input[1];\n    },\n    text: 'is not',\n    appliesTo: ['enum'],\n    inputCount: 1,\n    getDescription: function (values) {\n      return ' != ' + values[0];\n    }\n  }\n};\n\nexport default OPERATIONS;\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/views/SummaryWidgetView.js",
    "content": "import * as urlSanitizeLib from '@braintree/sanitize-url';\n\nconst WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';\n\nclass SummaryWidgetView {\n  #createSummaryWidgetTemplate() {\n    const anchor = document.createElement('a');\n    anchor.classList.add(\n      't-summary-widget',\n      'c-summary-widget',\n      'js-sw',\n      'u-links',\n      'u-fills-container'\n    );\n\n    const widgetIcon = document.createElement('div');\n    widgetIcon.id = 'widgetIcon';\n    widgetIcon.classList.add('c-sw__icon', 'js-sw__icon');\n    anchor.appendChild(widgetIcon);\n\n    const widgetLabel = document.createElement('div');\n    widgetLabel.id = 'widgetLabel';\n    widgetLabel.classList.add('c-sw__label', 'js-sw__label');\n    widgetLabel.textContent = 'Loading...';\n    anchor.appendChild(widgetLabel);\n\n    return anchor;\n  }\n\n  constructor(domainObject, openmct) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.hasUpdated = false;\n    this.render = this.render.bind(this);\n  }\n\n  updateState(datum) {\n    this.hasUpdated = true;\n    this.widget.style.color = datum.textColor;\n    this.widget.style.backgroundColor = datum.backgroundColor;\n    this.widget.style.borderColor = datum.borderColor;\n    this.widget.title = datum.message;\n    this.label.title = datum.message;\n    this.label.textContent = datum.ruleLabel;\n    this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon;\n  }\n\n  render() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n\n    this.hasUpdated = false;\n\n    const anchor = this.#createSummaryWidgetTemplate();\n    this.container.appendChild(anchor);\n\n    this.widget = this.container.querySelector('a');\n    this.icon = this.container.querySelector('#widgetIcon');\n    this.label = this.container.querySelector('.js-sw__label');\n\n    let url = this.domainObject.url;\n    if (url) {\n      this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url));\n    } else {\n      this.widget.removeAttribute('href');\n    }\n\n    if (this.domainObject.openNewTab === 'newTab') {\n      this.widget.setAttribute('target', '_blank');\n    } else {\n      this.widget.removeAttribute('target');\n    }\n\n    const renderTracker = {};\n    this.renderTracker = renderTracker;\n\n    this.openmct.telemetry\n      .request(this.domainObject, {\n        strategy: 'latest',\n        size: 1\n      })\n      .then((results) => {\n        if (\n          this.destroyed ||\n          this.hasUpdated ||\n          this.renderTracker !== renderTracker ||\n          results.length === 0\n        ) {\n          return;\n        }\n\n        this.updateState(results[results.length - 1]);\n      });\n\n    this.unsubscribe = this.openmct.telemetry.subscribe(\n      this.domainObject,\n      this.updateState.bind(this)\n    );\n  }\n\n  show(container) {\n    this.container = container;\n    this.render();\n    this.removeMutationListener = this.openmct.objects.observe(\n      this.domainObject,\n      '*',\n      this.onMutation.bind(this)\n    );\n    this.openmct.time.on('timeSystem', this.render);\n  }\n\n  onMutation(domainObject) {\n    this.domainObject = domainObject;\n    this.render();\n  }\n\n  destroy() {\n    this.unsubscribe();\n    this.removeMutationListener();\n    this.openmct.time.off('timeSystem', this.render);\n    this.destroyed = true;\n    delete this.widget;\n    delete this.label;\n    delete this.openmct;\n    delete this.domainObject;\n  }\n}\n\nexport default SummaryWidgetView;\n"
  },
  {
    "path": "src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetEditView from '../SummaryWidget.js';\nimport SummaryWidgetView from './SummaryWidgetView.js';\n\nconst DEFAULT_VIEW_PRIORITY = 100;\nexport default function SummaryWidgetViewProvider(openmct) {\n  return {\n    key: 'summary-widget-viewer',\n    name: 'Summary View',\n    cssClass: 'icon-summary-widget',\n    canView: function (domainObject) {\n      return domainObject.type === 'summary-widget';\n    },\n    canEdit: function (domainObject) {\n      return domainObject.type === 'summary-widget';\n    },\n    view: function (domainObject) {\n      return new SummaryWidgetView(domainObject, openmct);\n    },\n    edit: function (domainObject) {\n      return new SummaryWidgetEditView(domainObject, openmct);\n    },\n    priority: function (domainObject) {\n      if (domainObject.type === 'summary-widget') {\n        return Number.MAX_VALUE;\n      } else {\n        return DEFAULT_VIEW_PRIORITY;\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js",
    "content": "import ConditionEvaluator from '../src/ConditionEvaluator.js';\n\ndescribe('A Summary Widget Rule Evaluator', function () {\n  let evaluator;\n  let testEvaluator;\n  let testOperation;\n  let mockCache;\n  let mockTestCache;\n  let mockComposition;\n  let mockConditions;\n  let mockConditionsEmpty;\n  let mockConditionsUndefined;\n  let mockConditionsAnyTrue;\n  let mockConditionsAllTrue;\n  let mockConditionsAnyFalse;\n  let mockConditionsAllFalse;\n  let mockOperations;\n\n  beforeEach(function () {\n    mockCache = {\n      a: {\n        alpha: 3,\n        beta: 9,\n        gamma: 'Testing 1 2 3'\n      },\n      b: {\n        alpha: 44,\n        beta: 23,\n        gamma: 'Hello World'\n      },\n      c: {\n        foo: 'bar',\n        iAm: 'The Walrus',\n        creature: {\n          type: 'Centaur'\n        }\n      }\n    };\n    mockTestCache = {\n      a: {\n        alpha: 1,\n        beta: 1,\n        gamma: 'Testing 4 5 6'\n      },\n      b: {\n        alpha: 2,\n        beta: 2,\n        gamma: 'Goodbye world'\n      }\n    };\n    mockComposition = {\n      a: {},\n      b: {},\n      c: {}\n    };\n    mockConditions = [\n      {\n        object: 'a',\n        key: 'alpha',\n        operation: 'greaterThan',\n        values: [2]\n      },\n      {\n        object: 'b',\n        key: 'gamma',\n        operation: 'lessThan',\n        values: [5]\n      }\n    ];\n    mockConditionsEmpty = [\n      {\n        object: '',\n        key: '',\n        operation: '',\n        values: []\n      }\n    ];\n    mockConditionsUndefined = [\n      {\n        object: 'No Such Object',\n        key: '',\n        operation: '',\n        values: []\n      },\n      {\n        object: 'a',\n        key: 'No Such Key',\n        operation: '',\n        values: []\n      },\n      {\n        object: 'a',\n        key: 'alpha',\n        operation: 'No Such Operation',\n        values: []\n      },\n      {\n        object: 'all',\n        key: 'Nonexistent Field',\n        operation: 'Random Operation',\n        values: []\n      },\n      {\n        object: 'any',\n        key: 'Nonexistent Field',\n        operation: 'Whatever Operation',\n        values: []\n      }\n    ];\n    mockConditionsAnyTrue = [\n      {\n        object: 'any',\n        key: 'alpha',\n        operation: 'greaterThan',\n        values: [5]\n      }\n    ];\n    mockConditionsAnyFalse = [\n      {\n        object: 'any',\n        key: 'alpha',\n        operation: 'greaterThan',\n        values: [1000]\n      }\n    ];\n    mockConditionsAllFalse = [\n      {\n        object: 'all',\n        key: 'alpha',\n        operation: 'greaterThan',\n        values: [5]\n      }\n    ];\n    mockConditionsAllTrue = [\n      {\n        object: 'all',\n        key: 'alpha',\n        operation: 'greaterThan',\n        values: [0]\n      }\n    ];\n    mockOperations = {\n      greaterThan: {\n        operation: function (input) {\n          return input[0] > input[1];\n        },\n        text: 'is greater than',\n        appliesTo: ['number'],\n        inputCount: 1,\n        getDescription: function (values) {\n          return ' > ' + values[0];\n        }\n      },\n      lessThan: {\n        operation: function (input) {\n          return input[0] < input[1];\n        },\n        text: 'is less than',\n        appliesTo: ['number'],\n        inputCount: 1\n      },\n      textContains: {\n        operation: function (input) {\n          return input[0] && input[1] && input[0].includes(input[1]);\n        },\n        text: 'text contains',\n        appliesTo: ['string'],\n        inputCount: 1\n      },\n      textIsExactly: {\n        operation: function (input) {\n          return input[0] === input[1];\n        },\n        text: 'text is exactly',\n        appliesTo: ['string'],\n        inputCount: 1\n      },\n      isHalfHorse: {\n        operation: function (input) {\n          return input[0].type === 'Centaur';\n        },\n        text: 'is Half Horse',\n        appliesTo: ['mythicalCreature'],\n        inputCount: 0,\n        getDescription: function () {\n          return 'is half horse';\n        }\n      }\n    };\n    evaluator = new ConditionEvaluator(mockCache, mockComposition);\n    testEvaluator = new ConditionEvaluator(mockCache, mockComposition);\n    evaluator.operations = mockOperations;\n  });\n\n  it('evaluates a condition when it has no configuration', function () {\n    expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false);\n    expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false);\n  });\n\n  it('correctly evaluates a set of conditions', function () {\n    expect(evaluator.execute(mockConditions, 'any')).toEqual(true);\n    expect(evaluator.execute(mockConditions, 'all')).toEqual(false);\n  });\n\n  it('correctly evaluates conditions involving \"any telemetry\"', function () {\n    expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true);\n    expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false);\n  });\n\n  it('correctly evaluates conditions involving \"all telemetry\"', function () {\n    expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true);\n    expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false);\n  });\n\n  it('handles malformed conditions gracefully', function () {\n    //if no conditions are fully defined, should return false for any mode\n    expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false);\n    expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false);\n    //these conditions are true: evaluator should ignore undefined conditions,\n    //and evaluate the rule as true\n    mockConditionsUndefined.push({\n      object: 'a',\n      key: 'gamma',\n      operation: 'textContains',\n      values: ['Testing']\n    });\n    expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true);\n    mockConditionsUndefined.push({\n      object: 'c',\n      key: 'iAm',\n      operation: 'textContains',\n      values: ['Walrus']\n    });\n    expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true);\n  });\n\n  it('gets the keys for possible operations', function () {\n    expect(evaluator.getOperationKeys()).toEqual([\n      'greaterThan',\n      'lessThan',\n      'textContains',\n      'textIsExactly',\n      'isHalfHorse'\n    ]);\n  });\n\n  it('gets output text for a given operation', function () {\n    expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse');\n  });\n\n  it('correctly returns whether an operation applies to a given type', function () {\n    expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true);\n    expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false);\n  });\n\n  it('returns the HTML input type associated with a given data type', function () {\n    expect(evaluator.getInputTypeById('string')).toEqual('text');\n  });\n\n  it('gets the number of inputs required for a given operation', function () {\n    expect(evaluator.getInputCount('isHalfHorse')).toEqual(0);\n    expect(evaluator.getInputCount('greaterThan')).toEqual(1);\n  });\n\n  it('gets a human-readable description of a condition', function () {\n    expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse');\n    expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1');\n  });\n\n  it('allows setting a substitute cache for testing purposes, and toggling its use', function () {\n    evaluator.setTestDataCache(mockTestCache);\n    evaluator.useTestData(true);\n    expect(evaluator.execute(mockConditions, 'any')).toEqual(false);\n    expect(evaluator.execute(mockConditions, 'all')).toEqual(false);\n    mockConditions.push({\n      object: 'a',\n      key: 'gamma',\n      operation: 'textContains',\n      values: ['4 5 6']\n    });\n    expect(evaluator.execute(mockConditions, 'any')).toEqual(true);\n    expect(evaluator.execute(mockConditions, 'all')).toEqual(false);\n    mockConditions.pop();\n    evaluator.useTestData(false);\n    expect(evaluator.execute(mockConditions, 'any')).toEqual(true);\n    expect(evaluator.execute(mockConditions, 'all')).toEqual(false);\n  });\n\n  it('supports all required operations', function () {\n    //equal to\n    testOperation = testEvaluator.operations.equalTo.operation;\n    expect(testOperation([33, 33])).toEqual(true);\n    expect(testOperation([55, 147])).toEqual(false);\n    //not equal to\n    testOperation = testEvaluator.operations.notEqualTo.operation;\n    expect(testOperation([33, 33])).toEqual(false);\n    expect(testOperation([55, 147])).toEqual(true);\n    //greater than\n    testOperation = testEvaluator.operations.greaterThan.operation;\n    expect(testOperation([100, 33])).toEqual(true);\n    expect(testOperation([33, 33])).toEqual(false);\n    expect(testOperation([55, 147])).toEqual(false);\n    //less than\n    testOperation = testEvaluator.operations.lessThan.operation;\n    expect(testOperation([100, 33])).toEqual(false);\n    expect(testOperation([33, 33])).toEqual(false);\n    expect(testOperation([55, 147])).toEqual(true);\n    //greater than or equal to\n    testOperation = testEvaluator.operations.greaterThanOrEq.operation;\n    expect(testOperation([100, 33])).toEqual(true);\n    expect(testOperation([33, 33])).toEqual(true);\n    expect(testOperation([55, 147])).toEqual(false);\n    //less than or equal to\n    testOperation = testEvaluator.operations.lessThanOrEq.operation;\n    expect(testOperation([100, 33])).toEqual(false);\n    expect(testOperation([33, 33])).toEqual(true);\n    expect(testOperation([55, 147])).toEqual(true);\n    //between\n    testOperation = testEvaluator.operations.between.operation;\n    expect(testOperation([100, 33, 66])).toEqual(false);\n    expect(testOperation([1, 33, 66])).toEqual(false);\n    expect(testOperation([45, 33, 66])).toEqual(true);\n    //not between\n    testOperation = testEvaluator.operations.notBetween.operation;\n    expect(testOperation([100, 33, 66])).toEqual(true);\n    expect(testOperation([1, 33, 66])).toEqual(true);\n    expect(testOperation([45, 33, 66])).toEqual(false);\n    //text contains\n    testOperation = testEvaluator.operations.textContains.operation;\n    expect(testOperation(['Testing', 'tin'])).toEqual(true);\n    expect(testOperation(['Testing', 'bind'])).toEqual(false);\n    //text does not contain\n    testOperation = testEvaluator.operations.textDoesNotContain.operation;\n    expect(testOperation(['Testing', 'tin'])).toEqual(false);\n    expect(testOperation(['Testing', 'bind'])).toEqual(true);\n    //text starts with\n    testOperation = testEvaluator.operations.textStartsWith.operation;\n    expect(testOperation(['Testing', 'Tes'])).toEqual(true);\n    expect(testOperation(['Testing', 'ting'])).toEqual(false);\n    //text ends with\n    testOperation = testEvaluator.operations.textEndsWith.operation;\n    expect(testOperation(['Testing', 'Tes'])).toEqual(false);\n    expect(testOperation(['Testing', 'ting'])).toEqual(true);\n    //text is exactly\n    testOperation = testEvaluator.operations.textIsExactly.operation;\n    expect(testOperation(['Testing', 'Testing'])).toEqual(true);\n    expect(testOperation(['Testing', 'Test'])).toEqual(false);\n    //undefined\n    testOperation = testEvaluator.operations.isUndefined.operation;\n    expect(testOperation([1])).toEqual(false);\n    expect(testOperation([])).toEqual(true);\n    //isDefined\n    testOperation = testEvaluator.operations.isDefined.operation;\n    expect(testOperation([1])).toEqual(true);\n    expect(testOperation([])).toEqual(false);\n  });\n\n  it('can produce a description for all supported operations', function () {\n    testEvaluator.getOperationKeys().forEach(function (key) {\n      expect(testEvaluator.getOperationDescription(key, [])).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/ConditionManagerSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ConditionManager from '../src/ConditionManager.js';\n\ndescribe('A Summary Widget Condition Manager', function () {\n  let conditionManager;\n  let mockDomainObject;\n  let mockCompObject1;\n  let mockCompObject2;\n  let mockCompObject3;\n  let mockMetadata;\n  let mockTelemetryCallbacks;\n  let mockEventCallbacks;\n  let unsubscribeSpies;\n  let unregisterSpies;\n  let mockMetadataManagers;\n  let mockComposition;\n  let mockOpenMCT;\n  let mockTelemetryAPI;\n  let addCallbackSpy;\n  let loadCallbackSpy;\n  let removeCallbackSpy;\n  let telemetryCallbackSpy;\n  let metadataCallbackSpy;\n  let telemetryRequests;\n  let mockTelemetryValues;\n  let mockTelemetryValues2;\n  let mockConditionEvaluator;\n\n  beforeEach(function () {\n    mockDomainObject = {\n      identifier: {\n        key: 'testKey'\n      },\n      name: 'Test Object',\n      composition: [\n        {\n          mockCompObject1: {\n            key: 'mockCompObject1'\n          },\n          mockCompObject2: {\n            key: 'mockCompObject2'\n          }\n        }\n      ],\n      configuration: {}\n    };\n    mockCompObject1 = {\n      identifier: {\n        key: 'mockCompObject1'\n      },\n      name: 'Object 1'\n    };\n    mockCompObject2 = {\n      identifier: {\n        key: 'mockCompObject2'\n      },\n      name: 'Object 2'\n    };\n    mockCompObject3 = {\n      identifier: {\n        key: 'mockCompObject3'\n      },\n      name: 'Object 3'\n    };\n    mockMetadata = {\n      mockCompObject1: {\n        property1: {\n          key: 'property1',\n          name: 'Property 1',\n          format: 'string',\n          hints: {}\n        },\n        property2: {\n          key: 'property2',\n          name: 'Property 2',\n          hints: {\n            domain: 1\n          }\n        }\n      },\n      mockCompObject2: {\n        property3: {\n          key: 'property3',\n          name: 'Property 3',\n          format: 'string',\n          hints: {}\n        },\n        property4: {\n          key: 'property4',\n          name: 'Property 4',\n          hints: {\n            range: 1\n          }\n        }\n      },\n      mockCompObject3: {\n        property1: {\n          key: 'property1',\n          name: 'Property 1',\n          hints: {}\n        },\n        property2: {\n          key: 'property2',\n          name: 'Property 2',\n          hints: {}\n        }\n      }\n    };\n    mockTelemetryCallbacks = {};\n    mockEventCallbacks = {};\n    unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [\n      'mockCompObject1',\n      'mockCompObject2',\n      'mockCompObject3'\n    ]);\n    unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', ['load', 'remove', 'add']);\n    mockTelemetryValues = {\n      mockCompObject1: {\n        property1: 'Its a string',\n        property2: 42\n      },\n      mockCompObject2: {\n        property3: 'Execute order:',\n        property4: 66\n      },\n      mockCompObject3: {\n        property1: 'Testing 1 2 3',\n        property2: 9000\n      }\n    };\n    mockTelemetryValues2 = {\n      mockCompObject1: {\n        property1: 'Its a different string',\n        property2: 44\n      },\n      mockCompObject2: {\n        property3: 'Execute catch:',\n        property4: 22\n      },\n      mockCompObject3: {\n        property1: 'Walrus',\n        property2: 22\n      }\n    };\n    mockMetadataManagers = {\n      mockCompObject1: {\n        values: jasmine\n          .createSpy('metadataManager')\n          .and.returnValue(Object.values(mockMetadata.mockCompObject1))\n      },\n      mockCompObject2: {\n        values: jasmine\n          .createSpy('metadataManager')\n          .and.returnValue(Object.values(mockMetadata.mockCompObject2))\n      },\n      mockCompObject3: {\n        values: jasmine\n          .createSpy('metadataManager')\n          .and.returnValue(Object.values(mockMetadata.mockCompObject2))\n      }\n    };\n\n    mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load', 'triggerCallback']);\n    mockComposition.on.and.callFake(function (event, callback, context) {\n      mockEventCallbacks[event] = callback.bind(context);\n    });\n    mockComposition.off.and.callFake(function (event) {\n      unregisterSpies[event]();\n    });\n    mockComposition.load.and.callFake(function () {\n      mockComposition.triggerCallback('add', mockCompObject1);\n      mockComposition.triggerCallback('add', mockCompObject2);\n      mockComposition.triggerCallback('load');\n    });\n    mockComposition.triggerCallback.and.callFake(function (event, obj) {\n      if (event === 'add') {\n        mockEventCallbacks.add(obj);\n      } else if (event === 'remove') {\n        mockEventCallbacks.remove(obj.identifier);\n      } else {\n        mockEventCallbacks[event]();\n      }\n    });\n    telemetryRequests = [];\n    mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [\n      'request',\n      'isTelemetryObject',\n      'getMetadata',\n      'subscribe',\n      'triggerTelemetryCallback'\n    ]);\n    mockTelemetryAPI.request.and.callFake(function (obj) {\n      const req = {\n        object: obj\n      };\n      req.promise = new Promise(function (resolve, reject) {\n        req.resolve = resolve;\n        req.reject = reject;\n      });\n      telemetryRequests.push(req);\n\n      return req.promise;\n    });\n    mockTelemetryAPI.isTelemetryObject.and.returnValue(true);\n    mockTelemetryAPI.getMetadata.and.callFake(function (obj) {\n      return mockMetadataManagers[obj.identifier.key];\n    });\n    mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) {\n      mockTelemetryCallbacks[obj.identifier.key] = callback;\n\n      return unsubscribeSpies[obj.identifier.key];\n    });\n    mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) {\n      mockTelemetryCallbacks[key](mockTelemetryValues2[key]);\n    });\n\n    mockOpenMCT = {\n      telemetry: mockTelemetryAPI,\n      composition: {}\n    };\n    mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition);\n\n    loadCallbackSpy = jasmine.createSpy('loadCallbackSpy');\n    addCallbackSpy = jasmine.createSpy('addCallbackSpy');\n    removeCallbackSpy = jasmine.createSpy('removeCallbackSpy');\n    metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy');\n    telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy');\n\n    conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT);\n    conditionManager.on('load', loadCallbackSpy);\n    conditionManager.on('add', addCallbackSpy);\n    conditionManager.on('remove', removeCallbackSpy);\n    conditionManager.on('metadata', metadataCallbackSpy);\n    conditionManager.on('receiveTelemetry', telemetryCallbackSpy);\n\n    mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator');\n    mockConditionEvaluator.execute = jasmine.createSpy('execute');\n    conditionManager.evaluator = mockConditionEvaluator;\n  });\n\n  it('loads the initial composition and invokes the appropriate handlers', function () {\n    mockComposition.triggerCallback('load');\n    expect(conditionManager.getComposition()).toEqual({\n      mockCompObject1: mockCompObject1,\n      mockCompObject2: mockCompObject2\n    });\n    expect(loadCallbackSpy).toHaveBeenCalled();\n    expect(conditionManager.loadCompleted()).toEqual(true);\n  });\n\n  it('loads metadata from composition and gets it upon request', function () {\n    expect(conditionManager.getTelemetryMetadata('mockCompObject1')).toEqual(\n      mockMetadata.mockCompObject1\n    );\n    expect(conditionManager.getTelemetryMetadata('mockCompObject2')).toEqual(\n      mockMetadata.mockCompObject2\n    );\n  });\n\n  it('maintains lists of global metadata, and does not duplicate repeated fields', function () {\n    const allKeys = {\n      property1: {\n        key: 'property1',\n        name: 'Property 1',\n        format: 'string',\n        hints: {}\n      },\n      property2: {\n        key: 'property2',\n        name: 'Property 2',\n        hints: {\n          domain: 1\n        }\n      },\n      property3: {\n        key: 'property3',\n        name: 'Property 3',\n        format: 'string',\n        hints: {}\n      },\n      property4: {\n        key: 'property4',\n        name: 'Property 4',\n        hints: {\n          range: 1\n        }\n      }\n    };\n    expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);\n    expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);\n    mockComposition.triggerCallback('add', mockCompObject3);\n    expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);\n    expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);\n  });\n\n  it('loads and gets telemetry property types', function () {\n    conditionManager.parseAllPropertyTypes();\n    expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')).toEqual(\n      'string'\n    );\n    expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')).toEqual(\n      'number'\n    );\n    expect(conditionManager.metadataLoadCompleted()).toEqual(true);\n    expect(metadataCallbackSpy).toHaveBeenCalled();\n  });\n\n  it('responds to a composition add event and invokes the appropriate handlers', function () {\n    mockComposition.triggerCallback('add', mockCompObject3);\n    expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3);\n    expect(conditionManager.getComposition()).toEqual({\n      mockCompObject1: mockCompObject1,\n      mockCompObject2: mockCompObject2,\n      mockCompObject3: mockCompObject3\n    });\n  });\n\n  it('responds to a composition remove event and invokes the appropriate handlers', function () {\n    mockComposition.triggerCallback('remove', mockCompObject2);\n    expect(removeCallbackSpy).toHaveBeenCalledWith({\n      key: 'mockCompObject2'\n    });\n    expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled();\n    expect(conditionManager.getComposition()).toEqual({\n      mockCompObject1: mockCompObject1\n    });\n  });\n\n  it('unregisters telemetry subscriptions and composition listeners on destroy', function () {\n    mockComposition.triggerCallback('add', mockCompObject3);\n    conditionManager.destroy();\n    Object.values(unsubscribeSpies).forEach(function (spy) {\n      expect(spy).toHaveBeenCalled();\n    });\n    Object.values(unregisterSpies).forEach(function (spy) {\n      expect(spy).toHaveBeenCalled();\n    });\n  });\n\n  xit('populates its LAD cache with historical data on load, if available', function (done) {\n    expect(telemetryRequests.length).toBe(2);\n    expect(telemetryRequests[0].object).toBe(mockCompObject1);\n    expect(telemetryRequests[1].object).toBe(mockCompObject2);\n\n    expect(telemetryCallbackSpy).not.toHaveBeenCalled();\n\n    telemetryCallbackSpy.and.callFake(function () {\n      if (telemetryCallbackSpy.calls.count() === 2) {\n        expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(\n          'Its a string'\n        );\n        expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66);\n        done();\n      }\n    });\n\n    telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]);\n    telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);\n  });\n\n  xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {\n    mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');\n    expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(\n      'Its a different string'\n    );\n    mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');\n    expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22);\n    expect(telemetryCallbackSpy).toHaveBeenCalled();\n  });\n\n  it(\n    'evaluates a set of rules and returns the id of the ' +\n      'last active rule, or the first if no rules are active',\n    function () {\n      const mockRuleOrder = ['default', 'rule0', 'rule1'];\n      const mockRules = {\n        default: {\n          getProperty: function () {}\n        },\n        rule0: {\n          getProperty: function () {}\n        },\n        rule1: {\n          getProperty: function () {}\n        }\n      };\n\n      mockConditionEvaluator.execute.and.returnValue(false);\n      expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default');\n      mockConditionEvaluator.execute.and.returnValue(true);\n      expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1');\n    }\n  );\n\n  it('gets the human-readable name of a composition object', function () {\n    expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1');\n    expect(conditionManager.getObjectName('all')).toEqual('all Telemetry');\n  });\n\n  it('gets the human-readable name of a telemetry field', function () {\n    expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')).toEqual(\n      'Property 1'\n    );\n    expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')).toEqual(\n      'Property 4'\n    );\n  });\n\n  it('gets its associated ConditionEvaluator', function () {\n    expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator);\n  });\n\n  it('allows forcing a receive telemetry event', function () {\n    conditionManager.triggerTelemetryCallback();\n    expect(telemetryCallbackSpy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/ConditionSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Condition from '../src/Condition.js';\n\ndescribe('A summary widget condition', function () {\n  let testCondition;\n  let mockConfig;\n  let mockConditionManager;\n  let mockContainer;\n  let mockEvaluator;\n  let changeSpy;\n  let duplicateSpy;\n  let removeSpy;\n  let generateValuesSpy;\n\n  beforeEach(function () {\n    mockContainer = document.createElement('div');\n\n    mockConfig = {\n      object: 'object1',\n      key: 'property1',\n      operation: 'operation1',\n      values: [1, 2, 3]\n    };\n\n    mockEvaluator = {};\n    mockEvaluator.getInputCount = jasmine.createSpy('inputCount');\n    mockEvaluator.getInputType = jasmine.createSpy('inputType');\n\n    mockConditionManager = jasmine.createSpyObj('mockConditionManager', [\n      'on',\n      'getComposition',\n      'loadCompleted',\n      'getEvaluator',\n      'getTelemetryMetadata',\n      'metadataLoadCompleted',\n      'getObjectName',\n      'getTelemetryPropertyName'\n    ]);\n    mockConditionManager.loadCompleted.and.returnValue(false);\n    mockConditionManager.metadataLoadCompleted.and.returnValue(false);\n    mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);\n    mockConditionManager.getComposition.and.returnValue({});\n    mockConditionManager.getTelemetryMetadata.and.returnValue({});\n    mockConditionManager.getObjectName.and.returnValue('Object Name');\n    mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');\n\n    duplicateSpy = jasmine.createSpy('duplicate');\n    removeSpy = jasmine.createSpy('remove');\n    changeSpy = jasmine.createSpy('change');\n    generateValuesSpy = jasmine.createSpy('generateValueInputs');\n\n    testCondition = new Condition(mockConfig, 54, mockConditionManager);\n\n    testCondition.on('duplicate', duplicateSpy);\n    testCondition.on('remove', removeSpy);\n    testCondition.on('change', changeSpy);\n  });\n\n  it('exposes a DOM element to represent itself in the view', function () {\n    mockContainer.append(testCondition.getDOM());\n    expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1);\n  });\n\n  it('responds to a change in its object select', function () {\n    testCondition.selects.object.setSelected('');\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: '',\n      property: 'object',\n      index: 54\n    });\n  });\n\n  it('responds to a change in its key select', function () {\n    testCondition.selects.key.setSelected('');\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: '',\n      property: 'key',\n      index: 54\n    });\n  });\n\n  it('responds to a change in its operation select', function () {\n    testCondition.generateValueInputs = generateValuesSpy;\n    testCondition.selects.operation.setSelected('');\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: '',\n      property: 'operation',\n      index: 54\n    });\n    expect(generateValuesSpy).toHaveBeenCalledWith('');\n  });\n\n  it('generates value inputs of the appropriate type and quantity', function () {\n    let inputs;\n\n    mockContainer.append(testCondition.getDOM());\n    mockEvaluator.getInputType.and.returnValue('number');\n    mockEvaluator.getInputCount.and.returnValue(3);\n    testCondition.generateValueInputs('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');\n\n    expect(numberInputs.length).toEqual(3);\n    expect(numberInputs[0].valueAsNumber).toEqual(1);\n    expect(numberInputs[1].valueAsNumber).toEqual(2);\n    expect(numberInputs[2].valueAsNumber).toEqual(3);\n\n    mockEvaluator.getInputType.and.returnValue('text');\n    mockEvaluator.getInputCount.and.returnValue(2);\n    testCondition.config.values = ['Text I Am', 'Text It Is'];\n    testCondition.generateValueInputs('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const textInputs = Array.from(inputs).filter((input) => input.type === 'text');\n\n    expect(textInputs.length).toEqual(2);\n    expect(textInputs[0].value).toEqual('Text I Am');\n    expect(textInputs[1].value).toEqual('Text It Is');\n  });\n\n  it('ensures reasonable defaults on values if none are provided', function () {\n    let inputs;\n\n    mockContainer.append(testCondition.getDOM());\n    mockEvaluator.getInputType.and.returnValue('number');\n    mockEvaluator.getInputCount.and.returnValue(3);\n    testCondition.config.values = [];\n    testCondition.generateValueInputs('');\n\n    inputs = Array.from(mockContainer.querySelectorAll('input'));\n\n    expect(inputs[0].valueAsNumber).toEqual(0);\n    expect(inputs[1].valueAsNumber).toEqual(0);\n    expect(inputs[2].valueAsNumber).toEqual(0);\n    expect(testCondition.config.values).toEqual([0, 0, 0]);\n\n    mockEvaluator.getInputType.and.returnValue('text');\n    mockEvaluator.getInputCount.and.returnValue(2);\n    testCondition.config.values = [];\n    testCondition.generateValueInputs('');\n\n    inputs = Array.from(mockContainer.querySelectorAll('input'));\n\n    expect(inputs[0].value).toEqual('');\n    expect(inputs[1].value).toEqual('');\n    expect(testCondition.config.values).toEqual(['', '']);\n  });\n\n  it('responds to a change in its value inputs', function () {\n    mockContainer.append(testCondition.getDOM());\n    mockEvaluator.getInputType.and.returnValue('number');\n    mockEvaluator.getInputCount.and.returnValue(3);\n    testCondition.generateValueInputs('');\n\n    const event = new Event('input', {\n      bubbles: true,\n      cancelable: true\n    });\n    const inputs = mockContainer.querySelectorAll('input');\n\n    inputs[1].value = 9001;\n    inputs[1].dispatchEvent(event);\n\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: 9001,\n      property: 'values[1]',\n      index: 54\n    });\n  });\n\n  it('can remove itself from the configuration', function () {\n    testCondition.remove();\n    expect(removeSpy).toHaveBeenCalledWith(54);\n  });\n\n  it('can duplicate itself', function () {\n    testCondition.duplicate();\n    expect(duplicateSpy).toHaveBeenCalledWith({\n      sourceCondition: mockConfig,\n      index: 54\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/RuleSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport Rule from '../src/Rule.js';\n\ndescribe('A Summary Widget Rule', function () {\n  let mockRuleConfig;\n  let mockDomainObject;\n  let mockOpenMCT;\n  let mockConditionManager;\n  let mockWidgetDnD;\n  let mockEvaluator;\n  let mockContainer;\n  let testRule;\n  let removeSpy;\n  let duplicateSpy;\n  let changeSpy;\n  let conditionChangeSpy;\n\n  beforeEach(function () {\n    mockRuleConfig = {\n      name: 'Name',\n      id: 'mockRule',\n      icon: 'test-icon-name',\n      style: {\n        'background-color': '',\n        'border-color': '',\n        color: ''\n      },\n      expanded: true,\n      conditions: [\n        {\n          object: '',\n          key: '',\n          operation: '',\n          values: []\n        },\n        {\n          object: 'blah',\n          key: 'blah',\n          operation: 'blah',\n          values: ['blah.', 'blah!', 'blah?']\n        }\n      ]\n    };\n    mockDomainObject = {\n      configuration: {\n        ruleConfigById: {\n          mockRule: mockRuleConfig,\n          otherRule: {}\n        },\n        ruleOrder: ['default', 'mockRule', 'otherRule']\n      }\n    };\n\n    mockOpenMCT = {};\n    mockOpenMCT.objects = {};\n    mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');\n\n    mockEvaluator = {};\n    mockEvaluator.getOperationDescription = jasmine\n      .createSpy('evaluator')\n      .and.returnValue('Operation Description');\n\n    mockConditionManager = jasmine.createSpyObj('mockConditionManager', [\n      'on',\n      'getComposition',\n      'loadCompleted',\n      'getEvaluator',\n      'getTelemetryMetadata',\n      'metadataLoadCompleted',\n      'getObjectName',\n      'getTelemetryPropertyName'\n    ]);\n    mockConditionManager.loadCompleted.and.returnValue(false);\n    mockConditionManager.metadataLoadCompleted.and.returnValue(false);\n    mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);\n    mockConditionManager.getComposition.and.returnValue({});\n    mockConditionManager.getTelemetryMetadata.and.returnValue({});\n    mockConditionManager.getObjectName.and.returnValue('Object Name');\n    mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');\n\n    mockWidgetDnD = jasmine.createSpyObj('dnd', ['on', 'setDragImage', 'dragStart']);\n\n    mockContainer = document.createElement('div');\n\n    removeSpy = jasmine.createSpy('removeCallback');\n    duplicateSpy = jasmine.createSpy('duplicateCallback');\n    changeSpy = jasmine.createSpy('changeCallback');\n    conditionChangeSpy = jasmine.createSpy('conditionChangeCallback');\n\n    testRule = new Rule(\n      mockRuleConfig,\n      mockDomainObject,\n      mockOpenMCT,\n      mockConditionManager,\n      mockWidgetDnD\n    );\n    testRule.on('remove', removeSpy);\n    testRule.on('duplicate', duplicateSpy);\n    testRule.on('change', changeSpy);\n    testRule.on('conditionChange', conditionChangeSpy);\n  });\n\n  it('closes its configuration panel on initial load', function () {\n    expect(testRule.getProperty('expanded')).toEqual(false);\n  });\n\n  it('gets its DOM element', function () {\n    mockContainer.append(testRule.getDOM());\n    expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0);\n  });\n\n  it('gets its configuration properties', function () {\n    expect(testRule.getProperty('name')).toEqual('Name');\n    expect(testRule.getProperty('icon')).toEqual('test-icon-name');\n  });\n\n  it('can duplicate itself', function () {\n    testRule.duplicate();\n    mockRuleConfig.expanded = true;\n    expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig);\n  });\n\n  it('can remove itself from the configuration', function () {\n    testRule.remove();\n    expect(removeSpy).toHaveBeenCalled();\n    expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined();\n    expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']);\n  });\n\n  it('updates its configuration on a condition change and invokes callbacks', function () {\n    testRule.onConditionChange({\n      value: 'newValue',\n      property: 'object',\n      index: 0\n    });\n    expect(testRule.getProperty('conditions')[0].object).toEqual('newValue');\n    expect(conditionChangeSpy).toHaveBeenCalled();\n  });\n\n  it('allows initializing a new condition with a default configuration', function () {\n    testRule.initCondition();\n    expect(mockRuleConfig.conditions).toEqual([\n      {\n        object: '',\n        key: '',\n        operation: '',\n        values: []\n      },\n      {\n        object: 'blah',\n        key: 'blah',\n        operation: 'blah',\n        values: ['blah.', 'blah!', 'blah?']\n      },\n      {\n        object: '',\n        key: '',\n        operation: '',\n        values: []\n      }\n    ]);\n  });\n\n  it('allows initializing a new condition from a given configuration', function () {\n    testRule.initCondition({\n      sourceCondition: {\n        object: 'object1',\n        key: 'key1',\n        operation: 'operation1',\n        values: [1, 2, 3]\n      },\n      index: 0\n    });\n    expect(mockRuleConfig.conditions).toEqual([\n      {\n        object: '',\n        key: '',\n        operation: '',\n        values: []\n      },\n      {\n        object: 'object1',\n        key: 'key1',\n        operation: 'operation1',\n        values: [1, 2, 3]\n      },\n      {\n        object: 'blah',\n        key: 'blah',\n        operation: 'blah',\n        values: ['blah.', 'blah!', 'blah?']\n      }\n    ]);\n  });\n\n  it('invokes mutate when updating the domain object', function () {\n    testRule.updateDomainObject();\n    expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();\n  });\n\n  it('builds condition view from condition configuration', function () {\n    mockContainer.append(testRule.getDOM());\n    expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2);\n  });\n\n  it('responds to input of style properties, and updates the preview', function () {\n    testRule.colorInputs['background-color'].set('#434343');\n    expect(mockRuleConfig.style['background-color']).toEqual('#434343');\n    testRule.colorInputs['border-color'].set('#666666');\n    expect(mockRuleConfig.style['border-color']).toEqual('#666666');\n    testRule.colorInputs.color.set('#999999');\n    expect(mockRuleConfig.style.color).toEqual('#999999');\n\n    expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)');\n    expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)');\n    expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)');\n\n    expect(changeSpy).toHaveBeenCalled();\n  });\n\n  it('responds to input for the icon property', function () {\n    testRule.iconInput.set('icon-alert-rect');\n    expect(mockRuleConfig.icon).toEqual('icon-alert-rect');\n    expect(changeSpy).toHaveBeenCalled();\n  });\n\n  /*\n      test for js condition commented out for v1\n      */\n\n  // it('responds to input of text properties', function () {\n  //     var testInputs = ['name', 'label', 'message', 'jsCondition'],\n  //         input;\n\n  //     testInputs.forEach(function (key) {\n  //         input = testRule.textInputs[key];\n  //         input.prop('value', 'A new ' + key);\n  //         input.trigger('input');\n  //         expect(mockRuleConfig[key]).toEqual('A new ' + key);\n  //     });\n\n  //     expect(changeSpy).toHaveBeenCalled();\n  // });\n\n  it('allows input for when the rule triggers', function () {\n    testRule.trigger.value = 'all';\n    const event = new Event('change', {\n      bubbles: true,\n      cancelable: true\n    });\n    testRule.trigger.dispatchEvent(event);\n    expect(testRule.config.trigger).toEqual('all');\n    expect(conditionChangeSpy).toHaveBeenCalled();\n  });\n\n  it('generates a human-readable description from its conditions', function () {\n    testRule.generateDescription();\n    expect(testRule.config.description).toContain(\n      \"Object Name's Property Name Operation Description\"\n    );\n    testRule.config.trigger = 'js';\n    testRule.generateDescription();\n    expect(testRule.config.description).toContain(\n      'when a custom JavaScript condition evaluates to true'\n    );\n  });\n\n  it('initiates a drag event when its grippy is clicked', function () {\n    const event = new Event('mousedown', {\n      bubbles: true,\n      cancelable: true\n    });\n    testRule.grippy.dispatchEvent(event);\n\n    expect(mockWidgetDnD.setDragImage).toHaveBeenCalled();\n    expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule');\n  });\n\n  /*\n      test for js condition commented out for v1\n      */\n\n  it('can remove a condition from its configuration', function () {\n    testRule.removeCondition(0);\n    expect(testRule.config.conditions).toEqual([\n      {\n        object: 'blah',\n        key: 'blah',\n        operation: 'blah',\n        values: ['blah.', 'blah!', 'blah?']\n      }\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/SummaryWidgetSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidget from '../src/SummaryWidget.js';\n\ndescribe('The Summary Widget', function () {\n  let summaryWidget;\n  let mockDomainObject;\n  let mockOldDomainObject;\n  let mockOpenMCT;\n  let mockObjectService;\n  let mockStatusCapability;\n  let mockComposition;\n  let mockContainer;\n  let listenCallback;\n  let listenCallbackSpy;\n\n  beforeEach(function () {\n    mockDomainObject = {\n      identifier: {\n        key: 'testKey',\n        namespace: 'testNamespace'\n      },\n      name: 'testName',\n      composition: [],\n      configuration: {}\n    };\n    mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load']);\n    mockStatusCapability = jasmine.createSpyObj('statusCapability', [\n      'get',\n      'listen',\n      'triggerCallback'\n    ]);\n\n    listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {});\n    mockStatusCapability.get.and.returnValue([]);\n    mockStatusCapability.listen.and.callFake(function (callback) {\n      listenCallback = callback;\n\n      return listenCallbackSpy;\n    });\n    mockStatusCapability.triggerCallback.and.callFake(function () {\n      listenCallback(['editing']);\n    });\n\n    mockOldDomainObject = {};\n    mockOldDomainObject.getCapability = jasmine.createSpy('capability');\n    mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability);\n\n    mockObjectService = {};\n    mockObjectService.getObjects = jasmine.createSpy('objectService');\n    mockObjectService.getObjects.and.returnValue(\n      new Promise(function (resolve, reject) {\n        resolve({\n          'testNamespace:testKey': mockOldDomainObject\n        });\n      })\n    );\n    mockOpenMCT = jasmine.createSpyObj('openmct', ['$injector', 'composition', 'objects']);\n    mockOpenMCT.$injector.get = jasmine.createSpy('get');\n    mockOpenMCT.$injector.get.and.returnValue(mockObjectService);\n    mockOpenMCT.composition = jasmine.createSpyObj('composition', ['get', 'on']);\n    mockOpenMCT.composition.get.and.returnValue(mockComposition);\n    mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');\n    mockOpenMCT.objects.observe = jasmine.createSpy('observe');\n    mockOpenMCT.objects.observe.and.returnValue(function () {});\n\n    summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT);\n    mockContainer = document.createElement('div');\n    summaryWidget.show(mockContainer);\n  });\n\n  xit('queries with legacyId', function () {\n    expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']);\n  });\n\n  it('adds its DOM element to the view', function () {\n    expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);\n  });\n\n  it('initializes a default rule', function () {\n    expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined();\n    expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']);\n  });\n\n  it('builds rules and rule placeholders in view from configuration', function () {\n    expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2);\n  });\n\n  it('allows initializing a new rule with a particular identifier', function () {\n    summaryWidget.initRule('rule0', 'Rule');\n    expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined();\n  });\n\n  it('allows adding a new rule with a unique identifier to the configuration and view', function () {\n    summaryWidget.addRule();\n    expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2);\n    mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {\n      expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();\n    });\n    summaryWidget.addRule();\n    expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3);\n    mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {\n      expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();\n    });\n    expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6);\n  });\n\n  it('allows duplicating a rule from source configuration', function () {\n    const sourceConfig = JSON.parse(\n      JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)\n    );\n    summaryWidget.duplicateRule(sourceConfig);\n    expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2);\n  });\n\n  it('does not duplicate an existing rule in the configuration', function () {\n    summaryWidget.initRule('default', 'Default');\n    expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1);\n  });\n\n  it('uses mutate when updating the domain object only when in edit mode', function () {\n    summaryWidget.editing = true;\n    summaryWidget.updateDomainObject();\n    expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();\n  });\n\n  xit('shows configuration interfaces when in edit mode, and hides them otherwise', function () {\n    setTimeout(function () {\n      summaryWidget.onEdit([]);\n      expect(summaryWidget.editing).toEqual(false);\n      expect(summaryWidget.ruleArea.css('display')).toEqual('none');\n      expect(summaryWidget.testDataArea.css('display')).toEqual('none');\n      expect(summaryWidget.addRuleButton.css('display')).toEqual('none');\n      summaryWidget.onEdit(['editing']);\n      expect(summaryWidget.editing).toEqual(true);\n      expect(summaryWidget.ruleArea.css('display')).not.toEqual('none');\n      expect(summaryWidget.testDataArea.css('display')).not.toEqual('none');\n      expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none');\n    }, 100);\n  });\n\n  xit('unregisters any registered listeners on a destroy', function () {\n    setTimeout(function () {\n      summaryWidget.destroy();\n      expect(listenCallbackSpy).toHaveBeenCalled();\n    }, 100);\n  });\n\n  it('allows reorders of rules', function () {\n    summaryWidget.initRule('rule0');\n    summaryWidget.initRule('rule1');\n    summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1'];\n    summaryWidget.reorder({\n      draggingId: 'rule1',\n      dropTarget: 'default'\n    });\n    expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual([\n      'default',\n      'rule1',\n      'rule0'\n    ]);\n  });\n\n  it('adds hyperlink to the widget button and sets newTab preference', function () {\n    summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab');\n\n    const widgetButton = mockContainer.querySelector('#widget');\n\n    expect(widgetButton.href).toEqual('https://www.nasa.gov/');\n    expect(widgetButton.target).toEqual('_blank');\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport SummaryWidgetViewPolicy from '../SummaryWidgetViewPolicy.js';\n\ndescribe('SummaryWidgetViewPolicy', function () {\n  let policy;\n  let domainObject;\n  let view;\n  beforeEach(function () {\n    policy = new SummaryWidgetViewPolicy();\n    domainObject = jasmine.createSpyObj('domainObject', ['getModel']);\n    domainObject.getModel.and.returnValue({});\n    view = {};\n  });\n\n  it('returns true for other object types', function () {\n    domainObject.getModel.and.returnValue({\n      type: 'random'\n    });\n    expect(policy.allow(view, domainObject)).toBe(true);\n  });\n\n  it('allows summary widget view for summary widgets', function () {\n    domainObject.getModel.and.returnValue({\n      type: 'summary-widget'\n    });\n    view.key = 'summary-widget-viewer';\n    expect(policy.allow(view, domainObject)).toBe(true);\n  });\n\n  it('disallows other views for summary widgets', function () {\n    domainObject.getModel.and.returnValue({\n      type: 'summary-widget'\n    });\n    view.key = 'other view';\n    expect(policy.allow(view, domainObject)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/TestDataItemSpec.js",
    "content": "import TestDataItem from '../src/TestDataItem.js';\n\ndescribe('A summary widget test data item', function () {\n  let testDataItem;\n  let mockConfig;\n  let mockConditionManager;\n  let mockContainer;\n  let mockEvaluator;\n  let changeSpy;\n  let duplicateSpy;\n  let removeSpy;\n  let generateValueSpy;\n\n  beforeEach(function () {\n    mockContainer = document.createElement('div');\n\n    mockConfig = {\n      object: 'object1',\n      key: 'property1',\n      value: 1\n    };\n\n    mockEvaluator = {};\n    mockEvaluator.getInputTypeById = jasmine.createSpy('inputType');\n\n    mockConditionManager = jasmine.createSpyObj('mockConditionManager', [\n      'on',\n      'getComposition',\n      'loadCompleted',\n      'getEvaluator',\n      'getTelemetryMetadata',\n      'metadataLoadCompleted',\n      'getObjectName',\n      'getTelemetryPropertyName',\n      'getTelemetryPropertyType'\n    ]);\n    mockConditionManager.loadCompleted.and.returnValue(false);\n    mockConditionManager.metadataLoadCompleted.and.returnValue(false);\n    mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);\n    mockConditionManager.getComposition.and.returnValue({});\n    mockConditionManager.getTelemetryMetadata.and.returnValue({});\n    mockConditionManager.getObjectName.and.returnValue('Object Name');\n    mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');\n    mockConditionManager.getTelemetryPropertyType.and.returnValue('');\n\n    duplicateSpy = jasmine.createSpy('duplicate');\n    removeSpy = jasmine.createSpy('remove');\n    changeSpy = jasmine.createSpy('change');\n    generateValueSpy = jasmine.createSpy('generateValueInput');\n\n    testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager);\n\n    testDataItem.on('duplicate', duplicateSpy);\n    testDataItem.on('remove', removeSpy);\n    testDataItem.on('change', changeSpy);\n  });\n\n  it('exposes a DOM element to represent itself in the view', function () {\n    mockContainer.append(testDataItem.getDOM());\n    expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1);\n  });\n\n  it('responds to a change in its object select', function () {\n    testDataItem.selects.object.setSelected('');\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: '',\n      property: 'object',\n      index: 54\n    });\n  });\n\n  it('responds to a change in its key select', function () {\n    testDataItem.generateValueInput = generateValueSpy;\n    testDataItem.selects.key.setSelected('');\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: '',\n      property: 'key',\n      index: 54\n    });\n    expect(generateValueSpy).toHaveBeenCalledWith('');\n  });\n\n  it('generates a value input of the appropriate type', function () {\n    let inputs;\n\n    mockContainer.append(testDataItem.getDOM());\n    mockEvaluator.getInputTypeById.and.returnValue('number');\n    testDataItem.generateValueInput('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');\n\n    expect(numberInputs.length).toEqual(1);\n    expect(inputs[0].valueAsNumber).toEqual(1);\n\n    mockEvaluator.getInputTypeById.and.returnValue('text');\n    testDataItem.config.value = 'Text I Am';\n    testDataItem.generateValueInput('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const textInputs = Array.from(inputs).filter((input) => input.type === 'text');\n\n    expect(textInputs.length).toEqual(1);\n    expect(inputs[0].value).toEqual('Text I Am');\n  });\n\n  it('ensures reasonable defaults on values if none are provided', function () {\n    let inputs;\n\n    mockContainer.append(testDataItem.getDOM());\n\n    mockEvaluator.getInputTypeById.and.returnValue('number');\n    testDataItem.config.value = undefined;\n    testDataItem.generateValueInput('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');\n\n    expect(numberInputs.length).toEqual(1);\n    expect(inputs[0].valueAsNumber).toEqual(0);\n    expect(testDataItem.config.value).toEqual(0);\n\n    mockEvaluator.getInputTypeById.and.returnValue('text');\n    testDataItem.config.value = undefined;\n    testDataItem.generateValueInput('');\n\n    inputs = mockContainer.querySelectorAll('input');\n    const textInputs = Array.from(inputs).filter((input) => input.type === 'text');\n\n    expect(textInputs.length).toEqual(1);\n    expect(inputs[0].value).toEqual('');\n    expect(testDataItem.config.value).toEqual('');\n  });\n\n  it('responds to a change in its value inputs', function () {\n    mockContainer.append(testDataItem.getDOM());\n    mockEvaluator.getInputTypeById.and.returnValue('number');\n    testDataItem.generateValueInput('');\n\n    const event = new Event('input', {\n      bubbles: true,\n      cancelable: true\n    });\n\n    mockContainer.querySelector('input').value = 9001;\n    mockContainer.querySelector('input').dispatchEvent(event);\n\n    expect(changeSpy).toHaveBeenCalledWith({\n      value: 9001,\n      property: 'value',\n      index: 54\n    });\n  });\n\n  it('can remove itself from the configuration', function () {\n    testDataItem.remove();\n    expect(removeSpy).toHaveBeenCalledWith(54);\n  });\n\n  it('can duplicate itself', function () {\n    testDataItem.duplicate();\n    expect(duplicateSpy).toHaveBeenCalledWith({\n      sourceItem: mockConfig,\n      index: 54\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/TestDataManagerSpec.js",
    "content": "import TestDataManager from '../src/TestDataManager.js';\n\ndescribe('A Summary Widget Rule', function () {\n  let mockDomainObject;\n  let mockOpenMCT;\n  let mockConditionManager;\n  let mockEvaluator;\n  let mockContainer;\n  let mockTelemetryMetadata;\n  let testDataManager;\n  let mockCompObject1;\n  let mockCompObject2;\n\n  beforeEach(function () {\n    mockDomainObject = {\n      configuration: {\n        testDataConfig: [\n          {\n            object: '',\n            key: '',\n            value: ''\n          },\n          {\n            object: 'object1',\n            key: 'property1',\n            value: 66\n          },\n          {\n            object: 'object2',\n            key: 'property4',\n            value: 'Text It Is'\n          }\n        ]\n      },\n      composition: [\n        {\n          object1: {\n            key: 'object1',\n            name: 'Object 1'\n          },\n          object2: {\n            key: 'object2',\n            name: 'Object 2'\n          }\n        }\n      ]\n    };\n\n    mockTelemetryMetadata = {\n      object1: {\n        property1: {\n          key: 'property1'\n        },\n        property2: {\n          key: 'property2'\n        }\n      },\n      object2: {\n        property3: {\n          key: 'property3'\n        },\n        property4: {\n          key: 'property4'\n        }\n      }\n    };\n\n    mockCompObject1 = {\n      identifier: {\n        key: 'object1'\n      },\n      name: 'Object 1'\n    };\n    mockCompObject2 = {\n      identifier: {\n        key: 'object2'\n      },\n      name: 'Object 2'\n    };\n\n    mockOpenMCT = {};\n    mockOpenMCT.objects = {};\n    mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');\n\n    mockEvaluator = {};\n    mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache');\n    mockEvaluator.useTestData = jasmine.createSpy('useTestData');\n\n    mockConditionManager = jasmine.createSpyObj('mockConditionManager', [\n      'on',\n      'getComposition',\n      'loadCompleted',\n      'getEvaluator',\n      'getTelemetryMetadata',\n      'metadataLoadCompleted',\n      'getObjectName',\n      'getTelemetryPropertyName',\n      'triggerTelemetryCallback'\n    ]);\n    mockConditionManager.loadCompleted.and.returnValue(false);\n    mockConditionManager.metadataLoadCompleted.and.returnValue(false);\n    mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);\n    mockConditionManager.getComposition.and.returnValue({\n      object1: mockCompObject1,\n      object2: mockCompObject2\n    });\n    mockConditionManager.getTelemetryMetadata.and.callFake(function (id) {\n      return mockTelemetryMetadata[id];\n    });\n    mockConditionManager.getObjectName.and.returnValue('Object Name');\n    mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');\n\n    mockContainer = document.createElement('div');\n\n    testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT);\n  });\n\n  it('closes its configuration panel on initial load', function () {});\n\n  it('exposes a DOM element to represent itself in the view', function () {\n    mockContainer.append(testDataManager.getDOM());\n    expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan(0);\n  });\n\n  it('generates a test cache in the format expected by a condition evaluator', function () {\n    testDataManager.updateTestCache();\n    expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({\n      object1: {\n        property1: 66,\n        property2: ''\n      },\n      object2: {\n        property3: '',\n        property4: 'Text It Is'\n      }\n    });\n  });\n\n  it('updates its configuration on a item change and provides an updated cache to the evaluator', function () {\n    testDataManager.onItemChange({\n      value: 26,\n      property: 'value',\n      index: 1\n    });\n    expect(testDataManager.config[1].value).toEqual(26);\n    expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({\n      object1: {\n        property1: 26,\n        property2: ''\n      },\n      object2: {\n        property3: '',\n        property4: 'Text It Is'\n      }\n    });\n  });\n\n  it('allows initializing a new item with a default configuration', function () {\n    testDataManager.initItem();\n    expect(mockDomainObject.configuration.testDataConfig).toEqual([\n      {\n        object: '',\n        key: '',\n        value: ''\n      },\n      {\n        object: 'object1',\n        key: 'property1',\n        value: 66\n      },\n      {\n        object: 'object2',\n        key: 'property4',\n        value: 'Text It Is'\n      },\n      {\n        object: '',\n        key: '',\n        value: ''\n      }\n    ]);\n  });\n\n  it('allows initializing a new item from a given configuration', function () {\n    testDataManager.initItem({\n      sourceItem: {\n        object: 'object2',\n        key: 'property3',\n        value: 1\n      },\n      index: 0\n    });\n    expect(mockDomainObject.configuration.testDataConfig).toEqual([\n      {\n        object: '',\n        key: '',\n        value: ''\n      },\n      {\n        object: 'object2',\n        key: 'property3',\n        value: 1\n      },\n      {\n        object: 'object1',\n        key: 'property1',\n        value: 66\n      },\n      {\n        object: 'object2',\n        key: 'property4',\n        value: 'Text It Is'\n      }\n    ]);\n  });\n\n  it('invokes mutate when updating the domain object', function () {\n    testDataManager.updateDomainObject();\n    expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();\n  });\n\n  it('builds item view from item configuration', function () {\n    mockContainer.append(testDataManager.getDOM());\n    expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3);\n  });\n\n  it('can remove a item from its configuration', function () {\n    testDataManager.removeItem(0);\n    expect(mockDomainObject.configuration.testDataConfig).toEqual([\n      {\n        object: 'object1',\n        key: 'property1',\n        value: 66\n      },\n      {\n        object: 'object2',\n        key: 'property4',\n        value: 'Text It Is'\n      }\n    ]);\n  });\n\n  it('exposes a UI element to toggle test data on and off', function () {});\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/WidgetDnDSpec.js",
    "content": ""
  },
  {
    "path": "src/plugins/summaryWidget/test/input/ColorPaletteSpec.js",
    "content": "import ColorPalette from '../../src/input/ColorPalette.js';\n\ndescribe('An Open MCT color palette', function () {\n  let colorPalette;\n  let changeCallback;\n\n  beforeEach(function () {\n    changeCallback = jasmine.createSpy('changeCallback');\n  });\n\n  it('allows defining a custom color set', function () {\n    colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']);\n    expect(colorPalette.getCurrent()).toEqual('color1');\n    colorPalette.on('change', changeCallback);\n    colorPalette.set('color2');\n    expect(colorPalette.getCurrent()).toEqual('color2');\n    expect(changeCallback).toHaveBeenCalledWith('color2');\n  });\n\n  it('loads with a default color set if one is not provided', function () {\n    colorPalette = new ColorPalette('someClass', 'someContainer');\n    expect(colorPalette.getCurrent()).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/IconPaletteSpec.js",
    "content": "import IconPalette from '../../src/input/IconPalette.js';\n\ndescribe('An Open MCT icon palette', function () {\n  let iconPalette;\n  let changeCallback;\n\n  beforeEach(function () {\n    changeCallback = jasmine.createSpy('changeCallback');\n  });\n\n  it('allows defining a custom icon set', function () {\n    iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']);\n    expect(iconPalette.getCurrent()).toEqual('icon1');\n    iconPalette.on('change', changeCallback);\n    iconPalette.set('icon2');\n    expect(iconPalette.getCurrent()).toEqual('icon2');\n    expect(changeCallback).toHaveBeenCalledWith('icon2');\n  });\n\n  it('loads with a default icon set if one is not provided', function () {\n    iconPalette = new IconPalette('someClass', 'someContainer');\n    expect(iconPalette.getCurrent()).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/KeySelectSpec.js",
    "content": "import KeySelect from '../../src/input/KeySelect.js';\n\ndescribe('A select for choosing composition object properties', function () {\n  let mockConfig;\n  let mockBadConfig;\n  let mockManager;\n  let keySelect;\n  let mockMetadata;\n  let mockObjectSelect;\n  beforeEach(function () {\n    mockConfig = {\n      object: 'object1',\n      key: 'a'\n    };\n\n    mockBadConfig = {\n      object: 'object1',\n      key: 'someNonexistentKey'\n    };\n\n    mockMetadata = {\n      object1: {\n        a: {\n          name: 'A'\n        },\n        b: {\n          name: 'B'\n        }\n      },\n      object2: {\n        alpha: {\n          name: 'Alpha'\n        },\n        beta: {\n          name: 'Beta'\n        }\n      },\n      object3: {\n        a: {\n          name: 'A'\n        }\n      }\n    };\n\n    mockManager = jasmine.createSpyObj('mockManager', [\n      'on',\n      'metadataLoadCompleted',\n      'triggerCallback',\n      'getTelemetryMetadata'\n    ]);\n\n    mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', ['on', 'triggerCallback']);\n\n    mockObjectSelect.on.and.callFake((event, callback) => {\n      mockObjectSelect.callbacks = mockObjectSelect.callbacks || {};\n      mockObjectSelect.callbacks[event] = callback;\n    });\n\n    mockObjectSelect.triggerCallback.and.callFake((event, key) => {\n      mockObjectSelect.callbacks[event](key);\n    });\n\n    mockManager.on.and.callFake((event, callback) => {\n      mockManager.callbacks = mockManager.callbacks || {};\n      mockManager.callbacks[event] = callback;\n    });\n\n    mockManager.triggerCallback.and.callFake((event) => {\n      mockManager.callbacks[event]();\n    });\n\n    mockManager.getTelemetryMetadata.and.callFake(function (key) {\n      return mockMetadata[key];\n    });\n  });\n\n  it('waits until the metadata fully loads to populate itself', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(false);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    expect(keySelect.getSelected()).toEqual('');\n  });\n\n  it('populates itself with metadata on a metadata load', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(false);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    mockManager.triggerCallback('metadata');\n    expect(keySelect.getSelected()).toEqual('a');\n  });\n\n  it('populates itself with metadata if metadata load is already complete', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    expect(keySelect.getSelected()).toEqual('a');\n  });\n\n  it('clears its selection state if the property in its config is not in its object', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager);\n    expect(keySelect.getSelected()).toEqual('');\n  });\n\n  it('populates with the appropriate options when its linked object changes', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    mockObjectSelect.triggerCallback('change', 'object2');\n    keySelect.setSelected('alpha');\n    expect(keySelect.getSelected()).toEqual('alpha');\n  });\n\n  it('clears its selected state on change if the field is not present in the new object', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    mockObjectSelect.triggerCallback('change', 'object2');\n    expect(keySelect.getSelected()).toEqual('');\n  });\n\n  it('maintains its selected state on change if field is present in new object', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);\n    mockObjectSelect.triggerCallback('change', 'object3');\n    expect(keySelect.getSelected()).toEqual('a');\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/ObjectSelectSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ObjectSelect from '../../src/input/ObjectSelect.js';\n\ndescribe('A select for choosing composition objects', function () {\n  let mockConfig;\n  let mockBadConfig;\n  let mockManager;\n  let objectSelect;\n  let mockComposition;\n  beforeEach(function () {\n    mockConfig = {\n      object: 'key1'\n    };\n\n    mockBadConfig = {\n      object: 'someNonexistentObject'\n    };\n\n    mockComposition = {\n      key1: {\n        identifier: {\n          key: 'key1'\n        },\n        name: 'Object 1'\n      },\n      key2: {\n        identifier: {\n          key: 'key2'\n        },\n        name: 'Object 2'\n      }\n    };\n    mockManager = jasmine.createSpyObj('mockManager', [\n      'on',\n      'loadCompleted',\n      'triggerCallback',\n      'getComposition'\n    ]);\n\n    mockManager.on.and.callFake((event, callback) => {\n      mockManager.callbacks = mockManager.callbacks || {};\n      mockManager.callbacks[event] = callback;\n    });\n\n    mockManager.triggerCallback.and.callFake((event, newObj) => {\n      if (event === 'add') {\n        mockManager.callbacks.add(newObj);\n      } else {\n        mockManager.callbacks[event]();\n      }\n    });\n\n    mockManager.getComposition.and.callFake(function () {\n      return mockComposition;\n    });\n  });\n\n  it('allows setting special keyword options', function () {\n    mockManager.loadCompleted.and.returnValue(true);\n    objectSelect = new ObjectSelect(mockConfig, mockManager, [\n      ['keyword1', 'A special option'],\n      ['keyword2', 'A special option']\n    ]);\n    objectSelect.setSelected('keyword1');\n    expect(objectSelect.getSelected()).toEqual('keyword1');\n  });\n\n  it('waits until the composition fully loads to populate itself', function () {\n    mockManager.loadCompleted.and.returnValue(false);\n    objectSelect = new ObjectSelect(mockConfig, mockManager);\n    expect(objectSelect.getSelected()).toEqual('');\n  });\n\n  it('populates itself with composition objects on a composition load', function () {\n    mockManager.loadCompleted.and.returnValue(false);\n    objectSelect = new ObjectSelect(mockConfig, mockManager);\n    mockManager.triggerCallback('load');\n    expect(objectSelect.getSelected()).toEqual('key1');\n  });\n\n  it('populates itself with composition objects if load is already complete', function () {\n    mockManager.loadCompleted.and.returnValue(true);\n    objectSelect = new ObjectSelect(mockConfig, mockManager);\n    expect(objectSelect.getSelected()).toEqual('key1');\n  });\n\n  it('clears its selection state if the object in its config is not in the composition', function () {\n    mockManager.loadCompleted.and.returnValue(true);\n    objectSelect = new ObjectSelect(mockBadConfig, mockManager);\n    expect(objectSelect.getSelected()).toEqual('');\n  });\n\n  it('adds a new option on a composition add', function () {\n    mockManager.loadCompleted.and.returnValue(true);\n    objectSelect = new ObjectSelect(mockConfig, mockManager);\n    mockManager.triggerCallback('add', {\n      identifier: {\n        key: 'key3'\n      },\n      name: 'Object 3'\n    });\n    objectSelect.setSelected('key3');\n    expect(objectSelect.getSelected()).toEqual('key3');\n  });\n\n  it('removes an option on a composition remove', function () {\n    mockManager.loadCompleted.and.returnValue(true);\n    objectSelect = new ObjectSelect(mockConfig, mockManager);\n    delete mockComposition.key1;\n    mockManager.triggerCallback('remove');\n    expect(objectSelect.getSelected()).not.toEqual('key1');\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/OperationSelectSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport OperationSelect from '../../src/input/OperationSelect.js';\n\ndescribe('A select for choosing composition object properties', function () {\n  let mockConfig;\n  let mockBadConfig;\n  let mockManager;\n  let operationSelect;\n  let mockOperations;\n  let mockPropertyTypes;\n  let mockKeySelect;\n  let mockEvaluator;\n  beforeEach(function () {\n    mockConfig = {\n      object: 'object1',\n      key: 'a',\n      operation: 'operation1'\n    };\n\n    mockBadConfig = {\n      object: 'object1',\n      key: 'a',\n      operation: 'someNonexistentOperation'\n    };\n\n    mockOperations = {\n      operation1: {\n        text: 'An operation',\n        appliesTo: ['number']\n      },\n      operation2: {\n        text: 'Another operation',\n        appliesTo: ['string']\n      }\n    };\n\n    mockPropertyTypes = {\n      object1: {\n        a: 'number',\n        b: 'string',\n        c: 'number'\n      }\n    };\n\n    mockManager = jasmine.createSpyObj('mockManager', [\n      'on',\n      'metadataLoadCompleted',\n      'triggerCallback',\n      'getTelemetryPropertyType',\n      'getEvaluator'\n    ]);\n\n    mockKeySelect = jasmine.createSpyObj('mockKeySelect', ['on', 'triggerCallback']);\n\n    mockEvaluator = jasmine.createSpyObj('mockEvaluator', [\n      'getOperationKeys',\n      'operationAppliesTo',\n      'getOperationText'\n    ]);\n\n    mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations));\n\n    mockEvaluator.getOperationText.and.callFake(function (key) {\n      return mockOperations[key].text;\n    });\n\n    mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) {\n      return mockOperations[operation].appliesTo.includes(type);\n    });\n\n    mockKeySelect.on.and.callFake((event, callback) => {\n      mockKeySelect.callbacks = mockKeySelect.callbacks || {};\n      mockKeySelect.callbacks[event] = callback;\n    });\n\n    mockKeySelect.triggerCallback.and.callFake((event, key) => {\n      mockKeySelect.callbacks[event](key);\n    });\n\n    mockManager.on.and.callFake((event, callback) => {\n      mockManager.callbacks = mockManager.callbacks || {};\n      mockManager.callbacks[event] = callback;\n    });\n\n    mockManager.triggerCallback.and.callFake((event) => {\n      mockManager.callbacks[event]();\n    });\n\n    mockManager.getTelemetryPropertyType.and.callFake(function (object, key) {\n      return mockPropertyTypes[object][key];\n    });\n\n    mockManager.getEvaluator.and.returnValue(mockEvaluator);\n  });\n\n  it('waits until the metadata fully loads to populate itself', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(false);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    expect(operationSelect.getSelected()).toEqual('');\n  });\n\n  it('populates itself with operations on a metadata load', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(false);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    mockManager.triggerCallback('metadata');\n    expect(operationSelect.getSelected()).toEqual('operation1');\n  });\n\n  it('populates itself with operations if metadata load is already complete', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    expect(operationSelect.getSelected()).toEqual('operation1');\n  });\n\n  it('clears its selection state if the operation in its config does not apply', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager);\n    expect(operationSelect.getSelected()).toEqual('');\n  });\n\n  it('populates with the appropriate options when its linked key changes', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    mockKeySelect.triggerCallback('change', 'b');\n    operationSelect.setSelected('operation2');\n    expect(operationSelect.getSelected()).toEqual('operation2');\n    operationSelect.setSelected('operation1');\n    expect(operationSelect.getSelected()).not.toEqual('operation1');\n  });\n\n  it('clears its selection on a change if the operation does not apply', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    mockKeySelect.triggerCallback('change', 'b');\n    expect(operationSelect.getSelected()).toEqual('');\n  });\n\n  it('maintains its selected state on change if the operation does apply', function () {\n    mockManager.metadataLoadCompleted.and.returnValue(true);\n    operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);\n    mockKeySelect.triggerCallback('change', 'c');\n    expect(operationSelect.getSelected()).toEqual('operation1');\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/PaletteSpec.js",
    "content": "import Palette from '../../src/input/Palette.js';\n\ndescribe('A generic Open MCT palette input', function () {\n  let palette;\n  let callbackSpy1;\n  let callbackSpy2;\n\n  beforeEach(function () {\n    palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']);\n    callbackSpy1 = jasmine.createSpy('changeCallback1');\n    callbackSpy2 = jasmine.createSpy('changeCallback2');\n  });\n\n  it('gets the current item', function () {\n    expect(palette.getCurrent()).toEqual('item1');\n  });\n\n  it('allows setting the current item', function () {\n    palette.set('item2');\n    expect(palette.getCurrent()).toEqual('item2');\n  });\n\n  it('allows registering change callbacks, and errors when an unsupported event is registered', function () {\n    expect(function () {\n      palette.on('change', callbackSpy1);\n    }).not.toThrow();\n    expect(function () {\n      palette.on('someUnsupportedEvent', callbackSpy1);\n    }).toThrow();\n  });\n\n  it('injects its callbacks with the new selected item on change', function () {\n    palette.on('change', callbackSpy1);\n    palette.on('change', callbackSpy2);\n    palette.set('item2');\n    expect(callbackSpy1).toHaveBeenCalledWith('item2');\n    expect(callbackSpy2).toHaveBeenCalledWith('item2');\n  });\n\n  it('gracefully handles being set to an item not included in its set', function () {\n    palette.set('foobar');\n    expect(palette.getCurrent()).not.toEqual('foobar');\n  });\n});\n"
  },
  {
    "path": "src/plugins/summaryWidget/test/input/SelectSpec.js",
    "content": "import Select from '../../src/input/Select.js';\n\ndescribe('A select wrapper', function () {\n  let select;\n  let testOptions;\n  let callbackSpy1;\n  let callbackSpy2;\n  beforeEach(function () {\n    select = new Select();\n    testOptions = [\n      ['item1', 'Item 1'],\n      ['item2', 'Item 2'],\n      ['item3', 'Item 3']\n    ];\n    select.setOptions(testOptions);\n    callbackSpy1 = jasmine.createSpy('callbackSpy1');\n    callbackSpy2 = jasmine.createSpy('callbackSpy2');\n  });\n\n  it('gets and sets the current item', function () {\n    select.setSelected('item1');\n    expect(select.getSelected()).toEqual('item1');\n  });\n\n  it('allows adding a single new option', function () {\n    select.addOption('newOption', 'A New Option');\n    select.setSelected('newOption');\n    expect(select.getSelected()).toEqual('newOption');\n  });\n\n  it('allows populating with a new set of options', function () {\n    select.setOptions([\n      ['newItem1', 'Item 1'],\n      ['newItem2', 'Item 2']\n    ]);\n    select.setSelected('newItem1');\n    expect(select.getSelected()).toEqual('newItem1');\n  });\n\n  it('allows registering change callbacks, and errors when an unsupported event is registered', function () {\n    expect(function () {\n      select.on('change', callbackSpy1);\n    }).not.toThrow();\n    expect(function () {\n      select.on('someUnsupportedEvent', callbackSpy1);\n    }).toThrow();\n  });\n\n  it('injects its callbacks with its property and value on a change', function () {\n    select.on('change', callbackSpy1);\n    select.on('change', callbackSpy2);\n    select.setSelected('item2');\n    expect(callbackSpy1).toHaveBeenCalledWith('item2');\n    expect(callbackSpy2).toHaveBeenCalledWith('item2');\n  });\n\n  it('gracefully handles being set to an item not included in its set', function () {\n    select.setSelected('foobar');\n    expect(select.getSelected()).not.toEqual('foobar');\n  });\n});\n"
  },
  {
    "path": "src/plugins/tabs/components/TabsComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"tabs\" class=\"c-tabs-view\">\n    <div\n      ref=\"tabsHolder\"\n      role=\"tablist\"\n      class=\"c-tabs-view__tabs-holder c-tabs\"\n      :class=\"{\n        'is-dragging': isDragging && allowEditing,\n        'is-mouse-over': allowDrop\n      }\"\n    >\n      <div class=\"c-drop-hint\" @drop=\"onDrop\" @dragenter=\"dragenter\" @dragleave=\"dragleave\"></div>\n      <div v-if=\"!tabsList.length > 0\" class=\"c-tabs-view__empty-message\">\n        Drag objects here to add them to this view.\n      </div>\n      <div\n        v-for=\"(tab, index) in tabsList\"\n        :ref=\"tab.keyString\"\n        :key=\"tab.keyString\"\n        :aria-label=\"`${tab.domainObject.name} tab${tab.keyString === currentTab.keyString ? ' - selected' : ''}`\"\n        class=\"c-tab c-tabs-view__tab js-tab\"\n        role=\"tab\"\n        :class=\"{\n          'is-current': tab.keyString === currentTab.keyString\n        }\"\n        @click=\"showTab(tab, index)\"\n        @mouseover.ctrl=\"showToolTip(tab)\"\n        @mouseleave=\"hideToolTip\"\n      >\n        <div\n          ref=\"tabsLabel\"\n          class=\"c-tabs-view__tab__label c-object-label\"\n          :class=\"[tab.status ? `is-status--${tab.status}` : '']\"\n        >\n          <div class=\"c-object-label__type-icon\" :class=\"tab.type.definition.cssClass\">\n            <span\n              class=\"is-status__indicator\"\n              :aria-label=\"`This item is ${tab.status}`\"\n              :title=\"`This item is ${tab.status}`\"\n            ></span>\n          </div>\n          <span class=\"c-button__label c-object-label__name\">{{ tab.domainObject.name }}</span>\n        </div>\n        <button\n          v-if=\"isEditing\"\n          class=\"icon-x c-click-icon c-tabs-view__tab__close-btn\"\n          @click=\"showRemoveDialog(index)\"\n        ></button>\n      </div>\n    </div>\n    <div\n      v-for=\"tab in tabsList\"\n      :key=\"tab.keyString\"\n      :style=\"getTabStyles(tab)\"\n      class=\"c-tabs-view__object-holder\"\n      :class=\"{ 'c-tabs-view__object-holder--hidden': tab.keyString !== currentTab.keyString }\"\n    >\n      <ObjectView\n        v-if=\"shouldLoadTab(tab)\"\n        class=\"c-tabs-view__object\"\n        :default-object=\"tab.domainObject\"\n        :object-path=\"tab.objectPath\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport ObjectView from '../../../ui/components/ObjectView.vue';\n\nconst unknownObjectType = {\n  definition: {\n    cssClass: 'icon-object-unknown',\n    name: 'Unknown Type'\n  }\n};\n\nexport default {\n  components: {\n    ObjectView\n  },\n  mixins: [tooltipHelpers],\n  inject: ['openmct', 'domainObject', 'composition', 'objectPath'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  data: function () {\n    let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n    return {\n      tabWidth: undefined,\n      tabHeight: undefined,\n      internalDomainObject: this.domainObject,\n      currentTab: {},\n      currentTabIndex: undefined,\n      tabsList: [],\n      setCurrentTab: false,\n      isDragging: false,\n      allowDrop: false,\n      searchTabKey: `tabs.pos.${keyString}`,\n      loadedTabs: {}\n    };\n  },\n  computed: {\n    allowEditing() {\n      return !this.internalDomainObject.locked && this.isEditing;\n    }\n  },\n  mounted() {\n    if (this.composition) {\n      this.composition.on('add', this.addItem);\n      this.composition.on('remove', this.removeItem);\n      this.composition.on('reorder', this.onReorder);\n\n      this.composition.load().then((tabObjects) => {\n        let currentTabIndexFromURL = this.openmct.router.getSearchParam(this.searchTabKey);\n        let currentTabIndexFromDomainObject = this.internalDomainObject.currentTabIndex;\n\n        if (currentTabIndexFromURL !== null) {\n          this.setCurrentTabByIndex(currentTabIndexFromURL);\n        } else if (currentTabIndexFromDomainObject !== undefined) {\n          this.setCurrentTabByIndex(currentTabIndexFromDomainObject);\n          this.storeCurrentTabIndexInURL(currentTabIndexFromDomainObject);\n        } else if (tabObjects?.length) {\n          this.setCurrentTabByIndex(0);\n          this.storeCurrentTabIndexInURL(0);\n        } else {\n          this.setCurrentTab = true;\n        }\n      });\n    }\n\n    this.handleWindowResize = _.debounce(this.handleWindowResize, 500);\n    this.tabsViewResizeObserver = new ResizeObserver(this.handleWindowResize);\n    this.tabsViewResizeObserver.observe(this.$refs.tabs);\n\n    this.unsubscribe = this.openmct.objects.observe(\n      this.internalDomainObject,\n      '*',\n      this.updateInternalDomainObject\n    );\n\n    this.updateCurrentTab = this.updateCurrentTab.bind(this);\n    this.openmct.router.on('change:params', this.updateCurrentTab);\n\n    document.addEventListener('dragstart', this.dragstart);\n    document.addEventListener('dragend', this.dragend);\n  },\n  beforeUnmount() {\n    this.persistCurrentTabIndex(this.currentTabIndex);\n  },\n  unmounted() {\n    this.composition.off('add', this.addItem);\n    this.composition.off('remove', this.removeItem);\n    this.composition.off('reorder', this.onReorder);\n\n    this.tabsViewResizeObserver.disconnect();\n\n    this.tabsList.forEach((tab) => {\n      tab.statusUnsubscribe();\n    });\n\n    this.unsubscribe();\n    this.clearCurrentTabIndexFromURL();\n\n    this.openmct.router.off('change:params', this.updateCurrentTab);\n\n    document.removeEventListener('dragstart', this.dragstart);\n    document.removeEventListener('dragend', this.dragend);\n  },\n  methods: {\n    addTabToLoaded(tab) {\n      if (!this.internalDomainObject.keep_alive) {\n        this.loadedTabs = {};\n      }\n\n      this.loadedTabs[tab.keyString] = true;\n    },\n    getTabStyles(tab) {\n      let styles = {};\n\n      if (!this.isCurrent(tab)) {\n        styles = {\n          height: this.tabHeight,\n          width: this.tabWidth\n        };\n      }\n\n      return styles;\n    },\n    setCurrentTabByIndex(index) {\n      if (this.tabsList[index]) {\n        this.showTab(this.tabsList[index]);\n      }\n    },\n    showTab(tab, index) {\n      if (!tab) {\n        return;\n      }\n\n      if (index !== undefined) {\n        this.storeCurrentTabIndexInURL(index);\n      }\n\n      this.currentTab = tab;\n      this.setCurrentTab = false;\n      this.addTabToLoaded(tab);\n    },\n    shouldLoadTab(tab) {\n      const isLoaded = this.isTabLoaded(tab);\n      const isCurrent = this.isCurrent(tab);\n      const tabElLoaded = this.tabWidth !== undefined && this.tabHeight !== undefined;\n\n      return (isLoaded && isCurrent) || (isLoaded && !isCurrent && tabElLoaded);\n    },\n    showRemoveDialog(index) {\n      if (!this.tabsList[index]) {\n        return;\n      }\n\n      let activeTab = this.tabsList[index];\n      let childDomainObject = activeTab.domainObject;\n\n      let prompt = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: `This action will remove this tab from the Tabs Layout. Do you want to continue?`,\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: 'true',\n            callback: () => {\n              this.composition.remove(childDomainObject);\n              prompt.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              prompt.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    addItem(domainObject) {\n      let type = this.openmct.types.get(domainObject.type) || unknownObjectType;\n      let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      let status = this.openmct.status.get(domainObject.identifier);\n      let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => {\n        this.updateStatus(keyString, updatedStatus);\n      });\n      let objectPath = [domainObject].concat(this.objectPath.slice());\n      let tabItem = {\n        domainObject,\n        status,\n        statusUnsubscribe,\n        objectPath,\n        type,\n        keyString\n      };\n\n      this.tabsList.push(tabItem);\n\n      if (this.setCurrentTab) {\n        this.showTab(tabItem);\n      }\n    },\n    reset() {\n      this.currentTab = {};\n      this.currentTabIndex = undefined;\n      this.setCurrentTab = true;\n    },\n    removeItem(identifier) {\n      let keyStringToBeRemoved = this.openmct.objects.makeKeyString(identifier);\n\n      let pos = this.tabsList.findIndex((tab) => {\n        return tab.keyString === keyStringToBeRemoved;\n      });\n\n      let tabToBeRemoved = this.tabsList[pos];\n\n      tabToBeRemoved.statusUnsubscribe();\n\n      this.tabsList.splice(pos, 1);\n\n      this.loadedTabs[keyStringToBeRemoved] = undefined;\n      delete this.loadedTabs[keyStringToBeRemoved];\n\n      if (this.isCurrent(tabToBeRemoved)) {\n        this.showTab(this.tabsList[this.tabsList.length - 1], this.tabsList.length - 1);\n      }\n\n      if (!this.tabsList.length) {\n        this.reset();\n      }\n    },\n    onReorder(reorderPlan) {\n      let oldTabs = this.tabsList.slice();\n\n      reorderPlan.forEach((reorderEvent) => {\n        this.tabsList[reorderEvent.newIndex] = oldTabs[reorderEvent.oldIndex];\n      });\n    },\n    onDrop(e) {\n      this.storeCurrentTabIndexInURL(this.tabsList.length);\n    },\n    dragstart(e) {\n      if (e.dataTransfer.types.includes('openmct/domain-object-path')) {\n        this.isDragging = true;\n      }\n    },\n    dragend(e) {\n      this.isDragging = false;\n      this.allowDrop = false;\n    },\n    dragenter() {\n      this.allowDrop = true;\n    },\n    dragleave() {\n      this.allowDrop = false;\n    },\n    isCurrent(tab) {\n      return this.currentTab.keyString === tab.keyString;\n    },\n    updateInternalDomainObject(domainObject) {\n      this.internalDomainObject = domainObject;\n    },\n    persistCurrentTabIndex(index) {\n      //only persist if the domain object is not locked. The object mutate API will deal with whether the object is persistable or not.\n      if (!this.internalDomainObject.locked) {\n        this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);\n      }\n    },\n    storeCurrentTabIndexInURL(index) {\n      let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);\n\n      if (index !== currentTabIndexInURL) {\n        this.openmct.router.setSearchParam(this.searchTabKey, index);\n        this.currentTabIndex = index;\n      }\n    },\n    clearCurrentTabIndexFromURL() {\n      this.openmct.router.deleteSearchParam(this.searchTabKey);\n    },\n    updateStatus(keyString, status) {\n      let tabPos = this.tabsList.findIndex((tab) => {\n        return tab.keyString === keyString;\n      });\n      let tab = this.tabsList[tabPos];\n\n      tab.status = status;\n    },\n    isTabLoaded(tab) {\n      if (this.internalDomainObject.keep_alive) {\n        return true;\n      } else {\n        return this.loadedTabs[tab.keyString];\n      }\n    },\n    updateCurrentTab(newParams, oldParams, changedParams) {\n      const tabIndex = changedParams[this.searchTabKey];\n      if (!tabIndex) {\n        return;\n      }\n\n      if (this.currentTabIndex === parseInt(tabIndex, 10)) {\n        return;\n      }\n\n      this.currentTabIndex = tabIndex;\n      this.showTab(this.tabsList[tabIndex]);\n    },\n    handleWindowResize() {\n      if (!this.$refs.tabs || !this.$refs.tabsHolder) {\n        return;\n      }\n\n      this.tabWidth = this.$refs.tabs.offsetWidth + 'px';\n      this.tabHeight = this.$refs.tabsHolder.offsetHeight - this.$refs.tabs.offsetHeight + 'px';\n    },\n    async showToolTip(tab) {\n      const identifier = tab.domainObject.identifier;\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(identifier), BELOW, tab.keyString);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/tabs/components/tabs.scss",
    "content": ".c-tabs-view {\n  $h: 20px;\n  @include abs();\n  display: flex;\n  flex-flow: column nowrap;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__tabs-holder {\n    min-height: $h;\n  }\n\n  &__tab {\n    justify-content: space-between; // Places remove button to far side of tab\n\n    &__close-btn {\n      flex: 0 0 auto;\n      pointer-events: all;\n    }\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  &__object-holder {\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n\n    &--hidden {\n      position: absolute;\n      left: -9999px;\n      top: -9999px;\n    }\n  }\n\n  &__object-name {\n    font-size: 1em;\n    margin: $interiorMargin 0 $interiorMarginLg 0;\n  }\n\n  &__object {\n    display: flex;\n    flex-flow: column nowrap;\n    flex: 1 1 auto;\n    height: 0; // Chrome 73 overflow bug fix\n  }\n\n  &__empty-message {\n    background: rgba($colorBodyFg, 0.1);\n    color: rgba($colorBodyFg, 0.7);\n    font-style: italic;\n    text-align: center;\n    line-height: $h;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "src/plugins/tabs/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Tabs from './tabs.js';\n\nexport default function plugin(options) {\n  return function install(openmct) {\n    const eagerLoad = options?.eagerLoad ?? false;\n\n    openmct.objectViews.addProvider(new Tabs(openmct));\n\n    openmct.types.addType('tabs', {\n      name: 'Tabs View',\n      description: 'Quickly navigate between multiple objects of any type using tabs.',\n      creatable: true,\n      cssClass: 'icon-tabs-view',\n      initialize(domainObject) {\n        domainObject.composition = [];\n        domainObject.keep_alive = eagerLoad;\n      },\n      form: [\n        {\n          key: 'keep_alive',\n          name: 'Eager Load Tabs',\n          control: 'toggleSwitch',\n          options: [\n            {\n              name: 'True',\n              value: true\n            },\n            {\n              name: 'False',\n              value: false\n            }\n          ],\n          required: true,\n          cssClass: 'l-input'\n        }\n      ]\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/tabs/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport TabsLayout from './plugin.js';\n\ndescribe('the plugin', function () {\n  let element;\n  let child;\n  let openmct;\n  let tabsType;\n\n  const testViewObject = {\n    identifier: {\n      key: 'mock-tabs-object',\n      namespace: ''\n    },\n    type: 'tabs',\n    name: 'Tabs view',\n    keep_alive: true,\n    composition: [\n      {\n        identifier: {\n          namespace: '',\n          key: 'swg-1'\n        }\n      },\n      {\n        identifier: {\n          namespace: '',\n          key: 'swg-2'\n        }\n      }\n    ]\n  };\n  const telemetryItemTemplate = {\n    telemetry: {\n      period: 5,\n      amplitude: 5,\n      offset: 5,\n      dataRateInHz: 5,\n      phase: 5,\n      randomness: 0\n    },\n    type: 'generator',\n    modified: 1592851063871,\n    location: 'mine',\n    persisted: 1592851063871\n  };\n  let telemetryItem1 = Object.assign({}, telemetryItemTemplate, {\n    name: 'Sine Wave Generator 1',\n    identifier: {\n      namespace: '',\n      key: 'swg-1'\n    }\n  });\n  let telemetryItem2 = Object.assign({}, telemetryItemTemplate, {\n    name: 'Sine Wave Generator 2',\n    identifier: {\n      namespace: '',\n      key: 'swg-2'\n    }\n  });\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    tabsType = openmct.types.get('tabs');\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    child.style.display = 'block';\n    child.style.width = '1920px';\n    child.style.height = '1080px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    child = undefined;\n    element = undefined;\n\n    return resetApplicationState(openmct);\n  });\n\n  it('is installed by default and provides a tabs object', () => {\n    expect(tabsType.definition.name).toEqual('Tabs View');\n  });\n\n  it('the tabs object is creatable', () => {\n    expect(tabsType.definition.creatable).toEqual(true);\n  });\n\n  it('sets eager load to false by default', () => {\n    const tabsObject = {\n      identifier: {\n        key: 'some-tab-object',\n        namespace: ''\n      },\n      type: 'tabs'\n    };\n\n    tabsType.definition.initialize(tabsObject);\n\n    expect(tabsObject.keep_alive).toBeFalse();\n  });\n\n  it('can be installed with eager load defaulting to true', () => {\n    const options = {\n      eagerLoad: true\n    };\n    const openmct2 = createOpenMct();\n    openmct2.install(new TabsLayout(options));\n    openmct2.startHeadless();\n\n    const tabsObject = {\n      identifier: {\n        key: 'some-tab-object',\n        namespace: ''\n      },\n      type: 'tabs'\n    };\n\n    const overriddenTabsType = openmct2.types.get('tabs');\n    overriddenTabsType.definition.initialize(tabsObject);\n\n    expect(tabsObject.keep_alive).toBeTrue();\n\n    return resetApplicationState(openmct2);\n  });\n\n  describe('the view', function () {\n    let tabsLayoutViewProvider;\n    let mockComposition;\n\n    beforeEach(() => {\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        return Promise.resolve([telemetryItem1]);\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const applicableViews = openmct.objectViews.get(testViewObject, []);\n      tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');\n      let view = tabsLayoutViewProvider.view(testViewObject, []);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('provides a view', () => {\n      expect(tabsLayoutViewProvider).toBeDefined();\n    });\n\n    it('renders tab element', () => {\n      const tabsElements = element.querySelectorAll('.c-tabs');\n\n      expect(tabsElements.length).toBe(1);\n    });\n\n    it('renders empty tab element with msg', () => {\n      const tabsElement = element.querySelector('.c-tabs');\n\n      expect(tabsElement.innerText.trim()).toEqual('Drag objects here to add them to this view.');\n    });\n  });\n\n  describe('the view', function () {\n    let tabsLayoutViewProvider;\n    let mockComposition;\n    let count = 0;\n\n    beforeEach(() => {\n      mockComposition = new EventEmitter();\n      mockComposition.load = () => {\n        if (count === 0) {\n          mockComposition.emit('add', telemetryItem1);\n          mockComposition.emit('add', telemetryItem2);\n          count++;\n        }\n\n        return Promise.resolve([telemetryItem1, telemetryItem2]);\n      };\n\n      spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n\n      const applicableViews = openmct.objectViews.get(testViewObject, []);\n      tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');\n      let view = tabsLayoutViewProvider.view(testViewObject, []);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    afterEach(() => {\n      count = 0;\n      testViewObject.keep_alive = true;\n    });\n\n    it('renders a tab for each item', () => {\n      let tabEls = element.querySelectorAll('.js-tab');\n\n      expect(tabEls.length).toEqual(2);\n    });\n\n    describe('with domainObject.keep_alive set to', () => {\n      it('true, will keep all views loaded, regardless of current tab view', async () => {\n        let tabEls = element.querySelectorAll('.js-tab');\n\n        for (let i = 0; i < tabEls.length; i++) {\n          const tab = tabEls[i];\n\n          tab.click();\n          await nextTick();\n\n          const tabViewEls = element.querySelectorAll('.c-tabs-view__object');\n          expect(tabViewEls.length).toEqual(2);\n        }\n      });\n\n      it('false, will only keep the current tab view loaded', async () => {\n        testViewObject.keep_alive = false;\n\n        await nextTick();\n\n        let tabViewEls = element.querySelectorAll('.c-tabs-view__object');\n\n        expect(tabViewEls.length).toEqual(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/tabs/tabs.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport TabsComponent from './components/TabsComponent.vue';\n\nconst TABS_KEY = 'tabs';\nexport default class Tabs {\n  constructor(openmct) {\n    this.openmct = openmct;\n    this.key = TABS_KEY;\n    this.name = 'Tabs';\n    this.cssClass = 'icon-list-view';\n    this.destroy = null;\n  }\n\n  canView(domainObject) {\n    return domainObject.type === TABS_KEY;\n  }\n\n  canEdit(domainObject) {\n    return domainObject.type === TABS_KEY;\n  }\n\n  view(domainObject, objectPath) {\n    let openmct = this.openmct;\n    let component = null;\n\n    return {\n      show(element, editMode) {\n        const { vNode, destroy } = mount(\n          {\n            el: element,\n            components: {\n              TabsComponent\n            },\n            provide: {\n              openmct,\n              domainObject,\n              objectPath,\n              composition: openmct.composition.get(domainObject)\n            },\n            data() {\n              return {\n                isEditing: editMode\n              };\n            },\n            template: '<tabs-component :isEditing=\"isEditing\"></tabs-component>'\n          },\n          {\n            app: openmct.app,\n            element\n          }\n        );\n        this.destroy = destroy;\n        component = vNode.componentInstance;\n      },\n      onEditModeChange(editMode) {\n        component.isEditing = editMode;\n      },\n      destroy: function (element) {\n        if (this.destroy) {\n          this.destroy();\n        }\n        component = null;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryMean/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport MeanTelemetryProvider from './src/MeanTelemetryProvider.js';\n\nconst DEFAULT_SAMPLES = 10;\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.types.addType('telemetry-mean', {\n      name: 'Telemetry Filter',\n      description:\n        'Provides telemetry values that represent the mean of the last N values of a telemetry stream',\n      creatable: true,\n      cssClass: 'icon-telemetry',\n      initialize: function (domainObject) {\n        domainObject.samples = DEFAULT_SAMPLES;\n        domainObject.telemetry = {};\n        domainObject.telemetry.values = openmct.time\n          .getAllTimeSystems()\n          .map(function (timeSystem, index) {\n            return {\n              key: timeSystem.key,\n              name: timeSystem.name,\n              hints: {\n                domain: index + 1\n              }\n            };\n          });\n        domainObject.telemetry.values.push({\n          key: 'value',\n          name: 'Value',\n          hints: {\n            range: 1\n          }\n        });\n      },\n      form: [\n        {\n          key: 'telemetryPoint',\n          name: 'Telemetry Point',\n          control: 'textfield',\n          required: true,\n          cssClass: 'l-input-lg'\n        },\n        {\n          key: 'samples',\n          name: 'Samples to Average',\n          control: 'textfield',\n          required: true,\n          cssClass: 'l-input-sm'\n        }\n      ]\n    });\n    openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/telemetryMean/src/MeanTelemetryProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { parseKeyString } from 'objectUtils';\n\nimport TelemetryAverager from './TelemetryAverager.js';\n\nexport default function MeanTelemetryProvider(openmct) {\n  this.openmct = openmct;\n  this.telemetryAPI = openmct.telemetry;\n  this.timeAPI = openmct.time;\n  this.objectAPI = openmct.objects;\n  this.perObjectProviders = {};\n}\n\nMeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) {\n  return domainObject.type === 'telemetry-mean';\n};\n\nMeanTelemetryProvider.prototype.supportsRequest =\n  MeanTelemetryProvider.prototype.supportsSubscribe =\n    MeanTelemetryProvider.prototype.canProvideTelemetry;\n\nMeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) {\n  let wrappedUnsubscribe;\n  let unsubscribeCalled = false;\n  const objectId = parseKeyString(domainObject.telemetryPoint);\n  const samples = domainObject.samples;\n\n  this.objectAPI\n    .get(objectId)\n    .then(\n      function (linkedDomainObject) {\n        if (!unsubscribeCalled) {\n          wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback);\n        }\n      }.bind(this)\n    )\n    .catch(logError);\n\n  return function unsubscribe() {\n    unsubscribeCalled = true;\n    if (wrappedUnsubscribe !== undefined) {\n      wrappedUnsubscribe();\n    }\n  };\n};\n\nMeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) {\n  const telemetryAverager = new TelemetryAverager(\n    this.telemetryAPI,\n    this.timeAPI,\n    domainObject,\n    samples,\n    callback\n  );\n  const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);\n\n  return this.telemetryAPI.subscribe(domainObject, createAverageDatum);\n};\n\nMeanTelemetryProvider.prototype.request = function (domainObject, request) {\n  const objectId = parseKeyString(domainObject.telemetryPoint);\n  const samples = domainObject.samples;\n\n  return this.objectAPI.get(objectId).then(\n    function (linkedDomainObject) {\n      return this.requestAverageTelemetry(linkedDomainObject, request, samples);\n    }.bind(this)\n  );\n};\n\n/**\n * @private\n */\nMeanTelemetryProvider.prototype.requestAverageTelemetry = function (\n  domainObject,\n  request,\n  samples\n) {\n  const averageData = [];\n  const addToAverageData = averageData.push.bind(averageData);\n  const telemetryAverager = new TelemetryAverager(\n    this.telemetryAPI,\n    this.timeAPI,\n    domainObject,\n    samples,\n    addToAverageData\n  );\n  const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);\n\n  return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) {\n    telemetryData.forEach(createAverageDatum);\n\n    return averageData;\n  });\n};\n\n/**\n * @private\n */\nMeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) {\n  const objectId = parseKeyString(domainObject.telemetryPoint);\n\n  return this.objectAPI.get(objectId);\n};\n\nfunction logError(error) {\n  if (error.stack) {\n    console.error(error.stack);\n  } else {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/* eslint-disable no-invalid-this */\n\nimport MeanTelemetryProvider from './MeanTelemetryProvider.js';\nimport MockTelemetryApi from './MockTelemetryApi.js';\n\nconst RANGE_KEY = 'value';\n\ndescribe('The Mean Telemetry Provider', function () {\n  let mockApi;\n  let meanTelemetryProvider;\n  let mockDomainObject;\n  let associatedObject;\n  let allPromises;\n\n  beforeEach(function () {\n    allPromises = [];\n    createMockApi();\n    setTimeSystemTo('utc');\n    createMockObjects();\n    meanTelemetryProvider = new MeanTelemetryProvider(mockApi);\n  });\n\n  it('supports telemetry-mean objects only', function () {\n    const mockTelemetryMeanObject = mockObjectWithType('telemetry-mean');\n    const mockOtherObject = mockObjectWithType('other');\n\n    expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true);\n    expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false);\n  });\n\n  describe('the subscribe function', function () {\n    let subscriptionCallback;\n\n    beforeEach(function () {\n      subscriptionCallback = jasmine.createSpy('subscriptionCallback');\n    });\n\n    it('subscribes to telemetry for the associated object', function () {\n      meanTelemetryProvider.subscribe(mockDomainObject);\n\n      return expectObjectWasSubscribedTo(associatedObject);\n    });\n\n    it('returns a function that unsubscribes from the associated object', function () {\n      const unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject);\n\n      return waitForPromises()\n        .then(unsubscribe)\n        .then(waitForPromises)\n        .then(function () {\n          expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled();\n        });\n    });\n\n    it('returns an average only when the sample size is reached', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        }\n      ];\n\n      setSampleSize(5);\n      meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n\n      return waitForPromises()\n        .then(feedInputTelemetry.bind(this, inputTelemetry))\n        .then(function () {\n          expect(subscriptionCallback).not.toHaveBeenCalled();\n        });\n    });\n\n    it('correctly averages a sample of five values', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        }\n      ];\n      const expectedAverages = [\n        {\n          utc: 5,\n          value: 222.44888\n        }\n      ];\n\n      setSampleSize(5);\n      meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n\n      return waitForPromises()\n        .then(feedInputTelemetry.bind(this, inputTelemetry))\n        .then(expectAveragesForTelemetry.bind(this, expectedAverages));\n    });\n\n    it('correctly averages a sample of ten values', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        },\n        {\n          utc: 6,\n          defaultRange: 2323.12\n        },\n        {\n          utc: 7,\n          defaultRange: 532.12\n        },\n        {\n          utc: 8,\n          defaultRange: 453.543\n        },\n        {\n          utc: 9,\n          defaultRange: 89.2111\n        },\n        {\n          utc: 10,\n          defaultRange: 0.543\n        }\n      ];\n      const expectedAverages = [\n        {\n          utc: 10,\n          value: 451.07815\n        }\n      ];\n\n      setSampleSize(10);\n      meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n\n      return waitForPromises()\n        .then(feedInputTelemetry.bind(this, inputTelemetry))\n        .then(expectAveragesForTelemetry.bind(this, expectedAverages));\n    });\n\n    it('only averages values within its sample window', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        },\n        {\n          utc: 6,\n          defaultRange: 2323.12\n        },\n        {\n          utc: 7,\n          defaultRange: 532.12\n        },\n        {\n          utc: 8,\n          defaultRange: 453.543\n        },\n        {\n          utc: 9,\n          defaultRange: 89.2111\n        },\n        {\n          utc: 10,\n          defaultRange: 0.543\n        }\n      ];\n      const expectedAverages = [\n        {\n          utc: 5,\n          value: 222.44888\n        },\n        {\n          utc: 6,\n          value: 662.4482599999999\n        },\n        {\n          utc: 7,\n          value: 704.6078\n        },\n        {\n          utc: 8,\n          value: 773.02748\n        },\n        {\n          utc: 9,\n          value: 679.8234399999999\n        },\n        {\n          utc: 10,\n          value: 679.70742\n        }\n      ];\n\n      setSampleSize(5);\n      meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n\n      return waitForPromises()\n        .then(feedInputTelemetry.bind(this, inputTelemetry))\n        .then(expectAveragesForTelemetry.bind(this, expectedAverages));\n    });\n    describe('given telemetry input with range values', function () {\n      let inputTelemetry;\n\n      beforeEach(function () {\n        inputTelemetry = [\n          {\n            utc: 1,\n            rangeKey: 5678,\n            otherKey: 9999\n          }\n        ];\n        setSampleSize(1);\n      });\n      it(\"uses the 'rangeKey' input range, when it is the default, to calculate the average\", function () {\n        const averageTelemetryForRangeKey = [\n          {\n            utc: 1,\n            value: 5678\n          }\n        ];\n\n        meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n        mockApi.telemetry.setDefaultRangeTo('rangeKey');\n\n        return waitForPromises()\n          .then(feedInputTelemetry.bind(this, inputTelemetry))\n          .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey));\n      });\n\n      it(\"uses the 'otherKey' input range, when it is the default, to calculate the average\", function () {\n        const averageTelemetryForOtherKey = [\n          {\n            utc: 1,\n            value: 9999\n          }\n        ];\n\n        meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n        mockApi.telemetry.setDefaultRangeTo('otherKey');\n\n        return waitForPromises()\n          .then(feedInputTelemetry.bind(this, inputTelemetry))\n          .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey));\n      });\n    });\n    describe('given telemetry input with range values', function () {\n      let inputTelemetry;\n\n      beforeEach(function () {\n        inputTelemetry = [\n          {\n            utc: 1,\n            rangeKey: 5678,\n            otherKey: 9999\n          }\n        ];\n        setSampleSize(1);\n      });\n      it(\"uses the 'rangeKey' input range, when it is the default, to calculate the average\", function () {\n        const averageTelemetryForRangeKey = [\n          {\n            utc: 1,\n            value: 5678\n          }\n        ];\n\n        meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n        mockApi.telemetry.setDefaultRangeTo('rangeKey');\n\n        return waitForPromises()\n          .then(feedInputTelemetry.bind(this, inputTelemetry))\n          .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey));\n      });\n\n      it(\"uses the 'otherKey' input range, when it is the default, to calculate the average\", function () {\n        const averageTelemetryForOtherKey = [\n          {\n            utc: 1,\n            value: 9999\n          }\n        ];\n\n        meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback);\n        mockApi.telemetry.setDefaultRangeTo('otherKey');\n\n        return waitForPromises()\n          .then(feedInputTelemetry.bind(this, inputTelemetry))\n          .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey));\n      });\n    });\n\n    function feedInputTelemetry(inputTelemetry) {\n      inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry);\n    }\n\n    function expectAveragesForTelemetry(expectedAverages) {\n      return waitForPromises().then(function () {\n        expectedAverages.forEach(function (averageDatum) {\n          expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum);\n        });\n      });\n    }\n\n    function expectObjectWasSubscribedTo(object) {\n      return waitForPromises().then(function () {\n        expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function));\n      });\n    }\n  });\n\n  describe('the request function', function () {\n    it('requests telemetry for the associated object', function () {\n      whenTelemetryRequestedReturn([]);\n\n      return meanTelemetryProvider.request(mockDomainObject).then(function () {\n        expect(mockApi.telemetry.request).toHaveBeenCalledWith(associatedObject, undefined);\n      });\n    });\n\n    it('returns an average only when the sample size is reached', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        }\n      ];\n\n      setSampleSize(5);\n      whenTelemetryRequestedReturn(inputTelemetry);\n\n      return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) {\n        expect(averageData.length).toBe(0);\n      });\n    });\n\n    it('correctly averages a sample of five values', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        }\n      ];\n\n      setSampleSize(5);\n      whenTelemetryRequestedReturn(inputTelemetry);\n\n      return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) {\n        expectAverageToBe(222.44888, averageData);\n      });\n    });\n\n    it('correctly averages a sample of ten values', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        },\n        {\n          utc: 6,\n          defaultRange: 2323.12\n        },\n        {\n          utc: 7,\n          defaultRange: 532.12\n        },\n        {\n          utc: 8,\n          defaultRange: 453.543\n        },\n        {\n          utc: 9,\n          defaultRange: 89.2111\n        },\n        {\n          utc: 10,\n          defaultRange: 0.543\n        }\n      ];\n\n      setSampleSize(10);\n      whenTelemetryRequestedReturn(inputTelemetry);\n\n      return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) {\n        expectAverageToBe(451.07815, averageData);\n      });\n    });\n\n    it('only averages values within its sample window', function () {\n      const inputTelemetry = [\n        {\n          utc: 1,\n          defaultRange: 123.1231\n        },\n        {\n          utc: 2,\n          defaultRange: 321.3223\n        },\n        {\n          utc: 3,\n          defaultRange: 111.4446\n        },\n        {\n          utc: 4,\n          defaultRange: 555.2313\n        },\n        {\n          utc: 5,\n          defaultRange: 1.1231\n        },\n        {\n          utc: 6,\n          defaultRange: 2323.12\n        },\n        {\n          utc: 7,\n          defaultRange: 532.12\n        },\n        {\n          utc: 8,\n          defaultRange: 453.543\n        },\n        {\n          utc: 9,\n          defaultRange: 89.2111\n        },\n        {\n          utc: 10,\n          defaultRange: 0.543\n        }\n      ];\n\n      setSampleSize(5);\n      whenTelemetryRequestedReturn(inputTelemetry);\n\n      return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) {\n        expectAverageToBe(679.70742, averageData);\n      });\n    });\n\n    function expectAverageToBe(expectedValue, averageData) {\n      const averageDatum = averageData[averageData.length - 1];\n      expect(averageDatum[RANGE_KEY]).toBe(expectedValue);\n    }\n\n    function whenTelemetryRequestedReturn(telemetry) {\n      mockApi.telemetry.request.and.returnValue(resolvePromiseWith(telemetry));\n    }\n  });\n\n  function createMockObjects() {\n    mockDomainObject = {\n      telemetryPoint: 'someTelemetryPoint'\n    };\n    associatedObject = {};\n    mockApi.objects.get.and.returnValue(resolvePromiseWith(associatedObject));\n  }\n\n  function setSampleSize(sampleSize) {\n    mockDomainObject.samples = sampleSize;\n  }\n\n  function createMockApi() {\n    mockApi = {\n      telemetry: new MockTelemetryApi(),\n      objects: createMockObjectApi(),\n      time: createMockTimeApi()\n    };\n  }\n\n  function createMockObjectApi() {\n    return jasmine.createSpyObj('ObjectAPI', ['get']);\n  }\n\n  function mockObjectWithType(type) {\n    return {\n      type: type\n    };\n  }\n\n  function resolvePromiseWith(value) {\n    const promise = Promise.resolve(value);\n    allPromises.push(promise);\n\n    return promise;\n  }\n\n  function waitForPromises() {\n    return Promise.all(allPromises);\n  }\n\n  function createMockTimeApi() {\n    return jasmine.createSpyObj('timeApi', ['getTimeSystem', 'setTimeSystem']);\n  }\n\n  function setTimeSystemTo(timeSystemKey) {\n    mockApi.time.getTimeSystem.and.returnValue({\n      key: timeSystemKey\n    });\n  }\n});\n"
  },
  {
    "path": "src/plugins/telemetryMean/src/MockTelemetryApi.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function MockTelemetryApi() {\n  this.createSpy('subscribe');\n  this.createSpy('getMetadata');\n\n  this.metadata = this.createMockMetadata();\n  this.setDefaultRangeTo('defaultRange');\n  this.unsubscribe = jasmine.createSpy('unsubscribe');\n  this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this);\n}\n\nMockTelemetryApi.prototype.subscribe = function () {\n  return this.unsubscribe;\n};\n\nMockTelemetryApi.prototype.getMetadata = function (object) {\n  return this.metadata;\n};\n\nMockTelemetryApi.prototype.request = jasmine.createSpy('request');\n\nMockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) {\n  const mockValueFormatter = jasmine.createSpyObj('valueFormatter', ['parse']);\n\n  mockValueFormatter.parse.and.callFake(function (value) {\n    return value[valueMetadata.key];\n  });\n\n  return mockValueFormatter;\n};\n\nMockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) {\n  const subscriptionCallback = this.subscribe.calls.mostRecent().args[1];\n  subscriptionCallback(newTelemetryDatum);\n};\n\n/**\n * @private\n */\nMockTelemetryApi.prototype.onRequestReturn = function (telemetryData) {\n  this.requestTelemetry = telemetryData;\n};\n\n/**\n * @private\n */\nMockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) {\n  const mockMetadataValue = {\n    key: rangeKey\n  };\n  this.metadata.valuesForHints.and.returnValue([mockMetadataValue]);\n};\n\n/**\n * @private\n */\nMockTelemetryApi.prototype.createMockMetadata = function () {\n  const mockMetadata = jasmine.createSpyObj('metadata', ['value', 'valuesForHints']);\n\n  mockMetadata.value.and.callFake(function (key) {\n    return {\n      key: key\n    };\n  });\n\n  return mockMetadata;\n};\n\n/**\n * @private\n */\nMockTelemetryApi.prototype.createSpy = function (functionName) {\n  this[functionName] = this[functionName].bind(this);\n  spyOn(this, functionName);\n  this[functionName].and.callThrough();\n};\n"
  },
  {
    "path": "src/plugins/telemetryMean/src/TelemetryAverager.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function TelemetryAverager(\n  telemetryAPI,\n  timeAPI,\n  domainObject,\n  samples,\n  averageDatumCallback\n) {\n  this.telemetryAPI = telemetryAPI;\n  this.timeAPI = timeAPI;\n\n  this.domainObject = domainObject;\n  this.samples = samples;\n  this.averagingWindow = [];\n\n  this.rangeKey = undefined;\n  this.rangeFormatter = undefined;\n  this.setRangeKeyAndFormatter();\n\n  // Defined dynamically based on current time system\n  this.domainKey = undefined;\n  this.domainFormatter = undefined;\n\n  this.averageDatumCallback = averageDatumCallback;\n}\n\nTelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) {\n  this.setDomainKeyAndFormatter();\n\n  const timeValue = this.domainFormatter.parse(telemetryDatum);\n  const rangeValue = this.rangeFormatter.parse(telemetryDatum);\n\n  this.averagingWindow.push(rangeValue);\n\n  if (this.averagingWindow.length < this.samples) {\n    // We do not have enough data to produce an average\n    return;\n  } else if (this.averagingWindow.length > this.samples) {\n    //Do not let averaging window grow beyond defined sample size\n    this.averagingWindow.shift();\n  }\n\n  const averageValue = this.calculateMean();\n\n  const meanDatum = {};\n  meanDatum[this.domainKey] = timeValue;\n  meanDatum.value = averageValue;\n\n  this.averageDatumCallback(meanDatum);\n};\n\n/**\n * @private\n */\nTelemetryAverager.prototype.calculateMean = function () {\n  let sum = 0;\n  let i = 0;\n\n  for (; i < this.averagingWindow.length; i++) {\n    sum += this.averagingWindow[i];\n  }\n\n  return sum / this.averagingWindow.length;\n};\n\n/**\n * The mean telemetry filter produces domain values in whatever time\n * system is currently selected from the conductor. Because this can\n * change dynamically, the averager needs to be updated regularly with\n * the current domain.\n * @private\n */\nTelemetryAverager.prototype.setDomainKeyAndFormatter = function () {\n  const domainKey = this.timeAPI.getTimeSystem().key;\n  if (domainKey !== this.domainKey) {\n    this.domainKey = domainKey;\n    this.domainFormatter = this.getFormatter(domainKey);\n  }\n};\n\n/**\n * @private\n */\nTelemetryAverager.prototype.setRangeKeyAndFormatter = function () {\n  const metadatas = this.telemetryAPI.getMetadata(this.domainObject);\n  const rangeValues = metadatas.valuesForHints(['range']);\n\n  this.rangeKey = rangeValues[0].key;\n  this.rangeFormatter = this.getFormatter(this.rangeKey);\n};\n\n/**\n * @private\n */\nTelemetryAverager.prototype.getFormatter = function (key) {\n  const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject);\n  const valueMetadata = objectMetadata.value(key);\n\n  return this.telemetryAPI.getValueFormatter(valueMetadata);\n};\n"
  },
  {
    "path": "src/plugins/telemetryTable/TableConfigurationViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport TableConfigurationComponent from './components/TableConfiguration.vue';\nimport TelemetryTableConfiguration from './TelemetryTableConfiguration.js';\n\nexport default function TableConfigurationViewProvider(openmct, options) {\n  return {\n    key: 'table-configuration',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length !== 1 || selection[0].length === 0) {\n        return false;\n      }\n\n      let object = selection[0][0].context.item;\n\n      return object && object.type === 'table';\n    },\n    view: function (selection) {\n      let _destroy = null;\n      let tableConfiguration;\n      const domainObject = selection[0][0].context.item;\n\n      return {\n        show: function (element) {\n          tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct, options);\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                TableConfiguration: TableConfigurationComponent\n              },\n              provide: {\n                openmct,\n                tableConfiguration\n              },\n              template: '<table-configuration></table-configuration>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        showTab: function (isEditing) {\n          return true;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n\n          tableConfiguration = undefined;\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTable.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport StalenessUtils from '../../utils/staleness.js';\nimport TableRowCollection from './collections/TableRowCollection.js';\nimport { MODE } from './constants.js';\nimport TelemetryTableColumn from './TelemetryTableColumn.js';\nimport TelemetryTableConfiguration from './TelemetryTableConfiguration.js';\nimport TelemetryTableNameColumn from './TelemetryTableNameColumn.js';\nimport TelemetryTableRow from './TelemetryTableRow.js';\nimport TelemetryTableUnitColumn from './TelemetryTableUnitColumn.js';\n\nexport default class TelemetryTable extends EventEmitter {\n  constructor(domainObject, openmct, options) {\n    super();\n\n    this.domainObject = domainObject;\n    this.openmct = openmct;\n    this.tableComposition = undefined;\n    this.datumCache = [];\n    this.configuration = new TelemetryTableConfiguration(domainObject, openmct, options);\n    this.telemetryMode = this.configuration.getTelemetryMode();\n    this.rowLimit = this.configuration.getRowLimit();\n    this.paused = false;\n    this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n    this.telemetryObjects = {};\n    this.subscribedStaleObjects = new Map();\n    this.telemetryCollections = {};\n    this.delayedActions = [];\n    this.outstandingRequests = 0;\n    this.stalenessSubscription = {};\n\n    this.addTelemetryObject = this.addTelemetryObject.bind(this);\n    this.removeTelemetryObject = this.removeTelemetryObject.bind(this);\n    this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);\n    this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this);\n    this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this);\n    this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);\n    this.isTelemetryObject = this.isTelemetryObject.bind(this);\n    this.updateFilters = this.updateFilters.bind(this);\n    this.clearData = this.clearData.bind(this);\n    this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);\n\n    this.filterObserver = undefined;\n\n    this.createTableRowCollections();\n    this.resubscribeToStaleness = this.resubscribeAllObjectsToStaleness.bind(this);\n    this.openmct.time.on('clockChanged', this.resubscribeToStaleness);\n  }\n\n  /**\n   * @private\n   */\n  addNameColumn(telemetryObject, metadataValues) {\n    let metadatum = metadataValues.find((m) => m.key === 'name');\n    if (!metadatum) {\n      metadatum = {\n        format: 'string',\n        key: 'name',\n        name: 'Name'\n      };\n    }\n\n    const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum);\n\n    this.configuration.addSingleColumnForObject(telemetryObject, column);\n  }\n\n  initialize() {\n    if (this.domainObject.type === 'table') {\n      this.filterObserver = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.filters',\n        this.updateFilters\n      );\n      this.filters = this.domainObject.configuration.filters;\n      this.loadComposition();\n    } else {\n      this.addTelemetryObject(this.domainObject);\n    }\n  }\n\n  updateTelemetryMode(mode) {\n    if (this.telemetryMode === mode) {\n      return;\n    }\n\n    this.telemetryMode = mode;\n\n    this.updateRowLimit();\n\n    this.clearAndResubscribe();\n  }\n\n  updateRowLimit(rowLimit) {\n    if (rowLimit) {\n      this.rowLimit = rowLimit;\n    }\n\n    if (this.telemetryMode === MODE.PERFORMANCE) {\n      this.tableRows.setLimit(this.rowLimit);\n    } else {\n      this.tableRows.removeLimit();\n    }\n  }\n\n  createTableRowCollections() {\n    this.tableRows = new TableRowCollection();\n\n    const sortOptions = this.configuration.getSortOptions();\n\n    this.updateRowLimit();\n\n    this.tableRows.sortBy(sortOptions);\n    this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);\n  }\n\n  loadComposition() {\n    this.tableComposition = this.openmct.composition.get(this.domainObject);\n\n    if (this.tableComposition !== undefined) {\n      this.tableComposition.load().then((composition) => {\n        composition = composition.filter(this.isTelemetryObject);\n        composition.forEach(this.addTelemetryObject);\n\n        this.tableComposition.on('add', this.addTelemetryObject);\n        this.tableComposition.on('remove', this.removeTelemetryObject);\n      });\n    }\n  }\n\n  addTelemetryObject(telemetryObject) {\n    this.addColumnsForObject(telemetryObject, true);\n\n    const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n    let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);\n    let columnMap = this.getColumnMapForObject(keyString);\n    let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);\n\n    const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);\n    const telemetryRemover = this.getTelemetryRemover();\n\n    this.removeTelemetryCollection(keyString);\n\n    let sortOptions = this.configuration.getSortOptions();\n    requestOptions.order = sortOptions.direction;\n\n    if (this.telemetryMode === MODE.PERFORMANCE) {\n      requestOptions.size = this.rowLimit;\n      requestOptions.enforceSize = true;\n    }\n\n    this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(\n      telemetryObject,\n      requestOptions\n    );\n\n    this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests);\n    this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests);\n    this.telemetryCollections[keyString].on('remove', telemetryRemover);\n    this.telemetryCollections[keyString].on('add', telemetryProcessor);\n    this.telemetryCollections[keyString].on('clear', this.clearData);\n    this.telemetryCollections[keyString].load();\n\n    this.subscribeToStaleness(telemetryObject);\n\n    this.telemetryObjects[keyString] = {\n      telemetryObject,\n      keyString,\n      requestOptions,\n      columnMap,\n      limitEvaluator\n    };\n\n    this.emit('object-added', telemetryObject);\n  }\n\n  resubscribeAllObjectsToStaleness() {\n    if (!this.subscribedStaleObjects || this.subscribedStaleObjects.size < 1) {\n      return;\n    }\n    for (const [, telemetryObject] of this.subscribedStaleObjects) {\n      this.subscribeToStaleness(telemetryObject);\n    }\n  }\n\n  subscribeToStaleness(domainObject) {\n    const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n    if (this.stalenessSubscription?.[keyString]) {\n      this.unsubscribeFromStaleness(domainObject.identifier);\n    }\n\n    this.stalenessSubscription[keyString] = {};\n    this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(\n      this.openmct,\n      domainObject\n    );\n    this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {\n      if (stalenessResponse !== undefined) {\n        this.handleStaleness(keyString, stalenessResponse);\n      }\n    });\n    const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(\n      domainObject,\n      (stalenessResponse) => {\n        this.handleStaleness(keyString, stalenessResponse);\n      }\n    );\n    this.subscribedStaleObjects.set(keyString, domainObject);\n\n    this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;\n  }\n\n  handleStaleness(keyString, stalenessResponse, skipCheck = false) {\n    if (\n      skipCheck ||\n      this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(\n        stalenessResponse,\n        keyString\n      )\n    ) {\n      this.emit('telemetry-staleness', {\n        keyString,\n        stalenessResponse: stalenessResponse\n      });\n    }\n  }\n\n  getTelemetryProcessor(keyString, columnMap, limitEvaluator) {\n    return (telemetry) => {\n      //Check that telemetry object has not been removed since telemetry was requested.\n      if (!this.telemetryObjects[keyString]) {\n        return;\n      }\n\n      const metadataValue = this.openmct.telemetry\n        .getMetadata(this.telemetryObjects[keyString].telemetryObject)\n        .getUseToUpdateInPlaceValue();\n\n      let telemetryRows = telemetry.map(\n        (datum) =>\n          new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)\n      );\n\n      if (this.paused) {\n        this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));\n      } else {\n        this.tableRows.addRows(telemetryRows);\n      }\n    };\n  }\n\n  getTelemetryRemover() {\n    return (telemetry) => {\n      if (this.paused) {\n        this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));\n      } else {\n        this.tableRows.removeRowsByData(telemetry);\n      }\n    };\n  }\n\n  /**\n   * @private\n   */\n  incrementOutstandingRequests() {\n    if (this.outstandingRequests === 0) {\n      this.emit('outstanding-requests', true);\n    }\n\n    this.outstandingRequests++;\n  }\n\n  /**\n   * @private\n   */\n  decrementOutstandingRequests() {\n    this.outstandingRequests--;\n\n    if (this.outstandingRequests === 0) {\n      this.emit('outstanding-requests', false);\n    }\n  }\n\n  // will pull all necessary information for all existing bounded telemetry\n  // and pass to table row collection to reset without making any new requests\n  // triggered by filtering\n  resetRowsFromAllData() {\n    let allRows = [];\n\n    Object.keys(this.telemetryCollections).forEach((keyString) => {\n      let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];\n\n      const metadataValue = this.openmct.telemetry\n        .getMetadata(this.telemetryObjects[keyString].telemetryObject)\n        .getUseToUpdateInPlaceValue();\n\n      this.telemetryCollections[keyString].getAll().forEach((datum) => {\n        allRows.push(\n          new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)\n        );\n      });\n    });\n\n    this.tableRows.clearRowsFromTableAndFilter(allRows);\n  }\n\n  updateFilters(updatedFilters) {\n    let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));\n\n    if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {\n      this.filters = deepCopiedFilters;\n      this.tableRows.clear();\n      this.clearAndResubscribe();\n    } else {\n      this.filters = deepCopiedFilters;\n    }\n  }\n\n  clearAndResubscribe() {\n    let objectKeys = Object.keys(this.telemetryObjects);\n\n    this.tableRows.clear();\n    objectKeys.forEach((keyString) => {\n      this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject);\n    });\n  }\n\n  removeTelemetryObject(objectIdentifier) {\n    const keyString = this.openmct.objects.makeKeyString(objectIdentifier);\n\n    this.configuration.removeColumnsForObject(objectIdentifier, true);\n    this.tableRows.removeRowsByObject(keyString);\n\n    this.removeTelemetryCollection(keyString);\n    delete this.telemetryObjects[keyString];\n\n    this.emit('object-removed', objectIdentifier);\n\n    this.unsubscribeFromStaleness(objectIdentifier);\n  }\n\n  unsubscribeFromStaleness(objectIdentifier) {\n    const keyString = this.openmct.objects.makeKeyString(objectIdentifier);\n    const SKIP_CHECK = true;\n\n    this.stalenessSubscription[keyString].unsubscribe();\n    this.stalenessSubscription[keyString].stalenessUtils.destroy();\n    this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);\n    delete this.stalenessSubscription[keyString];\n  }\n\n  clearData() {\n    this.tableRows.clear();\n    this.emit('refresh');\n  }\n\n  addColumnsForObject(telemetryObject) {\n    const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n    let metadataValues = metadata.values();\n\n    this.addNameColumn(telemetryObject, metadataValues);\n    metadataValues.forEach((metadatum) => {\n      if (metadatum.key === 'name' || metadata.isInPlaceUpdateValue(metadatum)) {\n        return;\n      }\n\n      let column = this.createColumn(metadatum);\n      this.configuration.addSingleColumnForObject(telemetryObject, column);\n      // add units column if available\n      if (metadatum.unit !== undefined) {\n        let unitColumn = this.createUnitColumn(metadatum);\n        this.configuration.addSingleColumnForObject(telemetryObject, unitColumn);\n      }\n    });\n  }\n\n  getColumnMapForObject(objectKeyString) {\n    let columns = this.configuration.getColumns();\n\n    if (columns[objectKeyString]) {\n      return columns[objectKeyString].reduce((map, column) => {\n        map[column.getKey()] = column;\n\n        return map;\n      }, {});\n    }\n\n    return {};\n  }\n\n  buildOptionsFromConfiguration(telemetryObject) {\n    let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n    let filters =\n      this.domainObject.configuration &&\n      this.domainObject.configuration.filters &&\n      this.domainObject.configuration.filters[keyString];\n\n    return { filters } || {};\n  }\n\n  createColumn(metadatum) {\n    return new TelemetryTableColumn(this.openmct, metadatum);\n  }\n\n  createUnitColumn(metadatum) {\n    return new TelemetryTableUnitColumn(this.openmct, metadatum);\n  }\n\n  isTelemetryObject(domainObject) {\n    return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');\n  }\n\n  sortBy(sortOptions) {\n    this.configuration.setSortOptions(sortOptions);\n\n    if (this.telemetryMode === MODE.PERFORMANCE) {\n      this.tableRows.setSortOptions(sortOptions);\n      this.clearAndResubscribe();\n    } else {\n      this.tableRows.sortBy(sortOptions);\n    }\n  }\n\n  runDelayedActions() {\n    this.delayedActions.forEach((action) => action());\n    this.delayedActions = [];\n  }\n\n  removeTelemetryCollection(keyString) {\n    if (this.telemetryCollections[keyString]) {\n      this.telemetryCollections[keyString].destroy();\n      this.telemetryCollections[keyString] = undefined;\n      delete this.telemetryCollections[keyString];\n    }\n  }\n\n  pause() {\n    this.paused = true;\n  }\n\n  unpause() {\n    this.paused = false;\n    this.runDelayedActions();\n  }\n\n  destroy() {\n    this.tableRows.destroy();\n\n    this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);\n    this.openmct.time.off('clockChanged', this.resubscribeToStaleness);\n\n    let keystrings = Object.keys(this.telemetryCollections);\n    keystrings.forEach(this.removeTelemetryCollection);\n\n    if (this.filterObserver) {\n      this.filterObserver();\n    }\n\n    Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {\n      stalenessSubscription.unsubscribe();\n      stalenessSubscription.stalenessUtils.destroy();\n    });\n\n    if (this.tableComposition !== undefined) {\n      this.tableComposition.off('add', this.addTelemetryObject);\n      this.tableComposition.off('remove', this.removeTelemetryObject);\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableColumn.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nexport default class TelemetryTableColumn {\n  constructor(openmct, metadatum, options = { selectable: false }) {\n    this.metadatum = metadatum;\n    this.formatter = openmct.telemetry.getValueFormatter(metadatum);\n    this.titleValue = this.metadatum.name;\n    this.selectable = options.selectable;\n  }\n\n  getKey() {\n    return this.metadatum.key;\n  }\n\n  getTitle() {\n    return this.metadatum.name;\n  }\n\n  getMetadatum() {\n    return this.metadatum;\n  }\n\n  hasValueForDatum(telemetryDatum) {\n    return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source);\n  }\n\n  getRawValue(telemetryDatum) {\n    return telemetryDatum[this.metadatum.source];\n  }\n\n  getFormattedValue(telemetryDatum) {\n    let formattedValue = this.formatter.format(telemetryDatum);\n    if (formattedValue !== undefined && typeof formattedValue !== 'string') {\n      return formattedValue.toString();\n    } else {\n      return formattedValue;\n    }\n  }\n\n  getParsedValue(telemetryDatum) {\n    return this.formatter.parse(telemetryDatum);\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableConfiguration.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport { ORDER } from './constants';\n\nexport default class TelemetryTableConfiguration extends EventEmitter {\n  #sortOptions;\n\n  constructor(domainObject, openmct, options) {\n    super();\n\n    this.domainObject = domainObject;\n    this.openmct = openmct;\n    this.defaultOptions = options;\n    this.columns = {};\n\n    this.removeColumnsForObject = this.removeColumnsForObject.bind(this);\n    this.objectMutated = this.objectMutated.bind(this);\n\n    this.unlistenFromMutation = openmct.objects.observe(\n      domainObject,\n      'configuration',\n      this.objectMutated\n    );\n\n    this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);\n  }\n\n  getSortOptions() {\n    return (\n      this.#sortOptions ||\n      this.getConfiguration().sortOptions || {\n        key: this.openmct.time.getTimeSystem().key,\n        direction: ORDER.DESCENDING\n      }\n    );\n  }\n\n  setSortOptions(sortOptions) {\n    this.#sortOptions = sortOptions;\n\n    if (this.openmct.editor.isEditing()) {\n      let configuration = this.getConfiguration();\n      configuration.sortOptions = sortOptions;\n      this.updateConfiguration(configuration);\n    }\n  }\n\n  getConfiguration() {\n    let configuration = this.domainObject.configuration || {};\n    configuration.hiddenColumns = configuration.hiddenColumns || {};\n    configuration.columnWidths = configuration.columnWidths || {};\n    configuration.columnOrder = configuration.columnOrder || [];\n    configuration.cellFormat = configuration.cellFormat || {};\n    configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;\n    // anything that doesn't have a telemetryMode existed before the change and should\n    // take the properties of any passed in defaults or the defaults from the plugin\n    configuration.telemetryMode = configuration.telemetryMode ?? this.defaultOptions.telemetryMode;\n    configuration.persistModeChange = this.notPersistable\n      ? false\n      : configuration.persistModeChange ?? this.defaultOptions.persistModeChange;\n    configuration.rowLimit = configuration.rowLimit ?? this.defaultOptions.rowLimit;\n\n    return configuration;\n  }\n\n  updateConfiguration(configuration) {\n    if (this.notPersistable) {\n      return;\n    }\n\n    this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);\n  }\n\n  /**\n   * @private\n   * @param {*} object\n   */\n  objectMutated(configuration) {\n    if (configuration !== undefined) {\n      this.emit('change', configuration);\n    }\n  }\n\n  addSingleColumnForObject(telemetryObject, column, position) {\n    let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n    this.columns[objectKeyString] = this.columns[objectKeyString] || [];\n    position = position || this.columns[objectKeyString].length;\n    this.columns[objectKeyString].splice(position, 0, column);\n  }\n\n  removeColumnsForObject(objectIdentifier) {\n    let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);\n    let columnsToRemove = this.columns[objectKeyString];\n\n    delete this.columns[objectKeyString];\n\n    let configuration = this.domainObject.configuration;\n    let configurationChanged = false;\n    columnsToRemove.forEach((column) => {\n      //There may be more than one column with the same key (eg. time system columns)\n      if (!this.hasColumnWithKey(column.getKey())) {\n        delete configuration.hiddenColumns[column.getKey()];\n        configurationChanged = true;\n      }\n    });\n    if (configurationChanged) {\n      this.updateConfiguration(configuration);\n    }\n  }\n\n  hasColumnWithKey(columnKey) {\n    return _.flatten(Object.values(this.columns)).some((column) => column.getKey() === columnKey);\n  }\n\n  getColumns() {\n    return this.columns;\n  }\n\n  getAllHeaders() {\n    let flattenedColumns = _.flatten(Object.values(this.columns));\n    /* eslint-disable you-dont-need-lodash-underscore/uniq */\n    let headers = _.uniq(flattenedColumns, false, (column) => column.getKey()).reduce(\n      fromColumnsToHeadersMap,\n      {}\n    );\n    /* eslint-enable you-dont-need-lodash-underscore/uniq */\n    function fromColumnsToHeadersMap(headersMap, column) {\n      headersMap[column.getKey()] = column.getTitle();\n\n      return headersMap;\n    }\n\n    return headers;\n  }\n\n  getVisibleHeaders() {\n    let allHeaders = this.getAllHeaders();\n    let configuration = this.getConfiguration();\n\n    let orderedColumns = this.getColumnOrder();\n    let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns);\n\n    return orderedColumns\n      .concat(unorderedColumns)\n      .filter((headerKey) => {\n        return configuration.hiddenColumns[headerKey] !== true;\n      })\n      .reduce((headers, headerKey) => {\n        headers[headerKey] = allHeaders[headerKey];\n\n        return headers;\n      }, {});\n  }\n\n  getTelemetryMode() {\n    let configuration = this.getConfiguration();\n\n    return configuration.telemetryMode;\n  }\n\n  setTelemetryMode(mode) {\n    let configuration = this.getConfiguration();\n    configuration.telemetryMode = mode;\n    this.updateConfiguration(configuration);\n  }\n\n  getRowLimit() {\n    let configuration = this.getConfiguration();\n\n    return configuration.rowLimit;\n  }\n\n  setRowLimit(limit) {\n    let configuration = this.getConfiguration();\n    configuration.rowLimit = limit;\n    this.updateConfiguration(configuration);\n  }\n\n  getPersistModeChange() {\n    let configuration = this.getConfiguration();\n\n    return configuration.persistModeChange;\n  }\n\n  setPersistModeChange(value) {\n    let configuration = this.getConfiguration();\n    configuration.persistModeChange = value;\n    this.updateConfiguration(configuration);\n  }\n\n  getColumnWidths() {\n    let configuration = this.getConfiguration();\n\n    return configuration.columnWidths;\n  }\n\n  setColumnWidths(columnWidths) {\n    let configuration = this.getConfiguration();\n    configuration.columnWidths = columnWidths;\n    this.updateConfiguration(configuration);\n  }\n\n  getColumnOrder() {\n    let configuration = this.getConfiguration();\n\n    return configuration.columnOrder;\n  }\n\n  setColumnOrder(columnOrder) {\n    let configuration = this.getConfiguration();\n    configuration.columnOrder = columnOrder;\n    this.updateConfiguration(configuration);\n  }\n\n  destroy() {\n    this.unlistenFromMutation();\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableNameColumn.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport TelemetryTableColumn from './TelemetryTableColumn.js';\n\nexport default class TelemetryTableNameColumn extends TelemetryTableColumn {\n  constructor(openmct, telemetryObject, metadatum) {\n    super(openmct, metadatum);\n\n    this.telemetryObject = telemetryObject;\n  }\n\n  getRawValue() {\n    return this.telemetryObject.name;\n  }\n\n  getFormattedValue() {\n    return this.telemetryObject.name;\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableRow.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { VIEW_DATUM_ACTION_KEY } from '@/plugins/viewDatumAction/ViewDatumAction.js';\nimport { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';\n\nexport default class TelemetryTableRow {\n  constructor(datum, columns, objectKeyString, limitEvaluator, inPlaceUpdateKey) {\n    this.columns = columns;\n\n    this.datum = createNormalizedDatum(datum, columns);\n    this.fullDatum = datum;\n    this.limitEvaluator = limitEvaluator;\n    this.objectKeyString = objectKeyString;\n    this.inPlaceUpdateKey = inPlaceUpdateKey;\n  }\n\n  getFormattedDatum(headers) {\n    return Object.keys(headers).reduce((formattedDatum, columnKey) => {\n      formattedDatum[columnKey] = this.getFormattedValue(columnKey);\n\n      return formattedDatum;\n    }, {});\n  }\n\n  getFormattedValue(key) {\n    let column = this.columns[key];\n\n    return column && column.getFormattedValue(this.datum[key]);\n  }\n\n  getParsedValue(key) {\n    let column = this.columns[key];\n\n    return column && column.getParsedValue(this.datum[key]);\n  }\n\n  getCellComponentName(key) {\n    let column = this.columns[key];\n\n    return column && column.getCellComponentName && column.getCellComponentName();\n  }\n\n  getRowClass() {\n    if (!this.rowClass) {\n      let limitEvaluation = this.limitEvaluator.evaluate(this.datum);\n      this.rowClass = limitEvaluation && limitEvaluation.cssClass;\n    }\n\n    return this.rowClass;\n  }\n\n  getCellLimitClasses() {\n    if (!this.cellLimitClasses) {\n      this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {\n        if (!column.isUnit) {\n          let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());\n          alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;\n        }\n\n        return alarmStateMap;\n      }, {});\n    }\n\n    return this.cellLimitClasses;\n  }\n\n  getContextualDomainObject(openmct, objectKeyString) {\n    return openmct.objects.get(objectKeyString);\n  }\n\n  getContextMenuActions() {\n    return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];\n  }\n\n  /**\n   * Merges the row parameter's datum with the current row datum\n   * @param {TelemetryTableRow} row\n   */\n  updateWithDatum(row) {\n    this.datum = {\n      ...this.datum,\n      ...row.datum\n    };\n\n    this.fullDatum = {\n      ...this.fullDatum,\n      ...row.fullDatum\n    };\n  }\n}\n\n/**\n * Normalize the structure of datums to assist sorting and merging of columns.\n * Maps all sources to keys.\n * @private\n * @param {*} telemetryDatum\n * @param {*} metadataValues\n */\nfunction createNormalizedDatum(datum, columns) {\n  const normalizedDatum = JSON.parse(JSON.stringify(datum));\n\n  Object.values(columns).forEach((column) => {\n    const rawValue = column.getRawValue(datum);\n    if (rawValue !== undefined) {\n      normalizedDatum[column.getKey()] = rawValue;\n    }\n  });\n\n  return normalizedDatum;\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableType.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { MODE } from './constants.js';\n\nexport default function getTelemetryTableType(options) {\n  let { telemetryMode, persistModeChange, rowLimit } = options;\n\n  return {\n    name: 'Telemetry Table',\n    description:\n      'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',\n    creatable: true,\n    cssClass: 'icon-tabular-scrolling',\n    form: [\n      {\n        key: 'telemetryMode',\n        name: 'Data Mode',\n        control: 'select',\n        options: [\n          {\n            value: MODE.PERFORMANCE,\n            name: 'Limited (Performance) Mode'\n          },\n          {\n            value: MODE.UNLIMITED,\n            name: 'Unlimited Mode'\n          }\n        ],\n        cssClass: 'l-inline',\n        property: ['configuration', 'telemetryMode']\n      },\n      {\n        name: 'Persist Data Mode Changes',\n        control: 'toggleSwitch',\n        cssClass: 'l-input',\n        key: 'persistModeChange',\n        property: ['configuration', 'persistModeChange']\n      },\n      {\n        name: 'Limited Data Mode Row Limit',\n        control: 'numberfield',\n        cssClass: 'l-input',\n        key: 'rowLimit',\n        property: ['configuration', 'rowLimit']\n      }\n    ],\n    initialize(domainObject) {\n      domainObject.composition = [];\n      domainObject.configuration = {\n        columnWidths: {},\n        hiddenColumns: {},\n        telemetryMode,\n        persistModeChange,\n        rowLimit,\n        objectStyles: {}\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableUnitColumn.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport TelemetryTableColumn from './TelemetryTableColumn.js';\n\nclass TelemetryTableUnitColumn extends TelemetryTableColumn {\n  constructor(openmct, metadatum) {\n    super(openmct, metadatum);\n    this.isUnit = true;\n    this.titleValue += ' Unit';\n    this.formatter = {\n      format: (datum) => {\n        return this.metadatum.unit;\n      },\n      parse: (datum) => {\n        return this.metadatum.unit;\n      }\n    };\n  }\n\n  getKey() {\n    return this.metadatum.key + '-unit';\n  }\n\n  getTitle() {\n    return this.metadatum.name + ' Unit';\n  }\n\n  getRawValue(telemetryDatum) {\n    return this.metadatum.unit;\n  }\n\n  getFormattedValue(telemetryDatum) {\n    return this.formatter.format(telemetryDatum);\n  }\n}\n\nexport default TelemetryTableUnitColumn;\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableView.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport mount from 'utils/mount';\n\nimport TableComponent from './components/TableComponent.vue';\nimport TelemetryTable from './TelemetryTable.js';\n\nexport default class TelemetryTableView {\n  constructor(openmct, domainObject, objectPath, options) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.objectPath = objectPath;\n    this._destroy = null;\n    this.component = null;\n\n    Object.defineProperty(this, 'table', {\n      value: new TelemetryTable(domainObject, openmct, options),\n      enumerable: false,\n      configurable: false\n    });\n  }\n\n  getViewContext() {\n    if (!this.component) {\n      return {};\n    }\n\n    return this.component.$refs.tableComponent.getViewContext();\n  }\n\n  onEditModeChange(editMode) {\n    this.component.isEditing = editMode;\n  }\n\n  onClearData() {\n    this.table.clearData();\n  }\n\n  getTable() {\n    return this.table;\n  }\n\n  destroy() {\n    if (this._destroy) {\n      this._destroy();\n    }\n    this.component = null;\n  }\n\n  show(element, editMode, { renderWhenVisible }) {\n    const { vNode, destroy } = mount(\n      {\n        el: element,\n        components: {\n          TableComponent\n        },\n        provide: {\n          openmct: this.openmct,\n          objectPath: this.objectPath,\n          table: this.table,\n          currentView: this,\n          renderWhenVisible\n        },\n        data() {\n          return {\n            isEditing: editMode,\n            marking: {\n              disableMultiSelect: false,\n              enable: true,\n              rowName: '',\n              rowNamePlural: '',\n              useAlternateControlBar: false\n            }\n          };\n        },\n        template:\n          '<table-component ref=\"tableComponent\" :is-editing=\"isEditing\" :marking=\"marking\"></table-component>'\n      },\n      {\n        app: this.openmct.app,\n        element\n      }\n    );\n    this.component = vNode.componentInstance;\n    this._destroy = destroy;\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/TelemetryTableViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport TelemetryTableView from './TelemetryTableView.js';\n\nexport default function TelemetryTableViewProvider(openmct, options) {\n  function hasTelemetry(domainObject) {\n    if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {\n      return false;\n    }\n\n    let metadata = openmct.telemetry.getMetadata(domainObject);\n\n    return metadata.values().length > 0;\n  }\n\n  return {\n    key: 'table',\n    name: 'Telemetry Table',\n    cssClass: 'icon-tabular-scrolling',\n    canView(domainObject) {\n      return domainObject.type === 'table' || hasTelemetry(domainObject);\n    },\n    canEdit(domainObject) {\n      return domainObject.type === 'table';\n    },\n    view(domainObject, objectPath) {\n      return new TelemetryTableView(openmct, domainObject, objectPath, options);\n    },\n    priority() {\n      return openmct.priority.LOW;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/ViewActions.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst exportCSV = {\n  name: 'Export Table Data',\n  key: 'export-csv-all',\n  description: \"Export this view's data\",\n  cssClass: 'icon-download labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().exportAllDataAsCSV();\n  },\n  group: 'view'\n};\n\nconst exportMarkedDataAsCSV = {\n  name: 'Export Marked Rows',\n  key: 'export-csv-marked',\n  description: 'Export marked rows as CSV',\n  cssClass: 'icon-download labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().exportMarkedDataAsCSV();\n  },\n  group: 'view'\n};\n\nconst unmarkAllRows = {\n  name: 'Unmark All Rows',\n  key: 'unmark-all-rows',\n  description: 'Unmark all rows',\n  cssClass: 'icon-x labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().unmarkAllRows();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst pause = {\n  name: 'Pause',\n  key: 'pause-data',\n  description: 'Pause real-time data flow',\n  cssClass: 'icon-pause',\n  invoke: (objectPath, view) => {\n    view.getViewContext().togglePauseByButton();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst play = {\n  name: 'Play',\n  key: 'play-data',\n  description: 'Continue real-time data flow',\n  cssClass: 'c-button pause-play is-paused',\n  invoke: (objectPath, view) => {\n    view.getViewContext().togglePauseByButton();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst expandColumns = {\n  name: 'Expand Columns',\n  key: 'expand-columns',\n  description: 'Increase column widths to fit currently available data.',\n  cssClass: 'icon-arrows-right-left labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().expandColumns();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst autosizeColumns = {\n  name: 'Autosize Columns',\n  key: 'autosize-columns',\n  description: 'Automatically size columns to fit the table into the available space.',\n  cssClass: 'icon-expand labeled',\n  invoke: (objectPath, view) => {\n    view.getViewContext().autosizeColumns();\n  },\n  showInStatusBar: true,\n  group: 'view'\n};\n\nconst viewActions = [\n  exportCSV,\n  exportMarkedDataAsCSV,\n  unmarkAllRows,\n  pause,\n  play,\n  expandColumns,\n  autosizeColumns\n];\n\nviewActions.forEach((action) => {\n  action.appliesTo = (objectPath, view = {}) => {\n    const viewContext = view.getViewContext && view.getViewContext();\n    if (!viewContext) {\n      return false;\n    }\n\n    return viewContext.type === 'telemetry-table';\n  };\n});\n\nexport default viewActions;\n"
  },
  {
    "path": "src/plugins/telemetryTable/collections/TableRowCollection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\nimport { ORDER } from '../constants.js';\n\n/**\n * @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow\n */\n\n/**\n * @constructor\n */\nexport default class TableRowCollection extends EventEmitter {\n  constructor() {\n    super();\n\n    this.rows = [];\n    this.columnFilters = {};\n    this.addRows = this.addRows.bind(this);\n    this.removeRowsByObject = this.removeRowsByObject.bind(this);\n    this.removeRowsByData = this.removeRowsByData.bind(this);\n\n    this.clear = this.clear.bind(this);\n  }\n\n  removeRowsByObject(keyString) {\n    let removed = [];\n\n    this.rows = this.rows.filter((row) => {\n      if (row.objectKeyString === keyString) {\n        removed.push(row);\n\n        return false;\n      } else {\n        return true;\n      }\n    });\n\n    this.emit('remove', removed);\n  }\n\n  addRows(rows) {\n    let rowsToAdd = this.filterRows(rows);\n\n    this.sortAndMergeRows(rowsToAdd);\n\n    // we emit filter no matter what to trigger\n    // an update of visible rows\n    if (rowsToAdd.length > 0) {\n      this.emit('add', rowsToAdd);\n    }\n  }\n\n  clearRowsFromTableAndFilter(rows) {\n    let rowsToAdd = this.filterRows(rows);\n    // Reset of all rows, need to wipe current rows\n    this.rows = [];\n\n    this.sortAndMergeRows(rowsToAdd);\n\n    // We emit filter and update of visible rows\n    this.emit('filter', rowsToAdd);\n  }\n\n  filterRows(rows) {\n    if (Object.keys(this.columnFilters).length > 0) {\n      return rows.filter(this.matchesFilters, this);\n    }\n\n    return rows;\n  }\n\n  sortAndMergeRows(rows) {\n    const sortedRows = this.sortCollection(rows);\n\n    if (this.rows.length === 0) {\n      this.rows = sortedRows;\n\n      return;\n    }\n\n    const firstIncomingRow = sortedRows[0];\n    const lastIncomingRow = sortedRows[sortedRows.length - 1];\n    const firstExistingRow = this.rows[0];\n    const lastExistingRow = this.rows[this.rows.length - 1];\n\n    if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) === lastIncomingRow) {\n      this.insertOrUpdateRows(sortedRows, true);\n    } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) === lastExistingRow) {\n      this.insertOrUpdateRows(sortedRows, false);\n    } else {\n      this.mergeSortedRows(sortedRows);\n    }\n  }\n\n  getInPlaceUpdateIndex(row) {\n    const inPlaceUpdateKey = row.inPlaceUpdateKey;\n    if (!inPlaceUpdateKey) {\n      return -1;\n    }\n\n    const foundIndex = this.rows.findIndex(\n      (existingRow) =>\n        existingRow.datum[inPlaceUpdateKey] &&\n        existingRow.datum[inPlaceUpdateKey] === row.datum[inPlaceUpdateKey]\n    );\n\n    return foundIndex;\n  }\n\n  /**\n   * `incomingRow` exists in the collection,\n   * so merge existing and incoming row properties\n   *\n   * Do to reactivity of Vue, we want to replace the existing row with the updated row\n   * @param {TelemetryTableRow} incomingRow to update\n   * @param {number} index of the existing row in the collection to update\n   */\n  updateRowInPlace(incomingRow, index) {\n    // Update the incoming row, not the existing row\n    const existingRow = this.rows[index];\n    incomingRow.updateWithDatum(existingRow);\n\n    // Replacing the existing row with the updated, incoming row will trigger Vue reactivity\n    // because the reference to the row has changed\n    this.rows.splice(index, 1, incomingRow);\n  }\n\n  setLimit(rowLimit) {\n    this.rowLimit = rowLimit;\n  }\n\n  removeLimit() {\n    this.rowLimit = null;\n    delete this.rowLimit;\n  }\n\n  sortCollection(rows) {\n    const sortedRows = _.orderBy(\n      rows,\n      (row) => row.getParsedValue(this.sortOptions.key),\n      this.sortOptions.direction\n    );\n\n    return sortedRows;\n  }\n\n  insertOrUpdateRows(rowsToAdd, addToBeginning) {\n    rowsToAdd.forEach((row, addRowsIndex) => {\n      const index = this.getInPlaceUpdateIndex(row);\n      if (index > -1) {\n        this.updateRowInPlace(row, index);\n      } else {\n        if (addToBeginning) {\n          this.rows.splice(addRowsIndex, 0, row);\n        } else {\n          this.rows.push(row);\n        }\n      }\n    });\n  }\n\n  mergeSortedRows(incomingRows) {\n    const mergedRows = [];\n    let existingRowIndex = 0;\n    let incomingRowIndex = 0;\n\n    while (existingRowIndex < this.rows.length && incomingRowIndex < incomingRows.length) {\n      const existingRow = this.rows[existingRowIndex];\n      const incomingRow = incomingRows[incomingRowIndex];\n\n      const inPlaceIndex = this.getInPlaceUpdateIndex(incomingRow);\n      if (inPlaceIndex > -1) {\n        this.updateRowInPlace(incomingRow, inPlaceIndex);\n        incomingRowIndex++;\n      } else {\n        if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {\n          mergedRows.push(existingRow);\n          existingRowIndex++;\n        } else {\n          mergedRows.push(incomingRow);\n          incomingRowIndex++;\n        }\n      }\n    }\n\n    // tail of existing rows is all that is left to merge\n    if (existingRowIndex < this.rows.length) {\n      for (existingRowIndex; existingRowIndex < this.rows.length; existingRowIndex++) {\n        mergedRows.push(this.rows[existingRowIndex]);\n      }\n    }\n\n    // tail of incoming rows is all that is left to merge\n    if (incomingRowIndex < incomingRows.length) {\n      for (incomingRowIndex; incomingRowIndex < incomingRows.length; incomingRowIndex++) {\n        mergedRows.push(incomingRows[incomingRowIndex]);\n      }\n    }\n\n    this.rows = mergedRows;\n  }\n\n  firstRowInSortOrder(row1, row2) {\n    const val1 = this.getValueForSortColumn(row1);\n    const val2 = this.getValueForSortColumn(row2);\n\n    if (this.sortOptions.direction === ORDER.ASCENDING) {\n      return val1 <= val2 ? row1 : row2;\n    } else {\n      return val1 >= val2 ? row1 : row2;\n    }\n  }\n\n  removeRowsByData(data) {\n    let removed = [];\n\n    this.rows = this.rows.filter((row) => {\n      if (data.includes(row.fullDatum)) {\n        removed.push(row);\n\n        return false;\n      } else {\n        return true;\n      }\n    });\n\n    this.emit('remove', removed);\n  }\n\n  /**\n   * Sorts the telemetry collection based on the provided sort field\n   * specifier. Subsequent inserts are sorted to maintain specified sport\n   * order.\n   *\n   * @example\n   * // First build some mock telemetry for the purpose of an example\n   * let now = Date.now();\n   * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) {\n   *     return {\n   *         // define an object property to demonstrate nested paths\n   *         timestamp: {\n   *             ms: now - value * 1000,\n   *             text:\n   *         },\n   *         value: value\n   *     }\n   * });\n   * let collection = new TelemetryCollection();\n   *\n   * collection.add(telemetry);\n   *\n   * // Sort by telemetry value\n   * collection.sortBy({\n   *  key: 'value', direction: 'asc'\n   * });\n   *\n   * // Sort by ms since epoch\n   * collection.sort({\n   *  key: 'timestamp.ms',\n   *  direction: 'asc'\n   * });\n   *\n   * // Sort by 'text' attribute, descending\n   * collection.sort(\"timestamp.text\");\n   *\n   *\n   * @param {Object} sortOptions An object specifying a sort key, and direction.\n   */\n  sortBy(sortOptions) {\n    if (arguments.length > 0) {\n      this.setSortOptions(sortOptions);\n      this.rows = _.orderBy(\n        this.rows,\n        (row) => row.getParsedValue(sortOptions.key),\n        sortOptions.direction\n      );\n      this.emit('sort');\n    }\n\n    // Return duplicate to avoid direct modification of underlying object\n    return Object.assign({}, this.sortOptions);\n  }\n\n  setSortOptions(sortOptions) {\n    this.sortOptions = sortOptions;\n  }\n\n  setColumnFilter(columnKey, filter) {\n    filter = filter.trim().toLowerCase();\n    let wasBlank = this.columnFilters[columnKey] === undefined;\n    let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);\n\n    if (filter.length === 0) {\n      delete this.columnFilters[columnKey];\n    } else {\n      this.columnFilters[columnKey] = filter;\n    }\n\n    if (isSubset || wasBlank) {\n      this.rows = this.rows.filter(this.matchesFilters, this);\n      this.emit('filter');\n    } else {\n      this.emit('resetRowsFromAllData');\n    }\n  }\n\n  setColumnRegexFilter(columnKey, filter) {\n    filter = filter.trim();\n    this.columnFilters[columnKey] = new RegExp(filter);\n\n    this.emit('resetRowsFromAllData');\n  }\n\n  getColumnMapForObject(objectKeyString) {\n    let columns = this.configuration.getColumns();\n\n    if (columns[objectKeyString]) {\n      return columns[objectKeyString].reduce((map, column) => {\n        map[column.getKey()] = column;\n\n        return map;\n      }, {});\n    }\n\n    return {};\n  }\n\n  // /**\n  //  * @private\n  //  */\n  isSubsetOfCurrentFilter(columnKey, filter) {\n    if (this.columnFilters[columnKey] instanceof RegExp) {\n      return false;\n    }\n\n    return (\n      this.columnFilters[columnKey] &&\n      filter.startsWith(this.columnFilters[columnKey]) &&\n      // startsWith check will otherwise fail when filter cleared\n      // because anyString.startsWith('') === true\n      filter !== ''\n    );\n  }\n\n  /**\n   * @private\n   */\n  matchesFilters(row) {\n    let doesMatchFilters = true;\n    Object.keys(this.columnFilters).forEach((key) => {\n      if (!doesMatchFilters || !this.rowHasColumn(row, key)) {\n        return false;\n      }\n\n      let formattedValue = row.getFormattedValue(key);\n      if (formattedValue === undefined) {\n        return false;\n      }\n\n      if (this.columnFilters[key] instanceof RegExp) {\n        doesMatchFilters = this.columnFilters[key].test(formattedValue);\n      } else {\n        doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;\n      }\n    });\n\n    return doesMatchFilters;\n  }\n\n  rowHasColumn(row, key) {\n    return Object.prototype.hasOwnProperty.call(row.columns, key);\n  }\n\n  getRows() {\n    if (this.rowLimit && this.rows.length > this.rowLimit) {\n      if (this.sortOptions.direction === ORDER.DESCENDING) {\n        return this.rows.slice(0, this.rowLimit);\n      } else {\n        return this.rows.slice(-this.rowLimit);\n      }\n    }\n\n    return this.rows;\n  }\n\n  getRowsLength() {\n    if (this.rowLimit && this.rows.length > this.rowLimit) {\n      return this.rowLimit;\n    }\n\n    return this.rows.length;\n  }\n\n  getValueForSortColumn(row) {\n    return row.getParsedValue(this.sortOptions.key);\n  }\n\n  clear() {\n    let removedRows = this.rows;\n    this.rows = [];\n\n    this.emit('remove', removedRows);\n  }\n\n  destroy() {\n    this.removeAllListeners();\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/SizingRow.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <tr class=\"c-telemetry-table__sizing-tr\">\n    <td>SIZING ROW</td>\n  </tr>\n</template>\n\n<script>\nexport default {\n  props: {\n    isEditing: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['change-height'],\n  watch: {\n    isEditing: function (isEditing) {\n      if (isEditing) {\n        this.pollForRowHeight();\n      } else {\n        this.clearPoll();\n      }\n    }\n  },\n  mounted() {\n    this.$nextTick().then(() => {\n      this.height = this.$el.offsetHeight;\n      this.$emit('change-height', this.height);\n    });\n    if (this.isEditing) {\n      this.pollForRowHeight();\n    }\n  },\n  unmounted() {\n    this.clearPoll();\n  },\n  methods: {\n    pollForRowHeight() {\n      this.clearPoll();\n      this.pollID = window.setInterval(this.heightPoll, 300);\n    },\n    clearPoll() {\n      if (this.pollID) {\n        window.clearInterval(this.pollID);\n        this.pollID = undefined;\n      }\n    },\n    heightPoll() {\n      let height = this.$el.offsetHeight;\n      if (height !== this.height) {\n        this.$emit('change-height', height);\n        this.height = height;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableCell.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <td\n    ref=\"tableCell\"\n    :title=\"formattedValue\"\n    :aria-label=\"`${columnKey} table cell ${formattedValue}`\"\n    @click=\"selectCell($event.currentTarget, columnKey)\"\n    @mouseover.ctrl=\"showToolTip\"\n    @mouseleave=\"hideToolTip\"\n  >\n    {{ formattedValue }}\n  </td>\n</template>\n\n<script>\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\n\nexport default {\n  mixins: [tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    row: {\n      type: Object,\n      required: true\n    },\n    columnKey: {\n      type: String,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  computed: {\n    formattedValue() {\n      return this.row.getFormattedValue(this.columnKey);\n    },\n    isSelectable() {\n      let column = this.row.columns[this.columnKey];\n\n      return column && column.selectable;\n    }\n  },\n  methods: {\n    selectCell(element, columnKey) {\n      if (this.isSelectable) {\n        this.openmct.selection.select(\n          [\n            {\n              element: element,\n              context: {\n                type: 'table-cell',\n                row: this.row.objectKeyString,\n                column: columnKey\n              }\n            },\n            {\n              element: this.openmct.layout.$refs.browseObject.$el,\n              context: {\n                item: this.objectPath[0]\n              }\n            }\n          ],\n          false\n        );\n        event.stopPropagation();\n      }\n    },\n    async showToolTip() {\n      if (this.columnKey !== 'name') {\n        return;\n      }\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(this.row.objectKeyString), BELOW, 'tableCell');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableColumnHeader.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <th\n    :style=\"{ width: columnWidth + 'px', 'max-width': columnWidth + 'px' }\"\n    :draggable=\"isEditing\"\n    @mouseup=\"sort\"\n    v-on=\"\n      isEditing\n        ? {\n            dragstart: columnMoveStart,\n            drop: columnMoveEnd,\n            dragleave: hideDropTarget,\n            dragover: dragOverColumn\n          }\n        : {}\n    \"\n  >\n    <div\n      class=\"c-telemetry-table__headers__content\"\n      :class=\"\n        [\n          isSortable ? 'is-sortable' : '',\n          isSortable && sortOptions.key === headerKey ? 'is-sorting' : '',\n          isSortable && sortOptions.direction\n        ].join(' ')\n      \"\n    >\n      <div class=\"c-telemetry-table__resize-hitarea\" @mousedown=\"resizeColumnStart\"></div>\n      <slot></slot>\n    </div>\n  </th>\n</template>\n<script>\nconst MOVE_COLUMN_DT_TYPE = 'movecolumnfromindex';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    headerKey: {\n      type: String,\n      default: undefined\n    },\n    headerIndex: {\n      type: Number,\n      default: undefined\n    },\n    isHeaderTitle: {\n      type: Boolean,\n      default: undefined\n    },\n    sortOptions: {\n      type: Object,\n      default: undefined\n    },\n    columnWidth: {\n      type: Number,\n      default: undefined\n    },\n    hotzone: Boolean,\n    isEditing: Boolean\n  },\n  emits: [\n    'resize-column-end',\n    'resize-column',\n    'drop-target-offset-changed',\n    'drop-target-active',\n    'reorder-column',\n    'sort'\n  ],\n  computed: {\n    isSortable() {\n      return this.sortOptions !== undefined;\n    }\n  },\n  methods: {\n    resizeColumnStart(event) {\n      this.resizeStartX = event.clientX;\n      this.resizeStartWidth = this.columnWidth;\n\n      document.addEventListener('mouseup', this.resizeColumnEnd, {\n        once: true,\n        capture: true\n      });\n      document.addEventListener('mousemove', this.resizeColumn);\n      event.preventDefault();\n    },\n    resizeColumnEnd(event) {\n      this.resizeStartX = undefined;\n      this.resizeStartWidth = undefined;\n      document.removeEventListener('mousemove', this.resizeColumn);\n      event.preventDefault();\n      event.stopPropagation();\n\n      this.$emit('resize-column-end');\n    },\n    resizeColumn(event) {\n      let delta = event.clientX - this.resizeStartX;\n      let newWidth = this.resizeStartWidth + delta;\n      let minWidth = parseInt(window.getComputedStyle(this.$el).minWidth, 10);\n      if (newWidth > minWidth) {\n        this.$emit('resize-column', this.headerKey, newWidth);\n      }\n    },\n    columnMoveStart(event) {\n      event.dataTransfer.setData(MOVE_COLUMN_DT_TYPE, this.headerIndex);\n    },\n    isColumnMoveEvent(event) {\n      return [...event.dataTransfer.types].includes(MOVE_COLUMN_DT_TYPE);\n    },\n    dragOverColumn(event) {\n      if (this.isColumnMoveEvent(event)) {\n        event.preventDefault();\n        this.updateDropOffset(event.currentTarget, event.clientX);\n      } else {\n        return false;\n      }\n    },\n    updateDropOffset(element, clientX) {\n      let thClientLeft = element.getBoundingClientRect().x;\n      let offsetInHeader = clientX - thClientLeft;\n      let dropOffsetLeft;\n\n      if (offsetInHeader < element.offsetWidth / 2) {\n        dropOffsetLeft = element.offsetLeft;\n      } else {\n        dropOffsetLeft = element.offsetLeft + element.offsetWidth;\n      }\n\n      this.$emit('drop-target-offset-changed', dropOffsetLeft);\n      this.$emit('drop-target-active', true);\n    },\n    hideDropTarget() {\n      this.$emit('drop-target-active', false);\n    },\n    columnMoveEnd(event) {\n      if (this.isColumnMoveEvent(event)) {\n        let toIndex = this.headerIndex;\n        let fromIndex = event.dataTransfer.getData(MOVE_COLUMN_DT_TYPE);\n        if (event.offsetX < event.target.offsetWidth / 2) {\n          if (toIndex > fromIndex) {\n            toIndex--;\n          }\n        } else {\n          if (toIndex < fromIndex) {\n            toIndex++;\n          }\n        }\n\n        if (toIndex !== fromIndex) {\n          this.$emit('reorder-column', fromIndex, toIndex);\n        }\n      }\n    },\n    sort() {\n      this.$emit('sort');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"root\" class=\"c-table-wrapper\" :class=\"tableClasses\">\n    <div v-if=\"enableLegacyToolbar\" class=\"c-table-control-bar c-control-bar\" role=\"menubar\">\n      <button\n        v-if=\"allowExport\"\n        v-show=\"!markedRows.length\"\n        class=\"c-button icon-download labeled\"\n        aria-label=\"Export this view's data\"\n        title=\"Export this view's data\"\n        @click=\"exportAllDataAsCSV()\"\n      >\n        <span class=\"c-button__label\">Export Table Data</span>\n      </button>\n      <button\n        v-if=\"allowExport\"\n        v-show=\"markedRows.length\"\n        class=\"c-button icon-download labeled\"\n        aria-label=\"Export marked rows as CSV\"\n        title=\"Export marked rows as CSV\"\n        @click=\"exportMarkedDataAsCSV()\"\n      >\n        <span class=\"c-button__label\">Export Marked Rows</span>\n      </button>\n      <button\n        v-show=\"markedRows.length\"\n        class=\"c-button icon-x labeled\"\n        aria-label=\"Unmark all rows\"\n        title=\"Unmark all rows\"\n        @click=\"unmarkAllRows()\"\n      >\n        <span class=\"c-button__label\">Unmark All Rows</span>\n      </button>\n      <div v-if=\"marking.enable\" class=\"c-separator\"></div>\n      <button\n        v-if=\"marking.enable\"\n        :aria-label=\"paused ? 'Continue real-time data flow' : 'Pause real-time data flow'\"\n        class=\"c-button icon-pause pause-play labeled\"\n        :class=\"paused ? 'icon-play is-paused' : 'icon-pause'\"\n        :title=\"paused ? 'Continue real-time data flow' : 'Pause real-time data flow'\"\n        @click=\"togglePauseByButton()\"\n      >\n        <span class=\"c-button__label\">\n          {{ paused ? 'Play' : 'Pause' }}\n        </span>\n      </button>\n\n      <template v-if=\"!isEditing\">\n        <div class=\"c-separator\" role=\"separator\"></div>\n        <button\n          v-if=\"isAutosizeEnabled\"\n          class=\"c-button icon-arrows-right-left labeled\"\n          aria-label=\"Increase column widths to fit currently available data.\"\n          title=\"Increase column widths to fit currently available data.\"\n          @click=\"recalculateColumnWidths\"\n        >\n          <span class=\"c-button__label\">Expand Columns</span>\n        </button>\n        <button\n          v-else\n          aria-label=\"Automatically size columns to fit the table into the available space.\"\n          class=\"c-button icon-expand labeled\"\n          title=\"Automatically size columns to fit the table into the available space.\"\n          @click=\"autosizeColumns\"\n        >\n          <span class=\"c-button__label\">Autosize Columns</span>\n        </button>\n      </template>\n\n      <slot name=\"buttons\"></slot>\n    </div>\n\n    <!-- alternate controlbar start -->\n    <div v-if=\"marking.useAlternateControlBar\" class=\"c-table-control-bar c-control-bar\">\n      <div class=\"c-control-bar__label\">\n        {{\n          markedRows.length > 1\n            ? `${markedRows.length} ${marking.rowNamePlural} selected`\n            : `${markedRows.length} ${marking.rowName} selected`\n        }}\n      </div>\n\n      <ToggleSwitch\n        id=\"show-filtered-rows-toggle\"\n        label=\"Show selected items only\"\n        :checked=\"isShowingMarkedRowsOnly\"\n        @change=\"toggleMarkedRows\"\n      />\n\n      <button\n        :class=\"{ 'hide-nice': !markedRows.length }\"\n        class=\"c-icon-button icon-x labeled\"\n        aria-label=\"Deselect All\"\n        title=\"Deselect All\"\n        @click=\"unmarkAllRows()\"\n      >\n        <span class=\"c-icon-button__label\"\n          >{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }}\n        </span>\n      </button>\n\n      <slot name=\"buttons\"></slot>\n    </div>\n    <!-- alternate controlbar end  -->\n\n    <div\n      class=\"c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver\"\n      :class=\"{\n        'is-paused': paused\n      }\"\n    >\n      <div :style=\"{ 'max-width': widthWithScroll, 'min-width': '150px' }\">\n        <slot></slot>\n      </div>\n\n      <div\n        v-if=\"isDropTargetActive\"\n        class=\"c-telemetry-table__drop-target\"\n        :style=\"dropTargetStyle\"\n      ></div>\n\n      <ProgressBar\n        v-if=\"loading\"\n        class=\"c-telemetry-table__progress-bar\"\n        :model=\"{ progressPerc: null }\"\n      />\n\n      <!-- Headers table -->\n      <div\n        v-show=\"!hideHeaders\"\n        ref=\"headersHolderEl\"\n        class=\"c-telemetry-table__headers-w js-table__headers-w\"\n        :style=\"{ 'max-width': widthWithScroll }\"\n      >\n        <table class=\"c-table__headers c-telemetry-table__headers\">\n          <thead>\n            <tr class=\"c-telemetry-table__headers__labels\">\n              <TableColumnHeader\n                v-for=\"(title, key, headerIndex) in headers\"\n                :key=\"key\"\n                :header-key=\"key\"\n                :header-index=\"headerIndex\"\n                :column-width=\"columnWidths[key]\"\n                :sort-options=\"sortOptions\"\n                :is-editing=\"isEditing\"\n                @sort=\"allowSorting && sortBy(key)\"\n                @resize-column=\"resizeColumn\"\n                @drop-target-offset-changed=\"setDropTargetOffset\"\n                @drop-target-active=\"dropTargetActive\"\n                @reorder-column=\"reorderColumn\"\n                @resize-column-end=\"updateConfiguredColumnWidths\"\n              >\n                <span class=\"c-telemetry-table__headers__label\">{{ title }}</span>\n              </TableColumnHeader>\n            </tr>\n            <tr v-if=\"allowFiltering\" class=\"c-telemetry-table__headers__filter\">\n              <TableColumnHeader\n                v-for=\"(title, key, headerIndex) in headers\"\n                :key=\"key\"\n                :header-key=\"key\"\n                :header-index=\"headerIndex\"\n                :column-width=\"columnWidths[key]\"\n                :is-editing=\"isEditing\"\n                :aria-label=\"`${headers[key]} filter header`\"\n                @resize-column=\"resizeColumn\"\n                @drop-target-offset-changed=\"setDropTargetOffset\"\n                @drop-target-active=\"dropTargetActive\"\n                @reorder-column=\"reorderColumn\"\n                @resize-column-end=\"updateConfiguredColumnWidths\"\n              >\n                <Search\n                  :value=\"filters[key]\"\n                  class=\"c-table__search\"\n                  :aria-label=\"`${key} filter input`\"\n                  @input=\"filterChanged(key, $event)\"\n                  @clear=\"clearFilter(key)\"\n                >\n                  <button\n                    class=\"c-search__use-regex\"\n                    :class=\"{ 'is-active': enableRegexSearch[key] }\"\n                    aria-label=\"Click to enable regex: enter a string with slashes, like this: /regex_exp/\"\n                    title=\"Click to enable regex: enter a string with slashes, like this: /regex_exp/\"\n                    @click=\"toggleRegex(key)\"\n                  >\n                    /R/\n                  </button>\n                </Search>\n              </TableColumnHeader>\n            </tr>\n          </thead>\n        </table>\n      </div>\n      <!-- Content table -->\n      <div\n        ref=\"scrollable\"\n        class=\"c-table__body-w c-telemetry-table__body-w js-telemetry-table__body-w\"\n        :style=\"{ 'max-width': widthWithScroll }\"\n        @scroll=\"scroll\"\n      >\n        <div class=\"c-telemetry-table__scroll-forcer\" :style=\"{ width: totalWidth + 'px' }\"></div>\n        <table\n          ref=\"contentTable\"\n          class=\"c-table__body c-telemetry-table__body js-telemetry-table__content\"\n          :style=\"{ height: totalHeight + 'px' }\"\n          :aria-label=\"`${table.domainObject.name} table content`\"\n        >\n          <tbody>\n            <TelemetryTableRow\n              v-for=\"(row, rowIndex) in visibleRows\"\n              :key=\"rowIndex\"\n              :headers=\"headers\"\n              :column-widths=\"columnWidths\"\n              :row-index=\"rowIndex\"\n              :object-path=\"objectPath\"\n              :row-offset=\"rowOffset\"\n              :row-height=\"rowHeight\"\n              :row=\"getRow(rowIndex)\"\n              :marked=\"row.marked\"\n              @mark=\"markRow\"\n              @unmark=\"unmarkRow\"\n              @mark-multiple-concurrent=\"markMultipleConcurrentRows\"\n              @row-context-click=\"updateViewContext\"\n            />\n          </tbody>\n        </table>\n      </div>\n      <!-- Sizing table -->\n      <table\n        ref=\"sizingTable\"\n        class=\"c-telemetry-table__sizing js-telemetry-table__sizing\"\n        :style=\"sizingTableWidth\"\n      >\n        <SizingRow :is-editing=\"isEditing\" @change-height=\"setRowHeight\" />\n        <tr>\n          <template v-for=\"(title, key) in headers\" :key=\"key\">\n            <th\n              :style=\"{\n                width: configuredColumnWidths[key] + 'px',\n                'max-width': configuredColumnWidths[key] + 'px'\n              }\"\n            >\n              {{ title }}\n            </th>\n          </template>\n        </tr>\n        <TelemetryTableRow\n          v-for=\"(sizingRowData, objectKeyString) in sizingRows\"\n          :key=\"objectKeyString\"\n          :headers=\"headers\"\n          :column-widths=\"configuredColumnWidths\"\n          :row=\"sizingRowData\"\n          :object-path=\"objectPath\"\n          @row-context-click=\"updateViewContext\"\n        />\n      </table>\n      <TableFooterIndicator\n        class=\"c-telemetry-table__footer\"\n        :marked-rows=\"markedRows.length\"\n        :total-rows=\"totalNumberOfRows\"\n        :telemetry-mode=\"telemetryMode\"\n        @telemetry-mode-change=\"updateTelemetryMode\"\n      />\n    </div>\n  </div>\n  <!-- closes c-table-wrapper -->\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { onMounted, ref, toRaw } from 'vue';\n\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport CSVExporter from '../../../exporters/CSVExporter.js';\nimport ProgressBar from '../../../ui/components/ProgressBar.vue';\nimport Search from '../../../ui/components/SearchComponent.vue';\nimport ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';\nimport { useResizeObserver } from '../../../ui/composables/resize.js';\nimport { MODE, ORDER } from '../constants.js';\nimport SizingRow from './SizingRow.vue';\nimport TableColumnHeader from './TableColumnHeader.vue';\nimport TableFooterIndicator from './TableFooterIndicator.vue';\nimport TelemetryTableRow from './TableRow.vue';\n\nconst VISIBLE_ROW_COUNT = 100;\nconst ROW_HEIGHT = 17;\nconst AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;\n\nexport default {\n  components: {\n    TelemetryTableRow,\n    TableColumnHeader,\n    Search,\n    TableFooterIndicator,\n    ToggleSwitch,\n    SizingRow,\n    ProgressBar\n  },\n  mixins: [stalenessMixin],\n  inject: ['openmct', 'objectPath', 'table', 'currentView', 'renderWhenVisible'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      default: false\n    },\n    marking: {\n      type: Object,\n      required: true,\n      default() {\n        return {\n          enable: false,\n          disableMultiSelect: false,\n          useAlternateControlBar: false,\n          rowName: '',\n          rowNamePlural: ''\n        };\n      }\n    },\n    allowExport: {\n      type: Boolean,\n      default: true\n    },\n    allowFiltering: {\n      type: Boolean,\n      default: true\n    },\n    allowSorting: {\n      type: Boolean,\n      default: true\n    },\n    enableLegacyToolbar: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['marked-rows-updated', 'filter'],\n  setup() {\n    const root = ref(null);\n    const { size: containerSize, startObserving } = useResizeObserver();\n    onMounted(() => {\n      startObserving(root.value);\n    });\n\n    return { containerSize, root };\n  },\n  data() {\n    let configuration = this.table.configuration.getConfiguration();\n\n    return {\n      headers: {},\n      visibleRows: [],\n      columnWidths: {},\n      configuredColumnWidths: configuration.columnWidths,\n      sizingRows: {},\n      rowHeight: ROW_HEIGHT,\n      totalHeight: 0,\n      totalWidth: 0,\n      rowOffset: 0,\n      autoScroll: true,\n      sortOptions: {},\n      filters: {},\n      loading: false,\n      scrollable: undefined,\n      tableEl: undefined,\n      headersHolderEl: undefined,\n      processingScroll: false,\n      updatingView: false,\n      dropOffsetLeft: undefined,\n      isDropTargetActive: false,\n      isAutosizeEnabled: configuration.autosize,\n      scrollW: 0,\n      markCounter: 0,\n      paused: false,\n      markedRows: [],\n      isShowingMarkedRowsOnly: false,\n      enableRegexSearch: {},\n      hideHeaders: configuration.hideHeaders,\n      totalNumberOfRows: 0,\n      rowContext: {},\n      telemetryMode: configuration.telemetryMode,\n      rowLimit: configuration.rowLimit,\n      persistModeChange: configuration.persistModeChange,\n      afterLoadActions: [],\n      existingConfiguration: configuration\n    };\n  },\n  computed: {\n    dropTargetStyle() {\n      return {\n        top: this.$refs.headersHolderEl.offsetTop + 'px',\n        height: this.totalHeight + this.$refs.headersHolderEl.offsetHeight + 'px',\n        left: this.dropOffsetLeft && this.dropOffsetLeft + 'px'\n      };\n    },\n    lastHeaderKey() {\n      let headerKeys = Object.keys(this.headers);\n\n      return headerKeys[headerKeys.length - 1];\n    },\n    widthWithScroll() {\n      return this.totalWidth + this.scrollW + 'px';\n    },\n    sizingTableWidth() {\n      let style;\n\n      if (this.isAutosizeEnabled) {\n        style = { width: 'calc(100% - ' + this.scrollW + 'px)' };\n      } else {\n        let totalWidth = Object.keys(this.headers).reduce((total, key) => {\n          total += this.configuredColumnWidths[key];\n\n          return total;\n        }, 0);\n\n        style = { width: totalWidth + 'px' };\n      }\n\n      return style;\n    },\n    tableClasses() {\n      let classes = [];\n\n      if (this.paused) {\n        classes.push('is-paused');\n      }\n\n      if (this.isStale) {\n        classes.push('is-stale');\n      }\n\n      return classes;\n    }\n  },\n  watch: {\n    //This should be refactored so that it doesn't require an explicit watch. Should be doable.\n    containerSize: {\n      handler() {\n        this.debouncedRescaleToContainer();\n      },\n      deep: true\n    },\n    loading: {\n      handler(isLoading) {\n        if (!isLoading) {\n          this.runAfterLoadActions();\n        }\n\n        if (this.viewActionsCollection) {\n          let action = isLoading ? 'disable' : 'enable';\n          this.viewActionsCollection[action](['export-csv-all']);\n        }\n      }\n    },\n    markedRows: {\n      handler(newVal, oldVal) {\n        this.$emit('marked-rows-updated', newVal, oldVal);\n\n        if (this.viewActionsCollection) {\n          if (newVal.length > 0) {\n            this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);\n          } else if (newVal.length === 0) {\n            this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);\n          }\n        }\n      },\n      deep: true\n    },\n    paused: {\n      handler(newVal) {\n        if (this.viewActionsCollection) {\n          if (newVal) {\n            this.viewActionsCollection.hide(['pause-data']);\n            this.viewActionsCollection.show(['play-data']);\n          } else {\n            this.viewActionsCollection.hide(['play-data']);\n            this.viewActionsCollection.show(['pause-data']);\n          }\n        }\n      }\n    },\n    isAutosizeEnabled: {\n      handler(newVal) {\n        if (this.viewActionsCollection) {\n          if (newVal) {\n            this.viewActionsCollection.show(['expand-columns']);\n            this.viewActionsCollection.hide(['autosize-columns']);\n          } else {\n            this.viewActionsCollection.show(['autosize-columns']);\n            this.viewActionsCollection.hide(['expand-columns']);\n          }\n        }\n      }\n    }\n  },\n  created() {\n    this.filterTelemetry = _.debounce(this.filterTelemetry, 500);\n  },\n  mounted() {\n    this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });\n    this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);\n\n    this.csvExporter = new CSVExporter();\n    this.scroll = _.throttle(this.scroll, 100);\n\n    if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {\n      this.$nextTick(() => {\n        this.viewActionsCollection = this.openmct.actions.getActionsCollection(\n          this.objectPath,\n          this.currentView\n        );\n        this.initializeViewActions();\n      });\n    }\n\n    this.table.on('object-added', this.addObject);\n    this.table.on('object-removed', this.removeObject);\n    this.table.on('refresh', this.clearRowsAndRerender);\n    this.table.on('historical-rows-processed', this.checkForMarkedRows);\n    this.table.on('outstanding-requests', this.outstandingRequests);\n    this.table.on('telemetry-staleness', this.handleStaleness);\n\n    this.table.configuration.on('change', this.handleConfigurationChanges);\n\n    this.table.tableRows.on('add', this.rowsAdded);\n    this.table.tableRows.on('remove', this.rowsRemoved);\n    this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);\n    this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);\n\n    this.openmct.time.on('boundsChanged', this.boundsChanged);\n\n    //Default sort\n    this.sortOptions = this.table.tableRows.sortBy();\n    this.scrollable = this.$refs.scrollable;\n    this.lastScrollLeft = this.scrollable.scrollLeft;\n    this.contentTable = this.$refs.contentTable;\n    this.sizingTable = this.$refs.sizingTable;\n    this.headersHolderEl = this.$refs.headersHolderEl;\n    this.table.configuration.on('change', this.updateConfiguration);\n\n    this.calculateTableSize();\n    this.calculateScrollbarWidth();\n\n    this.table.initialize();\n    this.rescaleToContainer();\n\n    // Scroll to the top of the table after loading\n    this.addToAfterLoadActions(this.scroll);\n  },\n  beforeUnmount() {\n    this.table.off('object-added', this.addObject);\n    this.table.off('object-removed', this.removeObject);\n    this.table.off('historical-rows-processed', this.checkForMarkedRows);\n    this.table.off('refresh', this.clearRowsAndRerender);\n    this.table.off('outstanding-requests', this.outstandingRequests);\n    this.table.off('telemetry-staleness', this.handleStaleness);\n\n    this.table.configuration.off('change', this.handleConfigurationChanges);\n\n    this.table.tableRows.off('add', this.rowsAdded);\n    this.table.tableRows.off('remove', this.rowsRemoved);\n    this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);\n    this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);\n\n    this.table.configuration.off('change', this.updateConfiguration);\n\n    this.openmct.time.off('boundsChanged', this.boundsChanged);\n\n    this.table.configuration.destroy();\n\n    this.table.destroy();\n  },\n  methods: {\n    addToAfterLoadActions(func) {\n      this.afterLoadActions.push(func);\n    },\n    runAfterLoadActions() {\n      if (this.afterLoadActions.length > 0) {\n        this.afterLoadActions.forEach((action) => action());\n        this.afterLoadActions = [];\n      }\n    },\n    handleConfigurationChanges(changes) {\n      const { rowLimit, telemetryMode, persistModeChange } = changes;\n      const telemetryModeChanged = this.existingConfiguration.telemetryMode !== telemetryMode;\n      let rowLimitChanged = false;\n\n      this.persistModeChange = persistModeChange;\n\n      // both rowLimit changes and telemetryMode changes\n      // require a re-request of telemetry\n\n      if (this.rowLimit !== rowLimit) {\n        rowLimitChanged = true;\n        this.rowLimit = rowLimit;\n        this.table.updateRowLimit(rowLimit);\n      }\n\n      // check for telemetry mode change, because you could technically have persist mode changes\n      // set to false, which could create a state where the configuration saved telemetry mode is\n      // different from the currently set telemetry mode\n      if (telemetryModeChanged && this.telemetryMode !== telemetryMode) {\n        this.telemetryMode = telemetryMode;\n\n        // this method also re-requests telemetry\n        this.table.updateTelemetryMode(telemetryMode);\n      }\n\n      if (rowLimitChanged && !telemetryModeChanged) {\n        this.table.clearAndResubscribe();\n      }\n\n      this.existingConfiguration = changes;\n    },\n    updateVisibleRows() {\n      if (!this.updatingView) {\n        this.updatingView = this.renderWhenVisible(() => {\n          let start = 0;\n          let end = VISIBLE_ROW_COUNT;\n          let tableRows = this.table.tableRows.getRows();\n          let tableRowsLength = tableRows.length;\n\n          this.totalNumberOfRows = tableRowsLength;\n\n          if (tableRowsLength < VISIBLE_ROW_COUNT) {\n            end = tableRowsLength;\n          } else {\n            let firstVisible = this.calculateFirstVisibleRow();\n            let lastVisible = this.calculateLastVisibleRow();\n            let totalVisible = lastVisible - firstVisible;\n\n            let numberOffscreen = VISIBLE_ROW_COUNT - totalVisible;\n            start = firstVisible - Math.floor(numberOffscreen / 2);\n            end = lastVisible + Math.ceil(numberOffscreen / 2);\n\n            if (start < 0) {\n              start = 0;\n              end = Math.min(VISIBLE_ROW_COUNT, tableRowsLength);\n            } else if (end >= tableRowsLength) {\n              end = tableRowsLength;\n              start = end - VISIBLE_ROW_COUNT + 1;\n            }\n          }\n\n          this.rowOffset = start;\n          this.visibleRows = tableRows.slice(start, end);\n\n          this.updatingView = false;\n        });\n      }\n    },\n    calculateFirstVisibleRow() {\n      let scrollTop = this.scrollable.scrollTop;\n\n      return Math.floor(scrollTop / this.rowHeight);\n    },\n    calculateLastVisibleRow() {\n      let scrollBottom = this.scrollable.scrollTop + this.scrollable.offsetHeight;\n\n      return Math.ceil(scrollBottom / this.rowHeight);\n    },\n    updateHeaders() {\n      this.headers = this.table.configuration.getVisibleHeaders();\n    },\n    calculateScrollbarWidth() {\n      // Scroll width seems to vary by a pixel for reasons that are not clear.\n      this.scrollW = this.scrollable.offsetWidth - this.scrollable.clientWidth + 1;\n    },\n    calculateColumnWidths() {\n      let columnWidths = {};\n      let totalWidth = 0;\n      let headerKeys = Object.keys(this.headers);\n      let sizingTableRow = this.sizingTable.children[1];\n      let sizingCells = sizingTableRow.children;\n\n      headerKeys.forEach((headerKey, headerIndex, array) => {\n        if (this.isAutosizeEnabled) {\n          columnWidths[headerKey] = this.sizingTable.clientWidth / array.length;\n        } else {\n          let cell = sizingCells[headerIndex];\n          columnWidths[headerKey] = cell.offsetWidth;\n        }\n\n        totalWidth += columnWidths[headerKey];\n      });\n\n      this.columnWidths = columnWidths;\n      this.totalWidth = totalWidth;\n\n      this.calculateScrollbarWidth();\n    },\n    getRow(rowIndex) {\n      return toRaw(this.visibleRows[rowIndex]);\n    },\n    sortBy(columnKey) {\n      let timeSystemKey = this.openmct.time.getTimeSystem().key;\n\n      if (this.telemetryMode === MODE.PERFORMANCE && columnKey !== timeSystemKey) {\n        this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => {\n          this.initiateSort(columnKey);\n        });\n      } else {\n        this.initiateSort(columnKey);\n      }\n    },\n    initiateSort(columnKey) {\n      // If sorting by the same column, flip the sort direction.\n      if (this.sortOptions.key === columnKey) {\n        if (this.sortOptions.direction === ORDER.ASCENDING) {\n          this.sortOptions.direction = ORDER.DESCENDING;\n        } else {\n          this.sortOptions.direction = ORDER.ASCENDING;\n        }\n      } else {\n        this.sortOptions = {\n          key: columnKey,\n          direction: ORDER.DESCENDING\n        };\n      }\n\n      this.table.sortBy(this.sortOptions);\n    },\n    scroll() {\n      if (this.lastScrollLeft === this.scrollable.scrollLeft) {\n        this.throttledUpdateVisibleRows();\n      }\n      this.synchronizeScrollX();\n\n      if (this.shouldAutoScroll()) {\n        this.autoScroll = true;\n      } else {\n        // If user scrolls away from bottom, disable auto-scroll.\n        // Auto-scroll will be re-enabled if user scrolls to bottom again.\n        this.autoScroll = false;\n      }\n    },\n    shouldAutoScroll() {\n      if (this.sortOptions.direction === ORDER.DESCENDING) {\n        return false;\n      }\n\n      return (\n        this.scrollable.scrollTop >=\n        this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT\n      );\n    },\n    initiateAutoScroll() {\n      this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;\n    },\n    synchronizeScrollX() {\n      this.lastScrollLeft = this.scrollable.scrollLeft;\n\n      if (this.$refs.headersHolderEl && this.scrollable) {\n        this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;\n      }\n    },\n    filterTelemetry(columnKey) {\n      if (this.enableRegexSearch[columnKey]) {\n        if (this.isCompleteRegex(this.filters[columnKey])) {\n          this.table.tableRows.setColumnRegexFilter(\n            columnKey,\n            this.filters[columnKey].slice(1, -1)\n          );\n        } else {\n          return;\n        }\n      } else {\n        this.table.tableRows.setColumnFilter(columnKey, this.filters[columnKey]);\n      }\n\n      this.setHeight();\n    },\n    filterChanged(columnKey, newFilterValue) {\n      this.filters[columnKey] = newFilterValue;\n      this.filterTelemetry(columnKey);\n    },\n    clearFilter(columnKey) {\n      this.filters[columnKey] = '';\n      this.table.tableRows.setColumnFilter(columnKey, '');\n      this.setHeight();\n    },\n    rowsAdded(rows) {\n      this.setHeight();\n\n      let sizingRow;\n      if (Array.isArray(rows)) {\n        sizingRow = rows[0];\n      } else {\n        sizingRow = rows;\n      }\n\n      if (!this.sizingRows[sizingRow.objectKeyString]) {\n        this.sizingRows[sizingRow.objectKeyString] = sizingRow;\n        this.$nextTick().then(this.calculateColumnWidths);\n      }\n\n      if (this.autoScroll) {\n        this.initiateAutoScroll();\n      }\n\n      this.throttledUpdateVisibleRows();\n    },\n    rowsRemoved(rows) {\n      this.setHeight();\n      this.throttledUpdateVisibleRows();\n    },\n    /**\n     * Calculates height based on total number of rows, and sets table height.\n     */\n    setHeight() {\n      let tableRowsLength = this.table.tableRows.getRowsLength();\n      this.totalHeight = this.rowHeight * tableRowsLength - 1;\n      // Set element height directly to avoid having to wait for Vue to update DOM\n      // which causes subsequent scroll to use an out of date height.\n      this.contentTable.style.height = this.totalHeight + 'px';\n    },\n    exportAsCSV(data) {\n      const headerKeys = Object.keys(this.headers);\n\n      this.csvExporter.export(data, {\n        filename: this.table.domainObject.name + '.csv',\n        headers: headerKeys\n      });\n    },\n    getTableRowData() {\n      const justTheData = this.table.tableRows\n        .getRows()\n        .map((row) => row.getFormattedDatum(this.headers));\n\n      return justTheData;\n    },\n    exportAllDataAsCSV() {\n      if (this.telemetryMode === MODE.PERFORMANCE) {\n        this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => {\n          const data = this.getTableRowData();\n\n          this.exportAsCSV(data);\n        });\n      } else {\n        const data = this.getTableRowData();\n\n        this.exportAsCSV(data);\n      }\n    },\n    exportMarkedDataAsCSV() {\n      const data = this.table.tableRows\n        .getRows()\n        .filter((row) => row.marked === true)\n        .map((row) => row.getFormattedDatum(this.headers));\n\n      this.exportAsCSV(data);\n    },\n    outstandingRequests(loading) {\n      this.loading = loading;\n    },\n    handleStaleness({ keyString, stalenessResponse }) {\n      this.addOrRemoveStaleObject(keyString, stalenessResponse);\n    },\n    calculateTableSize() {\n      this.$nextTick().then(this.calculateColumnWidths);\n    },\n    updateConfiguration(configuration) {\n      this.isAutosizeEnabled = configuration.autosize;\n      this.hideHeaders = configuration.hideHeaders;\n\n      this.updateHeaders();\n      this.$nextTick().then(this.calculateColumnWidths);\n    },\n    addObject() {\n      this.updateHeaders();\n      this.$nextTick().then(this.calculateColumnWidths);\n    },\n    removeObject(objectIdentifier) {\n      let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);\n      delete this.sizingRows[objectKeyString];\n      this.updateHeaders();\n      this.$nextTick().then(this.calculateColumnWidths);\n    },\n    resizeColumn(key, newWidth) {\n      let delta = newWidth - this.columnWidths[key];\n      this.columnWidths[key] = newWidth;\n      this.totalWidth += delta;\n    },\n    updateConfiguredColumnWidths() {\n      this.configuredColumnWidths = this.columnWidths;\n\n      let configuration = this.table.configuration.getConfiguration();\n      configuration.autosize = false;\n      configuration.columnWidths = this.configuredColumnWidths;\n\n      this.table.configuration.updateConfiguration(configuration);\n    },\n    setDropTargetOffset(dropOffsetLeft) {\n      this.dropOffsetLeft = dropOffsetLeft - this.scrollable.scrollLeft;\n    },\n    reorderColumn(from, to) {\n      let newHeaderKeys = Object.keys(this.headers);\n      let moveFromKey = newHeaderKeys[from];\n\n      if (to < from) {\n        newHeaderKeys.splice(from, 1);\n        newHeaderKeys.splice(to, 0, moveFromKey);\n      } else {\n        newHeaderKeys.splice(from, 1);\n        newHeaderKeys.splice(to, 0, moveFromKey);\n      }\n\n      let newHeaders = newHeaderKeys.reduce((headers, headerKey) => {\n        headers[headerKey] = this.headers[headerKey];\n\n        return headers;\n      }, {});\n\n      this.table.configuration.setColumnOrder(Object.keys(newHeaders));\n\n      this.headers = newHeaders;\n      this.dropOffsetLeft = undefined;\n\n      this.dropTargetActive(false);\n    },\n    dropTargetActive(isActive) {\n      this.isDropTargetActive = isActive;\n    },\n    rescaleToContainer() {\n      let scrollTop = this.scrollable.scrollTop;\n\n      this.renderWhenVisible(() => {\n        if (this.isAutosizeEnabled) {\n          this.calculateTableSize();\n          // On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?\n          // Need to preserve scroll position in this case.\n          if (this.autoScroll) {\n            this.initiateAutoScroll();\n          } else {\n            this.scrollable.scrollTop = scrollTop;\n          }\n        }\n\n        scrollTop = this.scrollable.scrollTop;\n      });\n    },\n    clearRowsAndRerender() {\n      this.visibleRows = [];\n      this.$nextTick().then(this.throttledUpdateVisibleRows);\n    },\n    pause(byButton) {\n      if (byButton) {\n        this.pausedByButton = true;\n      }\n\n      this.paused = true;\n      this.table.pause();\n    },\n    unpause(byButtonOrUserBoundsChange) {\n      if (byButtonOrUserBoundsChange) {\n        this.undoMarkedRows();\n        this.table.unpause();\n        this.paused = false;\n        this.pausedByButton = false;\n      } else {\n        if (!this.pausedByButton) {\n          this.undoMarkedRows();\n          this.table.unpause();\n          this.paused = false;\n        }\n      }\n\n      this.isShowingMarkedRowsOnly = false;\n    },\n    boundsChanged(_bounds, isTick) {\n      if (isTick) {\n        return;\n      }\n\n      // User bounds change.\n      if (this.paused) {\n        this.unpause(true);\n      }\n    },\n    togglePauseByButton() {\n      if (this.paused) {\n        this.unpause(true);\n      } else {\n        this.pause(true);\n      }\n    },\n    undoMarkedRows() {\n      this.markedRows.forEach((r) => (r.marked = false));\n      this.markedRows = [];\n    },\n    unmarkRow(rowIndex) {\n      if (this.markedRows.length > 1) {\n        let row = this.visibleRows[rowIndex];\n        let positionInMarkedArray = this.markedRows.indexOf(row);\n\n        row.marked = false;\n        this.markedRows.splice(positionInMarkedArray, 1);\n\n        if (this.isShowingMarkedRowsOnly) {\n          this.visibleRows.splice(rowIndex, 1);\n        }\n      } else if (this.markedRows.length === 1) {\n        this.unmarkAllRows();\n      }\n\n      if (this.markedRows.length === 0) {\n        this.unpause();\n      }\n    },\n    markRow(rowIndex, keyModifier) {\n      if (!this.marking.enable) {\n        return;\n      }\n\n      let insertMethod = 'unshift';\n\n      if (this.markedRows.length && !keyModifier) {\n        this.undoMarkedRows();\n        insertMethod = 'push';\n      }\n\n      let markedRow = this.visibleRows[rowIndex];\n\n      markedRow.marked = true;\n      this.pause();\n\n      if (this.marking.disableMultiSelect) {\n        this.unmarkAllRows();\n        insertMethod = 'push';\n      }\n\n      this.markedRows[insertMethod](markedRow);\n    },\n    unmarkAllRows(skipUnpause) {\n      this.undoMarkedRows();\n      this.isShowingMarkedRowsOnly = false;\n      this.unpause();\n      this.restorePreviousRows();\n    },\n    markMultipleConcurrentRows(rowIndex) {\n      if (!this.marking.enable) {\n        return;\n      }\n\n      if (!this.markedRows.length || this.marking.disableMultiSelect) {\n        this.markRow(rowIndex);\n      } else {\n        if (this.markedRows.length > 1) {\n          this.markedRows.forEach((r, i) => {\n            if (i !== 0) {\n              r.marked = false;\n            }\n          });\n          this.markedRows.splice(1);\n        }\n\n        const lastRowToBeMarked = this.visibleRows[rowIndex];\n\n        const allRows = this.table.tableRows.getRows();\n        let firstRowIndex = allRows.indexOf(toRaw(this.markedRows[0]));\n        let lastRowIndex = allRows.indexOf(toRaw(lastRowToBeMarked));\n\n        //supports backward selection\n        if (lastRowIndex < firstRowIndex) {\n          [firstRowIndex, lastRowIndex] = [lastRowIndex, firstRowIndex];\n        }\n\n        let baseRow = this.markedRows[0];\n\n        for (let i = firstRowIndex; i <= lastRowIndex; i++) {\n          let row = allRows[i];\n          row.marked = true;\n\n          if (row !== baseRow && this.markedRows.indexOf(row) === -1) {\n            this.markedRows.push(row);\n          }\n        }\n      }\n    },\n    checkForMarkedRows() {\n      this.isShowingMarkedRowsOnly = false;\n      this.markedRows = this.table.tableRows.getRows().filter((row) => row.marked);\n    },\n    showRows(rows) {\n      this.table.tableRows.rows = rows;\n      this.table.emit('filter');\n    },\n    toggleMarkedRows(flag) {\n      if (flag) {\n        this.isShowingMarkedRowsOnly = true;\n        this.userScroll = this.scrollable.scrollTop;\n        this.allRows = this.table.tableRows.getRows();\n\n        this.showRows(this.markedRows);\n        this.setHeight();\n      } else {\n        this.isShowingMarkedRowsOnly = false;\n        this.restorePreviousRows();\n      }\n    },\n    restorePreviousRows() {\n      if (this.allRows && this.allRows.length) {\n        this.showRows(this.allRows);\n        this.allRows = [];\n        this.setHeight();\n        this.scrollable.scrollTop = this.userScroll;\n      }\n    },\n    updateWidthsAndClearSizingTable() {\n      this.calculateColumnWidths();\n      this.configuredColumnWidths = this.columnWidths;\n\n      this.visibleRows.forEach((row, i) => {\n        this.sizingRows[i] = undefined;\n        delete this.sizingRows[i];\n      });\n    },\n    recalculateColumnWidths() {\n      this.visibleRows.forEach((row, i) => {\n        this.sizingRows[i] = row;\n      });\n\n      this.configuredColumnWidths = {};\n      this.isAutosizeEnabled = false;\n\n      this.$nextTick().then(this.updateWidthsAndClearSizingTable);\n    },\n    autosizeColumns() {\n      this.isAutosizeEnabled = true;\n\n      this.$nextTick().then(this.calculateColumnWidths);\n    },\n    toggleRegex(key) {\n      this.filters[key] = '';\n\n      if (this.enableRegexSearch[key] === undefined) {\n        this.enableRegexSearch[key] = true;\n      } else {\n        this.enableRegexSearch[key] = !this.enableRegexSearch[key];\n      }\n    },\n    isCompleteRegex(string) {\n      return string.length > 2 && string[0] === '/' && string[string.length - 1] === '/';\n    },\n    getViewContext() {\n      return {\n        type: 'telemetry-table',\n        exportAllDataAsCSV: this.exportAllDataAsCSV,\n        exportMarkedDataAsCSV: this.exportMarkedDataAsCSV,\n        unmarkAllRows: this.unmarkAllRows,\n        togglePauseByButton: this.togglePauseByButton,\n        expandColumns: this.recalculateColumnWidths,\n        autosizeColumns: this.autosizeColumns,\n        row: this.rowContext\n      };\n    },\n    initializeViewActions() {\n      if (this.markedRows.length > 0) {\n        this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);\n      } else if (this.markedRows.length === 0) {\n        this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);\n      }\n\n      if (this.loading) {\n        this.viewActionsCollection.disable(['export-csv-all']);\n      } else {\n        this.viewActionsCollection.enable(['export-csv-all']);\n      }\n\n      if (this.paused) {\n        this.viewActionsCollection.hide(['pause-data']);\n        this.viewActionsCollection.show(['play-data']);\n      } else {\n        this.viewActionsCollection.hide(['play-data']);\n        this.viewActionsCollection.show(['pause-data']);\n      }\n\n      if (this.isAutosizeEnabled) {\n        this.viewActionsCollection.show(['expand-columns']);\n        this.viewActionsCollection.hide(['autosize-columns']);\n      } else {\n        this.viewActionsCollection.show(['autosize-columns']);\n        this.viewActionsCollection.hide(['expand-columns']);\n      }\n    },\n    confirmUnlimitedMode(\n      label,\n      callback,\n      message = 'A new data request for all telemetry values for all endpoints will be made which will take some time. Do you want to continue?'\n    ) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message,\n        buttons: [\n          {\n            label,\n            emphasis: true,\n            callback: () => {\n              this.addToAfterLoadActions(callback);\n              this.updateTelemetryMode();\n\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    updateTelemetryMode() {\n      this.telemetryMode =\n        this.telemetryMode === MODE.UNLIMITED ? MODE.PERFORMANCE : MODE.UNLIMITED;\n\n      if (this.persistModeChange) {\n        this.table.configuration.setTelemetryMode(this.telemetryMode);\n      }\n\n      this.table.updateTelemetryMode(this.telemetryMode);\n\n      const timeSystemKey = this.openmct.time.getTimeSystem().key;\n\n      if (this.telemetryMode === MODE.PERFORMANCE && this.sortOptions.key !== timeSystemKey) {\n        this.openmct.notifications.info(\n          'Switched to Performance Mode: Table now sorted by time for optimized efficiency.'\n        );\n        this.initiateSort(timeSystemKey);\n      }\n    },\n    setRowHeight(height) {\n      this.rowHeight = height;\n      this.setHeight();\n      this.calculateTableSize();\n      this.clearRowsAndRerender();\n    },\n    updateViewContext(rowContext) {\n      this.rowContext = rowContext;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableConfiguration.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-inspect-properties\">\n    <div class=\"c-inspect-properties__header\">Layout</div>\n    <ul class=\"c-inspect-properties__section\">\n      <li class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Auto-size table\">\n          <label for=\"AutoSizeControl\">Auto-size</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            id=\"AutoSizeControl\"\n            type=\"checkbox\"\n            :checked=\"configuration.autosize !== false\"\n            @change=\"toggleAutosize()\"\n          />\n          <span v-if=\"!isEditing && configuration.autosize !== false\">Enabled</span>\n        </div>\n      </li>\n      <li class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Show or hide headers\">\n          <label for=\"header-visibility\">Hide Header</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            id=\"header-visibility\"\n            type=\"checkbox\"\n            :checked=\"configuration.hideHeaders === true\"\n            @change=\"toggleHeaderVisibility\"\n          />\n          <span v-if=\"!isEditing && configuration.hideHeaders === true\">True</span>\n        </div>\n      </li>\n    </ul>\n    <div class=\"c-inspect-properties__header\">Columns</div>\n    <ul class=\"c-inspect-properties__section\">\n      <li v-for=\"(title, key) in headers\" :key=\"key\" class=\"c-inspect-properties__row\">\n        <div class=\"c-inspect-properties__label\" title=\"Show or hide column\">\n          <label :for=\"key + 'ColumnControl'\">{{ title }}</label>\n        </div>\n        <div class=\"c-inspect-properties__value\">\n          <input\n            v-if=\"isEditing\"\n            :id=\"key + 'ColumnControl'\"\n            type=\"checkbox\"\n            :checked=\"configuration.hiddenColumns[key] !== true\"\n            @change=\"toggleColumn(key)\"\n          />\n          <span v-if=\"!isEditing && configuration.hiddenColumns[key] !== true\">Visible</span>\n        </div>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport TelemetryTableColumn from '../TelemetryTableColumn.js';\nimport TelemetryTableUnitColumn from '../TelemetryTableUnitColumn.js';\n\nexport default {\n  inject: ['tableConfiguration', 'openmct'],\n  data() {\n    return {\n      headers: {},\n      isEditing: this.openmct.editor.isEditing(),\n      configuration: this.tableConfiguration.getConfiguration()\n    };\n  },\n  async mounted() {\n    this.unlisteners = [];\n    this.openmct.editor.on('isEditing', this.toggleEdit);\n    const compositionCollection = this.openmct.composition.get(\n      this.tableConfiguration.domainObject\n    );\n\n    const composition = await compositionCollection.load();\n    this.addColumnsForAllObjects(composition);\n    this.updateHeaders(this.tableConfiguration.getAllHeaders());\n\n    compositionCollection.on('add', this.addObject);\n    this.unlisteners.push(\n      compositionCollection.off.bind(compositionCollection, 'add', this.addObject)\n    );\n\n    compositionCollection.on('remove', this.removeObject);\n    this.unlisteners.push(\n      compositionCollection.off.bind(compositionCollection, 'remove', this.removeObject)\n    );\n  },\n  unmounted() {\n    this.tableConfiguration.destroy();\n    this.openmct.editor.off('isEditing', this.toggleEdit);\n    this.unlisteners.forEach((unlisten) => unlisten());\n  },\n  methods: {\n    updateHeaders(headers) {\n      // add name column if it doesn't exist,\n      // it's always the first column when it's manually added\n      if (!headers.name) {\n        headers = {\n          name: 'Name',\n          ...headers\n        };\n      }\n\n      this.headers = headers;\n    },\n    toggleColumn(key) {\n      let isHidden = this.configuration.hiddenColumns[key] === true;\n\n      this.configuration.hiddenColumns[key] = !isHidden;\n      this.tableConfiguration.updateConfiguration(this.configuration);\n    },\n    addObject(domainObject) {\n      this.addColumnsForObject(domainObject, true);\n      this.updateHeaders(this.tableConfiguration.getAllHeaders());\n    },\n    removeObject(objectIdentifier) {\n      this.tableConfiguration.removeColumnsForObject(objectIdentifier, true);\n      this.updateHeaders(this.tableConfiguration.getAllHeaders());\n    },\n    toggleEdit(isEditing) {\n      this.isEditing = isEditing;\n    },\n    toggleAutosize() {\n      this.configuration.autosize = !this.configuration.autosize;\n      this.tableConfiguration.updateConfiguration(this.configuration);\n    },\n    addColumnsForAllObjects(objects) {\n      objects.forEach((object) => this.addColumnsForObject(object, false));\n    },\n    addColumnsForObject(telemetryObject) {\n      const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n      let metadataValues = metadata ? metadata.values() : [];\n      metadataValues.forEach((metadatum) => {\n        let column = new TelemetryTableColumn(this.openmct, metadatum);\n        this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);\n        // if units are available, need to add columns to be hidden\n        if (metadatum.unit !== undefined) {\n          let unitColumn = new TelemetryTableUnitColumn(this.openmct, metadatum);\n          this.tableConfiguration.addSingleColumnForObject(telemetryObject, unitColumn);\n        }\n      });\n    },\n    toggleHeaderVisibility() {\n      let hideHeaders = this.configuration.hideHeaders;\n\n      this.configuration.hideHeaders = !hideHeaders;\n      this.tableConfiguration.updateConfiguration(this.configuration);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableFooterIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-table-indicator\" :class=\"{ 'is-filtering': filterNames.length > 0 }\">\n    <div\n      v-if=\"filterNames.length > 0\"\n      class=\"c-table-indicator__filter c-table-indicator__elem c-filter-indication\"\n      :class=\"{ 'c-filter-indication--mixed': hasMixedFilters }\"\n      :aria-label=\"title\"\n      :title=\"title\"\n    >\n      <span class=\"c-filter-indication__mixed\">{{ label }}</span>\n      <span v-for=\"(name, index) in filterNames\" :key=\"index\" class=\"c-filter-indication__label\">\n        {{ name }}\n      </span>\n    </div>\n\n    <div class=\"c-table-indicator__counts\">\n      <span\n        :aria-label=\"rowCountTitle\"\n        :title=\"rowCountTitle\"\n        class=\"c-table-indicator__elem c-table-indicator__row-count\"\n      >\n        {{ rowCount }}\n      </span>\n\n      <span\n        v-if=\"markedRows\"\n        class=\"c-table-indicator__elem c-table-indicator__marked-count\"\n        :aria-label=\"markedRows + ' rows selected'\"\n        :title=\"markedRows + ' rows selected'\"\n      >\n        {{ markedRows }} Marked\n      </span>\n\n      <button :title=\"telemetryModeButtonTitle\" class=\"c-button\" @click=\"toggleTelemetryMode\">\n        {{ telemetryModeButtonLabel }}\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport { MODE } from '../constants.js';\n\nconst FILTER_INDICATOR_LABEL = 'Filters:';\nconst FILTER_INDICATOR_LABEL_MIXED = 'Mixed Filters:';\nconst FILTER_INDICATOR_TITLE = 'Data filters are being applied to this view.';\nconst FILTER_INDICATOR_TITLE_MIXED = 'A mix of data filter values are being applied to this view.';\nconst USE_GLOBAL = 'useGlobal';\n\nexport default {\n  inject: ['openmct', 'table'],\n  props: {\n    markedRows: {\n      type: Number,\n      default: 0\n    },\n    totalRows: {\n      type: Number,\n      default: 0\n    },\n    telemetryMode: {\n      type: String,\n      default: MODE.PERFORMANCE\n    }\n  },\n  emits: ['telemetry-mode-change'],\n  data() {\n    return {\n      filterNames: [],\n      filteredTelemetry: {}\n    };\n  },\n  computed: {\n    hasMixedFilters() {\n      let filtersToCompare = _.omit(\n        this.filteredTelemetry[Object.keys(this.filteredTelemetry)[0]],\n        [USE_GLOBAL]\n      );\n\n      return Object.values(this.filteredTelemetry).some((filters) => {\n        return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL]));\n      });\n    },\n    isUnlimitedMode() {\n      return this.telemetryMode === MODE.UNLIMITED;\n    },\n    label() {\n      if (this.hasMixedFilters) {\n        return FILTER_INDICATOR_LABEL_MIXED;\n      } else {\n        return FILTER_INDICATOR_LABEL;\n      }\n    },\n    rowCount() {\n      return this.isUnlimitedMode ? `${this.totalRows} ROWS` : `LATEST ${this.totalRows} ROWS`;\n    },\n    rowCountTitle() {\n      return this.isUnlimitedMode\n        ? this.totalRows + ' rows visible after any filtering'\n        : 'performance mode limited to 50 rows';\n    },\n    telemetryModeButtonLabel() {\n      return this.isUnlimitedMode ? 'SHOW LIMITED' : 'SHOW UNLIMITED';\n    },\n    telemetryModeButtonTitle() {\n      return this.isUnlimitedMode\n        ? 'Change to Limited (Performance) Mode'\n        : 'Change to Unlimited Mode';\n    },\n    title() {\n      if (this.hasMixedFilters) {\n        return FILTER_INDICATOR_TITLE_MIXED;\n      } else {\n        return FILTER_INDICATOR_TITLE;\n      }\n    }\n  },\n  mounted() {\n    let filters = this.table.configuration.getConfiguration().filters || {};\n    this.table.configuration.on('change', this.handleConfigurationChanges);\n    this.updateFilters(filters);\n  },\n  unmounted() {\n    this.table.configuration.off('change', this.handleConfigurationChanges);\n  },\n  methods: {\n    toggleTelemetryMode() {\n      this.$emit('telemetry-mode-change');\n    },\n    setFilterNames() {\n      let names = [];\n      let composition = this.openmct.composition.get(this.table.configuration.domainObject);\n      if (composition !== undefined) {\n        composition.load().then((domainObjects) => {\n          domainObjects.forEach((telemetryObject) => {\n            let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);\n            const metadata = this.openmct.telemetry.getMetadata(telemetryObject);\n            let metadataValues = metadata ? metadata.values() : [];\n            let filters = this.filteredTelemetry[keyString];\n\n            if (filters !== undefined) {\n              names.push(this.getFilterNamesFromMetadata(filters, metadataValues));\n            }\n          });\n\n          names = _.flatten(names);\n          this.filterNames = names.length === 0 ? names : Array.from(new Set(names));\n        });\n      }\n    },\n    getFilterNamesFromMetadata(filters, metadataValues) {\n      let filterNames = [];\n      filters = _.omit(filters, [USE_GLOBAL]);\n\n      Object.keys(filters).forEach((key) => {\n        if (!_.isEmpty(filters[key])) {\n          metadataValues.forEach((metadatum) => {\n            if (key === metadatum.key) {\n              if (typeof metadatum.filters[0] === 'object') {\n                filterNames.push(this.getFilterLabels(filters[key], metadatum));\n              } else {\n                filterNames.push(metadatum.name);\n              }\n            }\n          });\n        }\n      });\n\n      return _.flatten(filterNames);\n    },\n    getFilterLabels(filterObject, metadatum) {\n      let filterLabels = [];\n\n      Object.values(filterObject).forEach((comparator) => {\n        if (typeof comparator !== 'string') {\n          comparator.forEach((filterValue) => {\n            metadatum.filters[0].possibleValues.forEach((option) => {\n              if (option.value === filterValue) {\n                filterLabels.push(option.label);\n              }\n            });\n          });\n        } else {\n          filterLabels.push(comparator);\n        }\n      });\n\n      return filterLabels;\n    },\n    handleConfigurationChanges(configuration) {\n      if (!_.eq(this.filteredTelemetry, configuration.filters)) {\n        this.updateFilters(configuration.filters || {});\n      }\n    },\n    updateFilters(filters) {\n      this.filteredTelemetry = JSON.parse(JSON.stringify(filters));\n      this.setFilterNames();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/TableRow.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <tr\n    :style=\"{ top: rowTop }\"\n    class=\"noselect\"\n    :class=\"[rowClass, { 'is-selected': marked }]\"\n    :aria-label=\"ariaLabel\"\n    v-on=\"listeners\"\n  >\n    <component\n      :is=\"componentList[key]\"\n      v-for=\"(title, key) in headers\"\n      :key=\"key\"\n      :column-key=\"key\"\n      :style=\"rowStyle(key)\"\n      :class=\"[cellLimitClasses[key], selectableColumns[key] ? 'is-selectable' : '']\"\n      :object-path=\"objectPath\"\n      :row=\"row\"\n    />\n  </tr>\n</template>\n\n<script>\nimport TableCell from './TableCell.vue';\n\nexport default {\n  components: {\n    TableCell\n  },\n  inject: ['openmct', 'currentView'],\n  props: {\n    headers: {\n      type: Object,\n      required: true\n    },\n    row: {\n      type: Object,\n      required: true\n    },\n    columnWidths: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    },\n    rowIndex: {\n      type: Number,\n      required: false,\n      default: undefined\n    },\n    rowOffset: {\n      type: Number,\n      required: false,\n      default: 0\n    },\n    rowHeight: {\n      type: Number,\n      required: false,\n      default: 0\n    },\n    marked: {\n      type: Boolean,\n      required: false,\n      default: false\n    }\n  },\n  emits: ['mark-multiple-concurrent', 'unmark', 'mark', 'row-context-click'],\n  data: function () {\n    return {\n      rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',\n      rowClass: this.row.getRowClass(),\n      cellLimitClasses: this.row.getCellLimitClasses(),\n      selectableColumns: Object.keys(this.row.columns).reduce((selectable, columnKeys) => {\n        selectable[columnKeys] = this.row.columns[columnKeys].selectable;\n\n        return selectable;\n      }, {})\n    };\n  },\n  computed: {\n    ariaLabel() {\n      return this.marked ? 'Selected Table Row' : 'Table Row';\n    },\n    listeners() {\n      let listenersObject = {\n        click: this.markRow\n      };\n\n      if (this.row.getContextMenuActions().length) {\n        listenersObject.contextmenu = this.showContextMenu;\n      }\n\n      return listenersObject;\n    },\n    componentList() {\n      return Object.keys(this.headers).reduce((components, header) => {\n        components[header] = this.row.getCellComponentName(header) || 'table-cell';\n\n        return components;\n      }, {});\n    }\n  },\n  // TODO: use computed properties\n  watch: {\n    rowOffset: 'calculateRowTop',\n    row: {\n      handler: 'formatRow',\n      deep: true\n    }\n  },\n  methods: {\n    calculateRowTop: function (rowOffset) {\n      this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px';\n    },\n    formatRow: function (row) {\n      this.rowClass = row.getRowClass();\n      this.cellLimitClasses = row.getCellLimitClasses();\n    },\n    markRow: function (event) {\n      let keyCtrlModifier = false;\n\n      if (event.ctrlKey || event.metaKey) {\n        keyCtrlModifier = true;\n      }\n\n      if (event.shiftKey) {\n        this.$emit('mark-multiple-concurrent', this.rowIndex);\n      } else {\n        if (this.marked) {\n          this.$emit('unmark', this.rowIndex, keyCtrlModifier);\n        } else {\n          this.$emit('mark', this.rowIndex, keyCtrlModifier);\n        }\n      }\n    },\n    selectCell(element, columnKey) {\n      if (this.selectableColumns[columnKey]) {\n        //TODO: This is a hack. Cannot get parent this way.\n        this.openmct.selection.select(\n          [\n            {\n              element: element,\n              context: {\n                type: 'table-cell',\n                row: this.row.objectKeyString,\n                column: columnKey\n              }\n            },\n            {\n              element: this.openmct.layout.$refs.browseObject.$el,\n              context: {\n                item: this.openmct.router.path[0]\n              }\n            }\n          ],\n          false\n        );\n        event.stopPropagation();\n      }\n    },\n    getDatum() {\n      return this.row.fullDatum;\n    },\n    rowStyle(key) {\n      return this.columnWidths[key] === undefined\n        ? {}\n        : { width: this.columnWidths[key] + 'px', 'max-width': this.columnWidths[key] + 'px' };\n    },\n    showContextMenu: async function (event) {\n      event.preventDefault();\n\n      this.updateViewContext();\n      this.markRow(event);\n\n      const contextualDomainObject = await this.row.getContextualDomainObject?.(\n        this.openmct,\n        this.row.objectKeyString\n      );\n\n      let objectPath = this.objectPath;\n      if (contextualDomainObject) {\n        objectPath = objectPath.slice();\n        objectPath.unshift(contextualDomainObject);\n      }\n\n      const actions = this.row\n        .getContextMenuActions()\n        .map((key) => this.openmct.actions.getAction(key));\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        actions,\n        objectPath,\n        this.currentView\n      );\n      if (menuItems.length) {\n        this.openmct.menus.showMenu(event.x, event.y, menuItems);\n      }\n    },\n    updateViewContext() {\n      this.$emit('row-context-click', {\n        viewHistoricalData: true,\n        viewDatumAction: true,\n        getDatum: this.getDatum\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/table-footer-indicator.scss",
    "content": ".c-table-indicator {\n  display: flex;\n  align-items: center;\n  font-size: 0.9em;\n  overflow: hidden;\n\n  &__elem {\n    @include ellipsize();\n    flex: 0 1 auto;\n    padding: 2px;\n    text-transform: uppercase;\n\n    > * {\n      //display: contents;\n    }\n  }\n\n  &__counts {\n    //background: rgba(deeppink, 0.1);\n    display: flex;\n    align-items: center;\n    flex: 1 1 auto;\n    justify-content: flex-end;\n    overflow: hidden;\n\n    > * {\n      margin-left: $interiorMargin;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/table-row.scss",
    "content": ".noselect {\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none; /* Safari */\n  -khtml-user-select: none; /* Konqueror HTML */\n  -moz-user-select: none; /* Firefox */\n  -ms-user-select: none; /* Internet Explorer/Edge */\n  user-select: none; /* Non-prefixed version, currently\n                                supported by Chrome and Opera */\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/components/table.scss",
    "content": ".c-telemetry-table__drop-target {\n  position: absolute;\n  width: 2px;\n  background-color: $editUIColor;\n  box-shadow: rgba($editUIColor, 0.5) 0 0 10px;\n  z-index: 1;\n  pointer-events: none;\n}\n\n.c-telemetry-table {\n  // Table that displays telemetry in a scrolling body area\n\n  @include fontAndSize();\n\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: flex-start;\n  overflow: hidden;\n\n  th,\n  td {\n    display: block;\n    flex: 1 0 auto;\n    width: 100px;\n    vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default\n  }\n\n  /******************************* WRAPPERS */\n  &__headers-w {\n    // Wraps __headers table\n    flex: 0 0 auto;\n    overflow: hidden;\n    background: $colorTabHeaderBg;\n  }\n\n  /******************************* TABLES */\n  &__headers,\n  &__body {\n    tr {\n      display: flex;\n      align-items: stretch;\n    }\n  }\n\n  &__headers {\n    // A table\n    thead {\n      display: block;\n    }\n\n    &__labels {\n      // Top row, has labels\n      .c-telemetry-table__headers__content {\n        // Holds __label, sort indicator and resize-hitarea\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        width: 100%;\n      }\n    }\n\n    &__filter {\n      .c-table__search {\n        padding-top: 0;\n        padding-bottom: 0;\n      }\n\n      .--width-less-than-600 & {\n        display: none !important;\n      }\n    }\n  }\n\n  &__headers__label {\n    overflow: hidden;\n    flex: 0 1 auto;\n  }\n\n  &__resize-hitarea {\n    // In table-column-header.vue\n    @include abs();\n    display: none; // Set to display: block in .is-editing section below\n    left: auto;\n    right: -1 * $tabularTdPadLR;\n    width: $tableResizeColHitareaD;\n    cursor: col-resize;\n    transform: translateX(50%); // Move so this element sits over border between columns\n  }\n\n  /******************************* ELEMENTS */\n  &__scroll-forcer {\n    // Force horz scroll when needed; width set via JS\n    font-size: 0;\n    height: 1px; // Height 0 won't force scroll properly\n    position: relative;\n  }\n\n  &__progress-bar {\n    margin-bottom: 3px;\n  }\n\n  /******************************* WRAPPERS */\n  &__body-w {\n    // Wraps __body table provides scrolling\n    flex: 1 1 100%;\n    height: 0; // Fixes Chrome 73 overflow bug\n    overflow-x: auto;\n    overflow-y: scroll;\n  }\n\n  /******************************* TABLES */\n  &__body {\n    // A table\n    flex: 1 1 100%;\n    overflow-x: auto;\n\n    tr {\n      display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define\n      align-items: stretch;\n      position: absolute;\n      min-height: 18px; // Needed when a row has empty values in its cells\n\n      .is-editing .l-layout__frame & {\n        pointer-events: none;\n      }\n    }\n\n    td {\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n  }\n\n  &__sizing {\n    // A table\n    display: table;\n    z-index: -1;\n    visibility: hidden;\n    pointer-events: none;\n    position: absolute;\n\n    //Add some padding to allow for decorations such as limits indicator\n    tr {\n      display: table-row;\n    }\n\n    th,\n    td {\n      display: table-cell;\n      padding-right: 10px;\n      padding-left: 10px;\n      white-space: nowrap;\n    }\n  }\n\n  &__sizing-tr {\n    // A row element used to determine sizing of rows based on font size\n    visibility: hidden;\n    pointer-events: none;\n  }\n\n  &__footer {\n    margin-top: $interiorMargin;\n    margin-bottom: $interiorMarginSm;\n    overflow: hidden;\n\n    .c-frame & {\n      .c-button {\n        padding: 2px 5px;\n      }\n    }\n  }\n}\n\n// All tables\ntd {\n  @include isLimit();\n}\n\n.c-table tr {\n  &[s-selected],\n  &.is-selected {\n    background-color: $colorSelectedBg !important;\n    color: $colorSelectedFg !important;\n    td {\n      background: none !important;\n      color: inherit !important;\n    }\n  }\n}\n\n/******************************* SPECIFIC CASE WRAPPERS */\n.is-editing {\n  .c-telemetry-table__headers__labels {\n    th[draggable],\n    th[draggable] > * {\n      cursor: move;\n    }\n\n    th[draggable]:hover {\n      $b: $editFrameHovMovebarColorBg;\n      background: $b;\n      > * {\n        background: $b;\n      }\n    }\n  }\n\n  .c-telemetry-table__resize-hitarea {\n    display: block;\n  }\n}\n\n.is-paused {\n  .c-table__body-w {\n    border: 1px solid rgba($colorPausedBg, 0.8);\n  }\n}\n\n/******************************* LEGACY */\n.s-status-taking-snapshot,\n.overlay.snapshot {\n  .c-table {\n    &__body-w {\n      overflow: auto; // Handle overflow-y issues with tables and html2canvas\n    }\n\n    &-control-bar {\n      display: none;\n      + * {\n        margin-top: 0 !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/constants.js",
    "content": "const ORDER = {\n  ASCENDING: 'asc',\n  DESCENDING: 'desc'\n};\n\nconst MODE = {\n  PERFORMANCE: 'performance',\n  UNLIMITED: 'unlimited'\n};\n\nexport { MODE, ORDER };\n"
  },
  {
    "path": "src/plugins/telemetryTable/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { MODE } from './constants.js';\nimport TableConfigurationViewProvider from './TableConfigurationViewProvider.js';\nimport telemetryTableStylesInterceptor from './telemetryTableStylesInterceptor.js';\nimport getTelemetryTableType from './TelemetryTableType.js';\nimport TelemetryTableViewProvider from './TelemetryTableViewProvider.js';\nimport TelemetryTableViewActions from './ViewActions.js';\n\nexport default function plugin(\n  options = { telemetryMode: MODE.PERFORMANCE, persistModeChange: true, rowLimit: 50 }\n) {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));\n    openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));\n    openmct.types.addType('table', getTelemetryTableType(options));\n    openmct.objects.addGetInterceptor(telemetryTableStylesInterceptor(openmct));\n    openmct.composition.addPolicy((parent, child) => {\n      if (parent.type === 'table') {\n        return Object.prototype.hasOwnProperty.call(child, 'telemetry');\n      } else {\n        return true;\n      }\n    });\n\n    TelemetryTableViewActions.forEach((action) => {\n      openmct.actions.register(action);\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/telemetryTable/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport {\n  createMouseEvent,\n  createOpenMct,\n  renderWhenVisible,\n  resetApplicationState,\n  spyOnBuiltins\n} from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { MODE } from './constants.js';\nimport TablePlugin from './plugin.js';\n\nclass MockDataTransfer {\n  constructor() {\n    this.data = {};\n  }\n  get types() {\n    return Object.keys(this.data);\n  }\n  setData(format, data) {\n    this.data[format] = data;\n  }\n  getData(format) {\n    return this.data[format];\n  }\n}\n\ndescribe('the plugin', () => {\n  let openmct;\n  let tablePlugin;\n  let element;\n  let child;\n  let historicalTelemetryProvider;\n  let originalRouterPath;\n  let unlistenConfigMutation;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    // Table Plugin is actually installed by default, but because installing it\n    // again is harmless it is left here as an example for non-default plugins.\n    tablePlugin = new TablePlugin();\n    openmct.install(tablePlugin);\n\n    historicalTelemetryProvider = {\n      request: () => {\n        return Promise.resolve([]);\n      }\n    };\n    spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalTelemetryProvider);\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 4\n    });\n\n    openmct.types.addType('test-object', {\n      creatable: true\n    });\n\n    spyOnBuiltins(['requestAnimationFrame']);\n    window.requestAnimationFrame.and.callFake((callBack) => {\n      callBack();\n    });\n\n    originalRouterPath = openmct.router.path;\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    openmct.time.timeSystem('utc', {\n      start: 0,\n      end: 1\n    });\n\n    if (unlistenConfigMutation) {\n      unlistenConfigMutation();\n    }\n\n    return resetApplicationState(openmct);\n  });\n\n  describe('defines a table object', function () {\n    it('that is creatable', () => {\n      let tableType = openmct.types.get('table');\n      expect(tableType.definition.creatable).toBe(true);\n    });\n  });\n\n  it('provides a table view for objects with telemetry', () => {\n    const testTelemetryObject = {\n      id: 'test-object',\n      type: 'test-object',\n      telemetry: {\n        values: [\n          {\n            key: 'some-key'\n          }\n        ]\n      }\n    };\n\n    const applicableViews = openmct.objectViews.get(testTelemetryObject, []);\n    let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table');\n    expect(tableView).toBeDefined();\n  });\n\n  describe('The table view', () => {\n    let testTelemetryObject;\n    let applicableViews;\n    let tableViewProvider;\n    let tableView;\n    let tableInstance;\n    let mockClock;\n    let telemetryCallback;\n\n    beforeEach(async () => {\n      openmct.time.timeSystem('utc', {\n        start: 0,\n        end: 10\n      });\n\n      mockClock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);\n      mockClock.key = 'mockClock';\n      mockClock.currentValue.and.returnValue(1);\n\n      openmct.time.addClock(mockClock);\n      openmct.time.clock('mockClock', {\n        start: 0,\n        end: 10\n      });\n\n      testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'utc',\n              format: 'utc',\n              name: 'Time',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 2\n              }\n            }\n          ]\n        },\n        configuration: {\n          hiddenColumns: {\n            name: false,\n            utc: false,\n            'some-key': false,\n            'some-other-key': false\n          },\n          persistModeChange: true,\n          rowLimit: 50,\n          telemetryMode: MODE.PERFORMANCE\n        }\n      };\n      const testTelemetry = [\n        {\n          utc: 1,\n          'some-key': 'some-value 1',\n          'some-other-key': 'some-other-value 1'\n        },\n        {\n          utc: 2,\n          'some-key': 'some-value 2',\n          'some-other-key': 'some-other-value 2'\n        },\n        {\n          utc: 3,\n          'some-key': 'some-value 3',\n          'some-other-key': 'some-other-value 3'\n        }\n      ];\n\n      historicalTelemetryProvider.request = () => {\n        return Promise.resolve(testTelemetry);\n      };\n\n      const realtimeTelemetryProvider = {\n        supportsSubscribe: () => true,\n        subscribe: (domainObject, passedCallback) => {\n          telemetryCallback = passedCallback;\n          return Promise.resolve(() => {});\n        }\n      };\n\n      spyOn(openmct.telemetry, 'findSubscriptionProvider').and.returnValue(\n        realtimeTelemetryProvider\n      );\n\n      openmct.router.path = [testTelemetryObject];\n\n      applicableViews = openmct.objectViews.get(testTelemetryObject, []);\n      tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');\n      tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);\n      tableView.show(child, true, { renderWhenVisible });\n\n      tableInstance = tableView.getTable();\n\n      await nextTick();\n    });\n\n    afterEach(() => {\n      openmct.router.path = originalRouterPath;\n      openmct.time.setClock('local');\n    });\n\n    it('Shows no progress bar initially', () => {\n      let progressBar = element.querySelector('.c-progress-bar');\n\n      expect(tableInstance.outstandingRequests).toBe(0);\n      expect(progressBar).toBeNull();\n    });\n\n    it('Shows a progress bar while making requests', async () => {\n      tableInstance.incrementOutstandingRequests();\n      await nextTick();\n\n      let progressBar = element.querySelector('.c-progress-bar');\n\n      expect(tableInstance.outstandingRequests).toBe(1);\n      expect(progressBar).not.toBeNull();\n    });\n\n    it('Renders a row for every telemetry datum returned', async () => {\n      let rows = element.querySelectorAll('table.c-telemetry-table__body tr');\n      await nextTick();\n      expect(rows.length).toBe(3);\n    });\n\n    it('Adds a row in place when updating with existing telemetry', async () => {\n      let rows = element.querySelectorAll('table.c-telemetry-table__body tr');\n      await nextTick();\n      expect(rows.length).toBe(3);\n      // fire some telemetry\n      const newTelemetry = {\n        utc: 2,\n        'some-key': 'some-value 2',\n        'some-other-key': 'spacecraft'\n      };\n      spyOn(tableInstance.tableRows, 'getInPlaceUpdateIndex').and.returnValue(1);\n      spyOn(tableInstance.tableRows, 'updateRowInPlace').and.callThrough();\n      telemetryCallback(newTelemetry);\n\n      expect(tableInstance.tableRows.updateRowInPlace.calls.count()).toBeGreaterThan(0);\n    });\n\n    it('Renders a column for every item in telemetry metadata', () => {\n      let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');\n      expect(headers.length).toBe(4);\n      expect(headers[0].innerText).toBe('Name');\n      expect(headers[1].innerText).toBe('Time');\n      expect(headers[2].innerText).toBe('Some attribute');\n      expect(headers[3].innerText).toBe('Another attribute');\n    });\n\n    it('Supports column reordering via drag and drop', async () => {\n      let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');\n      let fromColumn = columns[0];\n      let toColumn = columns[1];\n      let fromColumnText = fromColumn.querySelector(\n        'span.c-telemetry-table__headers__label'\n      ).innerText;\n      let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText;\n\n      let dragStartEvent = createMouseEvent('dragstart');\n      let dragOverEvent = createMouseEvent('dragover');\n      let dropEvent = createMouseEvent('drop');\n\n      dragStartEvent.dataTransfer =\n        dragOverEvent.dataTransfer =\n        dropEvent.dataTransfer =\n          new MockDataTransfer();\n\n      fromColumn.dispatchEvent(dragStartEvent);\n      toColumn.dispatchEvent(dragOverEvent);\n      toColumn.dispatchEvent(dropEvent);\n\n      await nextTick();\n      columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');\n      let firstColumn = columns[0];\n      let secondColumn = columns[1];\n      let firstColumnText = firstColumn.querySelector(\n        'span.c-telemetry-table__headers__label'\n      ).innerText;\n      let secondColumnText = secondColumn.querySelector(\n        'span.c-telemetry-table__headers__label'\n      ).innerText;\n      expect(fromColumnText).not.toEqual(firstColumnText);\n      expect(fromColumnText).toEqual(secondColumnText);\n      expect(toColumnText).not.toEqual(secondColumnText);\n      expect(toColumnText).toEqual(firstColumnText);\n    });\n\n    it('displays the correct number of column headers when the configuration is mutated', async () => {\n      const tableInstanceConfiguration = tableInstance.domainObject.configuration;\n      tableInstanceConfiguration.hiddenColumns['some-key'] = true;\n      unlistenConfigMutation = tableInstance.openmct.objects.mutate(\n        tableInstance.domainObject,\n        'configuration',\n        tableInstanceConfiguration\n      );\n\n      await nextTick();\n      let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label');\n      expect(tableHeaderElements.length).toEqual(3);\n\n      tableInstanceConfiguration.hiddenColumns['some-key'] = false;\n      unlistenConfigMutation = tableInstance.openmct.objects.mutate(\n        tableInstance.domainObject,\n        'configuration',\n        tableInstanceConfiguration\n      );\n\n      await nextTick();\n      tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label');\n      expect(tableHeaderElements.length).toEqual(4);\n    });\n\n    it('displays the correct number of table cells in a row when the configuration is mutated', async () => {\n      const tableInstanceConfiguration = tableInstance.domainObject.configuration;\n      tableInstanceConfiguration.hiddenColumns['some-key'] = true;\n      unlistenConfigMutation = tableInstance.openmct.objects.mutate(\n        tableInstance.domainObject,\n        'configuration',\n        tableInstanceConfiguration\n      );\n\n      await nextTick();\n      let tableRowCells = element.querySelectorAll(\n        'table.c-telemetry-table__body > tbody > tr:first-child td'\n      );\n      expect(tableRowCells.length).toEqual(3);\n\n      tableInstanceConfiguration.hiddenColumns['some-key'] = false;\n      unlistenConfigMutation = tableInstance.openmct.objects.mutate(\n        tableInstance.domainObject,\n        'configuration',\n        tableInstanceConfiguration\n      );\n\n      await nextTick();\n      tableRowCells = element.querySelectorAll(\n        'table.c-telemetry-table__body > tbody > tr:first-child td'\n      );\n      expect(tableRowCells.length).toEqual(4);\n    });\n\n    it('Pauses the table when a row is marked', async () => {\n      let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');\n      let clickEvent = createMouseEvent('click');\n\n      // Mark a row\n      firstRow.dispatchEvent(clickEvent);\n\n      await nextTick();\n\n      // Verify table is paused\n      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();\n    });\n\n    it('Unpauses the table on user bounds change', async () => {\n      let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');\n      let clickEvent = createMouseEvent('click');\n\n      // Mark a row\n      firstRow.dispatchEvent(clickEvent);\n\n      await nextTick();\n\n      // Verify table is paused\n      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();\n\n      const currentBounds = openmct.time.bounds();\n      await nextTick();\n      const newBounds = {\n        start: currentBounds.start,\n        end: currentBounds.end - 3\n      };\n\n      // Manually change the time bounds\n      openmct.time.bounds(newBounds);\n      await nextTick();\n\n      // Verify table is no longer paused\n      expect(element.querySelector('div.c-table.is-paused')).toBeNull();\n    });\n\n    it('Unpauses the table on user bounds change if paused by button', async () => {\n      const viewContext = tableView.getViewContext();\n\n      // Pause by button\n      viewContext.togglePauseByButton();\n      await nextTick();\n\n      // Verify table is paused\n      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();\n\n      const currentBounds = openmct.time.bounds();\n      await nextTick();\n\n      const newBounds = {\n        start: currentBounds.start,\n        end: currentBounds.end - 1\n      };\n      // Manually change the time bounds\n      openmct.time.bounds(newBounds);\n\n      await nextTick();\n\n      // Verify table is no longer paused\n      expect(element.querySelector('div.c-table.is-paused')).toBeNull();\n    });\n\n    it('Does not unpause the table on tick', async () => {\n      const viewContext = tableView.getViewContext();\n\n      // Pause by button\n      viewContext.togglePauseByButton();\n\n      await nextTick();\n\n      // Verify table displays the correct number of rows\n      let tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');\n      expect(tableRows.length).toEqual(3);\n\n      // Verify table is paused\n      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();\n\n      // Tick the clock\n      openmct.time.tick(1);\n\n      await nextTick();\n\n      // Verify table is still paused\n      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();\n\n      await nextTick();\n\n      // Verify table displays the correct number of rows\n      tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');\n      expect(tableRows.length).toEqual(3);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/telemetryTable/telemetryTableStylesInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default function telemetryTableStylesInterceptor(openmct) {\n  return {\n    appliesTo: (identifier, domainObject) => {\n      return (\n        domainObject?.type === 'table' &&\n        domainObject.configuration && // only applies to tables with existing configuration\n        !domainObject.configuration.objectStyles\n      );\n    },\n    invoke: (identifier, domainObject) => {\n      domainObject.configuration.objectStyles = {};\n\n      return domainObject;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/themes/darkmatter-theme.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n \n@import '../../styles/vendor/normalize-min';\n@import '../../styles/constants';\n@import '../../styles/constants-mobile.scss';\n\n@import '../../styles/constants-darkmatter';\n\n@import '../../styles/mixins';\n@import '../../styles/animations';\n@import '../../styles/about';\n@import '../../styles/glyphs';\n@import '../../styles/global';\n@import '../../styles/status';\n@import '../../styles/limits';\n@import '../../styles/controls';\n@import '../../styles/forms';\n@import '../../styles/table';\n@import '../../styles/legacy';\n@import '../../styles/legacy-plots';\n@import '../../styles/plotly';\n@import '../../styles/legacy-messages';\n\n@import '../../styles/vue-styles.scss';\n"
  },
  {
    "path": "src/plugins/themes/darkmatter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n// Note: This darkmatter theme is in Beta and is not yet ready for prime time. It needs some more tweaking.\n\nimport { installTheme } from './installTheme.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    installTheme(openmct, 'darkmatter');\n  };\n}\n"
  },
  {
    "path": "src/plugins/themes/espresso-theme.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n \n@import '../../styles/vendor/normalize-min';\n@import '../../styles/constants';\n@import '../../styles/constants-mobile.scss';\n\n@import '../../styles/constants-espresso';\n\n@import '../../styles/mixins';\n@import '../../styles/animations';\n@import '../../styles/about';\n@import '../../styles/glyphs';\n@import '../../styles/global';\n@import '../../styles/status';\n@import '../../styles/limits';\n@import '../../styles/controls';\n@import '../../styles/forms';\n@import '../../styles/table';\n@import '../../styles/legacy';\n@import '../../styles/legacy-plots';\n@import '../../styles/plotly';\n@import '../../styles/legacy-messages';\n\n@import '../../styles/vue-styles.scss';\n"
  },
  {
    "path": "src/plugins/themes/espresso.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { installTheme } from './installTheme.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    installTheme(openmct, 'espresso');\n  };\n}\n"
  },
  {
    "path": "src/plugins/themes/installTheme.js",
    "content": "const dataAttribute = 'theme';\n\nexport function installTheme(openmct, themeName) {\n  const currentTheme = document.querySelector(`link[data-${dataAttribute}]`);\n  if (currentTheme) {\n    currentTheme.remove();\n  }\n\n  const newTheme = document.createElement('link');\n  newTheme.setAttribute('rel', 'stylesheet');\n\n  // eslint-disable-next-line no-undef\n  const href = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}${themeName}Theme.css`;\n  newTheme.setAttribute('href', href);\n  newTheme.dataset[dataAttribute] = themeName;\n\n  document.head.appendChild(newTheme);\n}\n"
  },
  {
    "path": "src/plugins/themes/snow-theme.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n \n@import '../../styles/vendor/normalize-min';\n@import '../../styles/constants';\n@import '../../styles/constants-mobile.scss';\n\n@import '../../styles/constants-snow';\n\n@import '../../styles/mixins';\n@import '../../styles/animations';\n@import '../../styles/about';\n@import '../../styles/glyphs';\n@import '../../styles/global';\n@import '../../styles/status';\n@import '../../styles/limits';\n@import '../../styles/controls';\n@import '../../styles/forms';\n@import '../../styles/table';\n@import '../../styles/legacy';\n@import '../../styles/legacy-plots';\n@import '../../styles/plotly';\n@import '../../styles/legacy-messages';\n\n@import '../../styles/vue-styles.scss';\n"
  },
  {
    "path": "src/plugins/themes/snow.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { installTheme } from './installTheme.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    installTheme(openmct, 'snow');\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorAxis.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"axisHolder\"\n    aria-label=\"Time Conductor Axis\"\n    class=\"c-conductor-axis\"\n    @mousedown=\"dragStart($event)\"\n  >\n    <div class=\"c-conductor-axis__zoom-indicator\" :style=\"zoomStyle\"></div>\n  </div>\n</template>\n\n<script>\nimport { axisTop } from 'd3-axis';\nimport { scaleLinear, scaleUtc } from 'd3-scale';\nimport { select } from 'd3-selection';\nimport { onMounted, ref } from 'vue';\n\nimport { useResizeObserver } from '../../../src/ui/composables/resize.js';\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\nimport utcMultiTimeFormat from './utcMultiTimeFormat.js';\n\nconst PADDING = 1;\nconst PIXELS_PER_TICK = 100;\nconst PIXELS_PER_TICK_WIDE = 200;\n\nexport default {\n  inject: ['openmct', 'isFixedTimeMode'],\n  props: {\n    viewBounds: {\n      type: Object,\n      required: true\n    },\n    altPressed: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['pan-axis', 'end-pan', 'zoom-axis', 'end-zoom'],\n  setup() {\n    const axisHolder = ref(null);\n    const { size: containerSize, startObserving } = useResizeObserver();\n\n    onMounted(() => {\n      startObserving(axisHolder.value);\n    });\n\n    return {\n      axisHolder,\n      containerSize\n    };\n  },\n  data() {\n    return {\n      inPanMode: false,\n      dragStartX: undefined,\n      dragX: undefined,\n      zoomStyle: {},\n      rect: null\n    };\n  },\n  computed: {\n    inZoomMode() {\n      return !this.inPanMode;\n    },\n    left() {\n      return this.rect.left;\n    },\n    panBounds() {\n      const bounds = this.openmct.time.getBounds();\n      const deltaTime = bounds.end - bounds.start;\n      const deltaX = this.dragX - this.dragStartX;\n      const percX = deltaX / this.width;\n      const panStart = bounds.start - percX * deltaTime;\n\n      return {\n        start: parseInt(panStart, 10),\n        end: parseInt(panStart + deltaTime, 10)\n      };\n    },\n    zoomRange() {\n      const leftBound = this.left;\n      const rightBound = this.left + this.width;\n      const zoomStart = this.dragX < leftBound ? leftBound : Math.min(this.dragX, this.dragStartX);\n      const zoomEnd = this.dragX > rightBound ? rightBound : Math.max(this.dragX, this.dragStartX);\n\n      return {\n        start: zoomStart,\n        end: zoomEnd\n      };\n    },\n    isChangingViewBounds() {\n      return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;\n    }\n  },\n  watch: {\n    containerSize: {\n      handler() {\n        this.resize();\n      },\n      deep: true\n    },\n    viewBounds: {\n      handler() {\n        this.setScale();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    const vis = select(this.axisHolder).append('svg:svg');\n\n    this.xAxis = axisTop();\n    this.dragging = false;\n\n    // draw x axis with labels. CSS is used to position them.\n    this.axisElement = vis.append('g').attr('class', 'axis');\n\n    this.setViewFromTimeSystem(this.openmct.time.getTimeSystem());\n    this.setAxisDimensions();\n    this.setScale();\n\n    //Respond to changes in conductor\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);\n  },\n  beforeUnmount() {\n    // Remove the listeners in case the component is unmounted while dragging\n    document.removeEventListener('mousemove', this.drag);\n    document.removeEventListener('mouseup', this.dragEnd);\n  },\n  methods: {\n    setAxisDimensions() {\n      this.rect = this.axisHolder.getBoundingClientRect();\n      this.width = this.axisHolder.clientWidth;\n    },\n    setScale() {\n      if (!this.width) {\n        return;\n      }\n\n      let timeSystem = this.openmct.time.getTimeSystem();\n\n      if (timeSystem.isUTCBased) {\n        this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);\n      } else {\n        this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);\n      }\n\n      this.xAxis.scale(this.xScale);\n\n      this.xScale.range([PADDING, this.width - PADDING * 2]);\n      this.axisElement.call(this.xAxis);\n\n      if (this.width > 1800) {\n        this.xAxis.ticks(this.width / PIXELS_PER_TICK_WIDE);\n      } else {\n        this.xAxis.ticks(this.width / PIXELS_PER_TICK);\n      }\n\n      this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;\n    },\n    setViewFromTimeSystem(timeSystem) {\n      //The D3 scale used depends on the type of time system as d3\n      // supports UTC out of the box.\n      if (timeSystem.isUTCBased) {\n        this.xScale = scaleUtc();\n      } else {\n        this.xScale = scaleLinear();\n      }\n\n      this.xAxis.scale(this.xScale);\n      this.xAxis.tickFormat(utcMultiTimeFormat);\n      this.axisElement.call(this.xAxis);\n      this.setScale();\n    },\n    dragStart($event) {\n      if (this.isFixedTimeMode) {\n        this.dragStartX = $event.clientX;\n\n        if (this.altPressed) {\n          this.inPanMode = true;\n        }\n\n        document.addEventListener('mousemove', this.drag);\n        document.addEventListener('mouseup', this.dragEnd, {\n          once: true\n        });\n\n        if (this.inZoomMode) {\n          this.startZoom();\n        }\n      }\n    },\n    drag($event) {\n      if (!this.dragging) {\n        this.dragging = true;\n\n        requestAnimationFrame(() => {\n          this.dragX = $event.clientX;\n\n          if (this.inPanMode) {\n            this.pan();\n          } else {\n            this.zoom();\n          }\n\n          this.dragging = false;\n        });\n      }\n    },\n    dragEnd() {\n      if (this.inPanMode) {\n        this.endPan();\n      } else {\n        this.endZoom();\n      }\n\n      document.removeEventListener('mousemove', this.drag);\n      this.dragStartX = undefined;\n      this.dragX = undefined;\n    },\n    pan() {\n      const panBounds = this.panBounds;\n      this.$emit('pan-axis', panBounds);\n    },\n    endPan() {\n      const panBounds = this.isChangingViewBounds ? this.panBounds : undefined;\n      this.$emit('end-pan', panBounds);\n      this.inPanMode = false;\n    },\n    startZoom() {\n      const x = this.scaleToBounds(this.dragStartX);\n\n      this.zoomStyle = {\n        left: `${this.dragStartX - this.left}px`\n      };\n\n      this.$emit('zoom-axis', {\n        start: x,\n        end: x\n      });\n    },\n    zoom() {\n      const zoomRange = this.zoomRange;\n\n      this.zoomStyle = {\n        left: `${zoomRange.start - this.left}px`,\n        width: `${zoomRange.end - zoomRange.start}px`\n      };\n\n      this.$emit('zoom-axis', {\n        start: this.scaleToBounds(zoomRange.start),\n        end: this.scaleToBounds(zoomRange.end)\n      });\n    },\n    endZoom() {\n      let zoomBounds;\n      if (this.isChangingViewBounds) {\n        const zoomRange = this.zoomRange;\n        zoomBounds = {\n          start: this.scaleToBounds(zoomRange.start),\n          end: this.scaleToBounds(zoomRange.end)\n        };\n      }\n\n      this.zoomStyle = {};\n      this.$emit('end-zoom', zoomBounds);\n    },\n    scaleToBounds(value) {\n      const bounds = this.openmct.time.getBounds();\n      const timeDelta = bounds.end - bounds.start;\n      const valueDelta = value - this.left;\n      const offset = (valueDelta / this.width) * timeDelta;\n\n      return parseInt(bounds.start + offset, 10);\n    },\n    resize() {\n      if (this.axisHolder.clientWidth !== this.width) {\n        this.setAxisDimensions();\n        this.setScale();\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorClock.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div v-if=\"readOnly === false\" ref=\"clockButton\" class=\"c-tc-input-popup__options\">\n    <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        class=\"c-button--menu js-clock-button c-icon-button\"\n        :class=\"selectedClock.cssClass\"\n        aria-label=\"Time Conductor Clock Menu\"\n        @click.prevent.stop=\"showClocksMenu\"\n      >\n        <span class=\"c-button__label\">{{ selectedClock.name }}</span>\n      </button>\n    </div>\n  </div>\n  <div v-else class=\"c-compact-tc__setting-value__elem\" aria-label=\"Time Conductor Clock\">\n    {{ selectedClock.name }}\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'configuration', 'clock', 'getAllClockMetadata', 'getClockMetadata'],\n  props: {\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  computed: {\n    selectedClock() {\n      return this.getClockMetadata(this.clock);\n    }\n  },\n  mounted() {\n    this.clocks = this.getAllClockMetadata(this.configuration.menuOptions);\n  },\n  methods: {\n    showClocksMenu() {\n      const elementBoundingClientRect = this.$refs.clockButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y;\n\n      const menuOptions = {\n        menuClass: 'c-conductor__clock-menu c-super-menu--sm',\n        placement: this.openmct.menus.menuPlacement.TOP_RIGHT\n      };\n\n      this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"timeConductorOptionsHolder\"\n    class=\"c-compact-tc is-expanded\"\n    :class=\"[\n      { 'is-zooming': isZooming },\n      { 'is-panning': isPanning },\n      { 'alt-pressed': altPressed },\n      isFixedTimeMode ? 'is-fixed-mode' : 'is-realtime-mode'\n    ]\"\n  >\n    <ConductorModeIcon class=\"c-conductor__mode-icon\" />\n    <div class=\"c-compact-tc__setting-value u-fade-truncate\">\n      <ConductorMode :read-only=\"true\" />\n      <ConductorClock :read-only=\"true\" />\n      <ConductorTimeSystem :read-only=\"true\" />\n    </div>\n    <ConductorInputsFixed v-if=\"isFixedTimeMode\" :read-only=\"true\" />\n    <ConductorInputsRealtime v-else :read-only=\"true\" />\n    <ConductorAxis\n      v-if=\"isFixedTimeMode\"\n      class=\"c-conductor__ticks\"\n      :view-bounds=\"viewBounds\"\n      :alt-pressed=\"altPressed\"\n      @end-pan=\"endPan\"\n      @end-zoom=\"endZoom\"\n      @pan-axis=\"pan\"\n      @zoom-axis=\"zoom\"\n    />\n    <ConductorHistory\n      class=\"c-conductor__history-select\"\n      title=\"Select and apply previously entered time intervals.\"\n    />\n    <ConductorPopUp\n      v-if=\"showConductorPopup\"\n      ref=\"conductorPopup\"\n      :bottom=\"false\"\n      :position-x=\"positionX\"\n      :position-y=\"positionY\"\n      @popup-loaded=\"initializePopup\"\n      @dismiss=\"clearPopup\"\n    />\n  </div>\n</template>\n\n<script>\nimport { inject, provide } from 'vue';\n\nimport ConductorAxis from './ConductorAxis.vue';\nimport ConductorClock from './ConductorClock.vue';\nimport ConductorHistory from './ConductorHistory.vue';\nimport ConductorInputsFixed from './ConductorInputsFixed.vue';\nimport ConductorInputsRealtime from './ConductorInputsRealtime.vue';\nimport ConductorMode from './ConductorMode.vue';\nimport ConductorModeIcon from './ConductorModeIcon.vue';\nimport ConductorPopUp from './ConductorPopUp.vue';\nimport conductorPopUpManager from './conductorPopUpManager.js';\nimport ConductorTimeSystem from './ConductorTimeSystem.vue';\nimport { useTime } from './useTime.js';\n\nexport default {\n  components: {\n    ConductorTimeSystem,\n    ConductorClock,\n    ConductorMode,\n    ConductorHistory,\n    ConductorInputsRealtime,\n    ConductorInputsFixed,\n    ConductorAxis,\n    ConductorModeIcon,\n    ConductorPopUp\n  },\n  mixins: [conductorPopUpManager],\n  setup(props) {\n    const openmct = inject('openmct');\n    const configuration = inject('configuration');\n\n    const {\n      timeContext,\n      timeSystemKey,\n      timeSystemFormatter,\n      timeSystemDurationFormatter,\n      isTimeSystemUTCBased,\n      timeMode,\n      isFixedTimeMode,\n      isRealTimeMode,\n      getAllModeMetadata,\n      getModeMetadata,\n      currentValue,\n      bounds,\n      isTick,\n      offsets,\n      clock,\n      getAllClockMetadata,\n      getClockMetadata\n    } = useTime(openmct, undefined, configuration);\n\n    provide('configuration', configuration);\n    provide('timeSystemKey', timeSystemKey);\n    provide('timeSystemFormatter', timeSystemFormatter);\n    provide('timeSystemDurationFormatter', timeSystemDurationFormatter);\n    provide('isTimeSystemUTCBased', isTimeSystemUTCBased);\n    provide('timeContext', timeContext);\n    provide('timeMode', timeMode);\n    provide('isFixedTimeMode', isFixedTimeMode);\n    provide('isRealTimeMode', isRealTimeMode);\n    provide('getAllModeMetadata', getAllModeMetadata);\n    provide('getModeMetadata', getModeMetadata);\n    provide('currentValue', currentValue);\n    provide('bounds', bounds);\n    provide('isTick', isTick);\n    provide('offsets', offsets);\n    provide('clock', clock);\n    provide('getAllClockMetadata', getAllClockMetadata);\n    provide('getClockMetadata', getClockMetadata);\n\n    return {\n      openmct,\n      timeSystemFormatter,\n      isFixedTimeMode,\n      bounds\n    };\n  },\n  data() {\n    return {\n      viewBounds: {\n        start: this.bounds.start,\n        end: this.bounds.end\n      },\n      showDatePicker: false,\n      showConductorPopup: false,\n      altPressed: false,\n      isPanning: false,\n      isZooming: false\n    };\n  },\n  computed: {\n    formattedBounds() {\n      return {\n        start: this.timeSystemFormatter.format(this.bounds.start),\n        end: this.timeSystemFormatter.format(this.bounds.end)\n      };\n    }\n  },\n  watch: {\n    bounds: {\n      handler() {\n        this.setViewBounds(this.bounds);\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    document.addEventListener('keydown', this.handleKeyDown);\n    document.addEventListener('keyup', this.handleKeyUp);\n  },\n  beforeUnmount() {\n    document.removeEventListener('keydown', this.handleKeyDown);\n    document.removeEventListener('keyup', this.handleKeyUp);\n  },\n  methods: {\n    handleKeyDown(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = true;\n      }\n    },\n    handleKeyUp(event) {\n      if (event.key === 'Alt') {\n        this.altPressed = false;\n      }\n    },\n    pan(bounds) {\n      this.isPanning = true;\n      this.setViewBounds(bounds);\n    },\n    endPan(bounds) {\n      this.isPanning = false;\n      if (bounds) {\n        this.openmct.time.setBounds(bounds);\n      }\n    },\n    zoom(bounds) {\n      if (isNaN(bounds.start) || isNaN(bounds.end)) {\n        this.isZooming = false;\n      } else {\n        this.isZooming = true;\n        this.formattedBounds.start = this.timeSystemFormatter.format(bounds.start);\n        this.formattedBounds.end = this.timeSystemFormatter.format(bounds.end);\n      }\n    },\n    endZoom(bounds) {\n      this.isZooming = false;\n      if (bounds) {\n        this.openmct.time.setBounds(bounds);\n      } else {\n        this.setViewBounds(this.bounds);\n      }\n    },\n    setViewBounds(bounds) {\n      this.viewBounds.start = bounds.start;\n      this.viewBounds.end = bounds.end;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorHistory.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"historyButton\" class=\"c-ctrl-wrapper c-ctrl-wrapper--menus-up\">\n    <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        aria-label=\"Time Conductor History\"\n        class=\"c-button--minor c-history-button icon-history c-icon-button\"\n        @click.prevent.stop=\"showHistoryMenu\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nconst DEFAULT_DURATION_FORMATTER = 'duration';\nconst LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';\nconst LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';\nconst DEFAULT_RECORDS_LENGTH = 10;\n\nimport { millisecondsToDHMS } from 'utils/duration';\n\nimport { REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\nimport UTCTimeFormat from '../utcTimeSystem/UTCTimeFormat.js';\n\nexport default {\n  inject: ['openmct', 'configuration'],\n  data() {\n    const mode = this.openmct.time.getMode();\n\n    return {\n      /**\n       * previous offsets entries available for easy re-use\n       * @realtimeHistory array of timespans\n       * @timespans {start, end} number representing offset\n       */\n      realtimeHistory: {},\n      /**\n       * previous bounds entries available for easy re-use\n       * @fixedHistory array of timespans\n       * @timespans {start, end} number representing timestamp\n       */\n      fixedHistory: {},\n      mode,\n      currentHistory: mode + 'History',\n      presets: [],\n      timeSystem: this.openmct.time.getTimeSystem(),\n      isFixed: this.openmct.time.isFixed()\n    };\n  },\n  computed: {\n    historyForCurrentTimeSystem() {\n      const history = this[this.currentHistory][this.timeSystem.key];\n\n      return history;\n    },\n    storageKey() {\n      let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;\n      if (this.mode === REALTIME_MODE_KEY) {\n        key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;\n      }\n\n      return key;\n    }\n  },\n  mounted() {\n    this.getHistoryFromLocalStorage();\n    this.loadConfiguration();\n\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);\n  },\n  beforeUnmount() {\n    this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);\n    this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);\n    this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);\n    this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);\n  },\n  methods: {\n    getHistoryMenuItems() {\n      const descriptionDateFormat = 'YYYY-MM-DD HH:mm:ss.SSS';\n      const history = this.historyForCurrentTimeSystem.map((timespan) => {\n        let name;\n        const startTime = this.formatTime(timespan.start);\n        const description = `${this.formatTime(\n          timespan.start,\n          descriptionDateFormat\n        )} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;\n\n        if (this.timeSystem.isUTCBased && !this.openmct.time.isRealTime()) {\n          name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;\n        } else {\n          name = description;\n        }\n\n        return {\n          cssClass: 'icon-history',\n          name,\n          description,\n          onItemClicked: () => this.selectTimespan(timespan)\n        };\n      });\n\n      history.unshift({\n        cssClass: 'c-menu__section-hint',\n        description: 'Past timeframes, ordered by latest first',\n        isDisabled: true,\n        name: 'Past timeframes, ordered by latest first',\n        onItemClicked: () => {}\n      });\n\n      return history;\n    },\n    getPresetMenuItems() {\n      return this.presets.map((preset) => {\n        return {\n          cssClass: 'icon-clock',\n          name: preset.label,\n          description: preset.label,\n          onItemClicked: () => this.selectPresetBounds(preset.bounds)\n        };\n      });\n    },\n    getHistoryFromLocalStorage() {\n      const localStorageHistory = localStorage.getItem(this.storageKey);\n      const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;\n      this[this.currentHistory] = history;\n\n      this.initializeHistoryIfNoHistory();\n    },\n    initializeHistoryIfNoHistory() {\n      if (!this[this.currentHistory]) {\n        this[this.currentHistory] = {};\n        this[this.currentHistory][this.timeSystem.key] = [];\n        this.persistHistoryToLocalStorage();\n      }\n    },\n    persistHistoryToLocalStorage() {\n      localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));\n    },\n    updateMode() {\n      this.mode = this.openmct.time.getMode();\n      this.currentHistory = this.mode + 'History';\n      this.loadConfiguration();\n      this.getHistoryFromLocalStorage();\n    },\n    updateTimeSystem(timeSystem) {\n      this.timeSystem = timeSystem;\n      this.loadConfiguration();\n      this.getHistoryFromLocalStorage();\n    },\n    addTimespan(bounds, isTick) {\n      if (isTick) {\n        return;\n      }\n\n      const key = this.timeSystem.key;\n      const isFixed = this.openmct.time.isFixed();\n      let [...currentHistory] = this.historyForCurrentTimeSystem || [];\n      const timespan = {\n        start: isFixed ? bounds.start : this.openmct.time.getClockOffsets().start,\n        end: isFixed ? bounds.end : this.openmct.time.getClockOffsets().end\n      };\n\n      // no dupes\n      currentHistory = currentHistory.filter(\n        (ts) => !(ts.start === timespan.start && ts.end === timespan.end)\n      );\n      currentHistory.unshift(timespan); // add to front\n\n      if (currentHistory.length > this.maxRecords) {\n        currentHistory.length = this.maxRecords;\n      }\n\n      this[this.currentHistory][key] = currentHistory;\n      this.persistHistoryToLocalStorage();\n    },\n    selectTimespan(timespan) {\n      if (this.openmct.time.isFixed()) {\n        this.openmct.time.setBounds(timespan);\n      } else {\n        this.openmct.time.setClockOffsets(timespan);\n      }\n    },\n    selectPresetBounds(bounds) {\n      const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;\n      const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;\n\n      this.selectTimespan({\n        start,\n        end\n      });\n    },\n    loadConfiguration() {\n      this.presets = this.getPresets();\n      this.maxRecords = this.getMaxRecords();\n    },\n    getPresets() {\n      const configurations = this.configuration.menuOptions.filter(\n        (option) => option.timeSystem === this.timeSystem.key\n      );\n\n      const configuration = configurations.find((option) => {\n        return option.presets && option.name.toLowerCase() === this.mode;\n      });\n      const presets = configuration ? configuration.presets : [];\n\n      return presets;\n    },\n    getMaxRecords() {\n      let maxRecords = this.configuration.records;\n\n      if (maxRecords === undefined) {\n        const configurations = this.configuration.menuOptions.filter(\n          (option) => option.timeSystem === this.timeSystem.key\n        );\n\n        const deprecatedUsageOfRecordsDefinedInSpecificMenuOption = configurations.find(\n          (option) => option.records\n        );\n        maxRecords = deprecatedUsageOfRecordsDefinedInSpecificMenuOption?.records;\n      }\n\n      return maxRecords ?? DEFAULT_RECORDS_LENGTH;\n    },\n    formatTime(time, utcDateFormat) {\n      let format = this.timeSystem.timeFormat;\n      let isNegativeOffset = false;\n\n      if (!this.openmct.time.isFixed()) {\n        if (time < 0) {\n          isNegativeOffset = true;\n        }\n\n        time = Math.abs(time);\n\n        format = this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER;\n      }\n\n      const formatter = this.openmct.telemetry.getValueFormatter({\n        format: format\n      }).formatter;\n\n      let formattedDate;\n\n      if (formatter instanceof UTCTimeFormat) {\n        const formatString = formatter.isValidFormatString(utcDateFormat)\n          ? utcDateFormat\n          : formatter.DATE_FORMATS.PRECISION_SECONDS;\n        formattedDate = formatter.format(time, formatString);\n      } else {\n        formattedDate = formatter.format(time);\n      }\n\n      return (isNegativeOffset ? '-' : '') + formattedDate;\n    },\n    showHistoryMenu() {\n      const elementBoundingClientRect = this.$refs.historyButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y;\n\n      const menuOptions = {\n        menuClass: 'c-conductor__history-menu',\n        placement: this.openmct.menus.menuPlacement.TOP_RIGHT\n      };\n\n      const menuActions = [];\n\n      const presets = this.getPresetMenuItems();\n      if (presets.length) {\n        menuActions.push(presets);\n      }\n\n      const history = this.getHistoryMenuItems();\n      menuActions.push(history);\n\n      this.openmct.menus.showMenu(x, y, menuActions, menuOptions);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorInputsFixed.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <DateTimePopupFixed\n    v-if=\"shouldUseSplitDateTimeInputs && !readOnly\"\n    @focus=\"$event.target.select()\"\n    @dismiss=\"dismiss\"\n  />\n  <TimePopupFixed v-else-if=\"!readOnly\" @focus=\"$event.target.select()\" @dismiss=\"dismiss\" />\n  <div v-else class=\"c-compact-tc__setting-wrapper\">\n    <div\n      class=\"c-compact-tc__setting-value u-fade-truncate--lg --no-sep\"\n      :title=\"`Start bounds: ${formattedBounds.start}`\"\n      :aria-label=\"`Start bounds: ${formattedBounds.start}`\"\n    >\n      {{ formattedBounds.start }}\n    </div>\n    <div class=\"c-compact-tc__bounds__start-end-sep icon-arrows-right-left\"></div>\n    <div\n      class=\"c-compact-tc__setting-value u-fade-truncate--lg --no-sep\"\n      :title=\"`End bounds: ${formattedBounds.end}`\"\n      :aria-label=\"`End bounds: ${formattedBounds.end}`\"\n    >\n      {{ formattedBounds.end }}\n    </div>\n  </div>\n</template>\n\n<script>\nimport DateTimePopupFixed from './DateTimePopupFixed.vue';\nimport TimePopupFixed from './TimePopupFixed.vue';\n\nexport default {\n  components: {\n    TimePopupFixed,\n    DateTimePopupFixed\n  },\n  inject: ['openmct', 'timeContext', 'bounds', 'timeSystemFormatter'],\n  props: {\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    compact: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['dismiss-inputs-fixed'],\n  computed: {\n    shouldUseSplitDateTimeInputs() {\n      return Boolean(this.timeSystemFormatter.formatDate);\n    },\n    formattedBounds() {\n      return {\n        start: this.timeSystemFormatter.format(this.bounds.start),\n        end: this.timeSystemFormatter.format(this.bounds.end)\n      };\n    }\n  },\n  methods: {\n    dismiss() {\n      this.$emit('dismiss-inputs-fixed');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorInputsRealtime.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <TimePopupRealtime\n    v-if=\"readOnly === false\"\n    :offsets=\"formattedOffsets\"\n    @focus=\"$event.target.select()\"\n    @dismiss=\"dismiss\"\n  />\n  <div v-else class=\"c-compact-tc__setting-wrapper\">\n    <div\n      v-if=\"!compact\"\n      class=\"c-compact-tc__setting-value icon-minus u-fade-truncate--lg --no-sep\"\n      :aria-label=\"`Start offset: ${formattedOffsets.start}`\"\n      :title=\"`Start offset: ${formattedOffsets.start}`\"\n    >\n      {{ formattedOffsets.start }}\n    </div>\n    <div v-if=\"!compact\" class=\"c-compact-tc__bounds__start-end-sep icon-arrows-right-left\"></div>\n    <div\n      v-if=\"!compact\"\n      class=\"c-compact-tc__setting-value icon-plus u-fade-truncate--lg\"\n      :class=\"{ '--no-sep': compact }\"\n      :aria-label=\"`End offset: ${formattedOffsets.end}`\"\n      :title=\"`End offset: ${formattedOffsets.end}`\"\n    >\n      {{ formattedOffsets.end }}\n    </div>\n    <div\n      class=\"c-compact-tc__setting-value icon-clock c-compact-tc__current-update u-fade-truncate--lg --no-sep\"\n      aria-label=\"Last update\"\n      title=\"Last update\"\n    >\n      {{ formattedCurrentValue }}\n    </div>\n    <div class=\"u-flex-spreader\"></div>\n  </div>\n</template>\n\n<script>\nimport TimePopupRealtime from './TimePopupRealtime.vue';\n\nexport default {\n  components: {\n    TimePopupRealtime\n  },\n  inject: [\n    'openmct',\n    'bounds',\n    'currentValue',\n    'clock',\n    'offsets',\n    'timeSystemFormatter',\n    'timeSystemDurationFormatter'\n  ],\n  props: {\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    compact: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['dismiss-inputs-realtime'],\n  computed: {\n    formattedOffsets() {\n      return {\n        start: this.timeSystemDurationFormatter.format(Math.abs(this.offsets.start)),\n        end: this.timeSystemDurationFormatter.format(Math.abs(this.offsets.end))\n      };\n    },\n    formattedCurrentValue() {\n      return this.timeSystemFormatter.format(this.currentValue);\n    }\n  },\n  methods: {\n    dismiss() {\n      this.$emit('dismiss-inputs-realtime');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorMode.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div v-if=\"readOnly === false\" ref=\"modeButton\" class=\"c-tc-input-popup__options\">\n    <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        class=\"c-button--menu js-mode-button c-icon-button\"\n        :class=\"selectedMode.cssClass\"\n        aria-label=\"Time Conductor Mode Menu\"\n        @click.prevent.stop=\"showModesMenu\"\n      >\n        <span class=\"c-button__label\">{{ selectedMode.name }}</span>\n      </button>\n    </div>\n  </div>\n  <div\n    v-else\n    role=\"button\"\n    class=\"c-compact-tc__setting-value__elem\"\n    aria-label=\"Time Conductor Mode\"\n  >\n    {{ selectedMode.name }}\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'timeMode', 'getAllModeMetadata', 'getModeMetadata'],\n  props: {\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  data() {\n    return {\n      selectedMode: this.getModeMetadata(this.timeMode)\n    };\n  },\n  watch: {\n    timeMode: {\n      handler() {\n        this.setView();\n      }\n    }\n  },\n  mounted() {\n    this.modes = this.getAllModeMetadata();\n  },\n  methods: {\n    showModesMenu() {\n      const elementBoundingClientRect = this.$refs.modeButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y;\n\n      const menuOptions = {\n        menuClass: 'c-conductor__mode-menu c-super-menu--sm',\n        placement: this.openmct.menus.menuPlacement.TOP_RIGHT\n      };\n\n      this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);\n    },\n    setView() {\n      this.selectedMode = this.getModeMetadata(this.timeMode);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorModeIcon.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-clock-symbol\">\n    <svg class=\"c-clock-symbol__outer\" viewBox=\"0 0 16 16\">\n      <path d=\"M6 0L3 0C1.34315 0 0 1.34315 0 3V13C0 14.6569 1.34315 16 3 16H6V13H3V3H6V0Z\" />\n      <path\n        d=\"M10 13H13V3H10V0H13C14.6569 0 16 1.34315 16 3V13C16 14.6569 14.6569 16 13 16H10V13Z\"\n      />\n    </svg>\n    <div class=\"hand-little\"></div>\n    <div class=\"hand-big\"></div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorPopUp.vue",
    "content": "<template>\n  <div ref=\"conductorPopupElement\" class=\"c-tc-input-popup\" :class=\"popupClasses\" :style=\"position\">\n    <div class=\"c-tc-input-popup__options\" aria-label=\"Time Conductor Options\">\n      <IndependentMode\n        v-if=\"isIndependent\"\n        class=\"c-conductor__mode-select\"\n        title=\"Sets the Time Conductor's mode.\"\n      />\n      <ConductorMode\n        v-else\n        class=\"c-conductor__mode-select\"\n        title=\"Sets the Time Conductor's mode.\"\n      />\n      <IndependentClock\n        v-if=\"isIndependent\"\n        class=\"c-conductor__mode-select\"\n        title=\"Sets the Time Conductor's clock.\"\n      />\n      <ConductorClock\n        v-else\n        class=\"c-conductor__mode-select\"\n        title=\"Sets the Time Conductor's clock.\"\n      />\n      <!-- TODO: Time system and history must work even with ITC later -->\n      <ConductorTimeSystem\n        v-if=\"!isIndependent\"\n        class=\"c-conductor__time-system-select\"\n        title=\"Sets the Time Conductor's time system.\"\n      />\n    </div>\n    <ConductorInputsFixed v-if=\"isFixedTimeMode\" @dismiss-inputs-fixed=\"dismiss\" />\n    <ConductorInputsRealtime v-else @dismiss-inputs-realtime=\"dismiss\" />\n  </div>\n</template>\n\n<script>\nimport { ref } from 'vue';\n\nimport ConductorClock from './ConductorClock.vue';\nimport ConductorInputsFixed from './ConductorInputsFixed.vue';\nimport ConductorInputsRealtime from './ConductorInputsRealtime.vue';\nimport ConductorMode from './ConductorMode.vue';\nimport ConductorTimeSystem from './ConductorTimeSystem.vue';\nimport IndependentClock from './independent/IndependentClock.vue';\nimport IndependentMode from './independent/IndependentMode.vue';\n\nexport default {\n  components: {\n    ConductorMode,\n    ConductorClock,\n    IndependentMode,\n    IndependentClock,\n    ConductorTimeSystem,\n    ConductorInputsFixed,\n    ConductorInputsRealtime\n  },\n  inject: ['openmct', 'isFixedTimeMode'],\n  props: {\n    positionX: {\n      type: Number,\n      required: true\n    },\n    positionY: {\n      type: Number,\n      required: true\n    },\n    isIndependent: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    bottom: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['popup-loaded', 'dismiss'],\n  setup(props) {\n    const conductorPopupElement = ref(null);\n    return {\n      conductorPopupElement\n    };\n  },\n  computed: {\n    position() {\n      const position = {\n        left: `${this.positionX}px`\n      };\n\n      if (this.isIndependent) {\n        position.top = `${this.positionY}px`;\n      }\n\n      return position;\n    },\n    popupClasses() {\n      const value = this.bottom ? 'c-tc-input-popup--bottom ' : '';\n      const mode = this.isFixedTimeMode ? 'fixed-mode' : 'realtime-mode';\n      const independentClass = this.isIndependent ? 'itc-popout ' : '';\n\n      return `${independentClass}${value}c-tc-input-popup--${mode}`;\n    }\n  },\n  mounted() {\n    this.$emit('popup-loaded');\n  },\n  methods: {\n    dismiss() {\n      this.$emit('dismiss');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/ConductorTimeSystem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"selectedTimeSystem.name && readOnly === false\"\n    ref=\"timeSystemButton\"\n    class=\"c-ctrl-wrapper c-ctrl-wrapper--menus-up\"\n  >\n    <button\n      class=\"c-button--menu c-time-system-button c-icon-button\"\n      aria-label=\"Time Conductor Time System\"\n      @click.prevent.stop=\"showTimeSystemMenu\"\n    >\n      <span class=\"c-button__label\">{{ selectedTimeSystem.name }}</span>\n    </button>\n  </div>\n  <div\n    v-else\n    class=\"c-compact-tc__setting-value__elem\"\n    :aria-label=\"`Time system: ${selectedTimeSystem.name}`\"\n    :title=\"`Time system: ${selectedTimeSystem.name}`\"\n  >\n    {{ selectedTimeSystem.name }}\n  </div>\n</template>\n\n<script>\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\nexport default {\n  inject: ['openmct', 'configuration'],\n  props: {\n    readOnly: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  data() {\n    let activeClock = this.openmct.time.getClock();\n\n    return {\n      selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())),\n      timeSystems: this.getValidTimesystemsForClock(activeClock)\n    };\n  },\n  mounted() {\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);\n  },\n  unmounted() {\n    this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);\n    this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);\n  },\n  methods: {\n    showTimeSystemMenu() {\n      const elementBoundingClientRect = this.$refs.timeSystemButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y;\n\n      const menuOptions = {\n        placement: this.openmct.menus.menuPlacement.TOP_RIGHT\n      };\n\n      this.openmct.menus.showMenu(x, y, this.timeSystems, menuOptions);\n    },\n    getValidTimesystemsForClock(clock) {\n      return this.configuration.menuOptions\n        .filter((menuOption) => menuOption.clock === (clock && clock.key))\n        .map((menuOption) => {\n          const timeSystem = JSON.parse(\n            JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem))\n          );\n          timeSystem.onItemClicked = () => this.setTimeSystemFromView(timeSystem);\n\n          return timeSystem;\n        });\n    },\n    setTimeSystemFromView(timeSystem) {\n      if (timeSystem.key !== this.selectedTimeSystem.key) {\n        let activeClock = this.openmct.time.getClock();\n        let configuration = this.getMatchingConfig({\n          clock: activeClock && activeClock.key,\n          timeSystem: timeSystem.key\n        });\n        if (activeClock === undefined) {\n          let bounds;\n\n          if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {\n            bounds = this.openmct.time.getBounds();\n          } else {\n            bounds = configuration.bounds;\n          }\n\n          this.openmct.time.setTimeSystem(timeSystem.key, bounds);\n        } else {\n          this.openmct.time.setTimeSystem(timeSystem.key);\n          this.openmct.time.setClockOffsets(configuration.clockOffsets);\n        }\n      }\n    },\n\n    getMatchingConfig(options) {\n      const matchers = {\n        clock(config) {\n          return options.clock === config.clock;\n        },\n        timeSystem(config) {\n          return options.timeSystem === config.timeSystem;\n        }\n      };\n\n      function configMatches(config) {\n        return Object.keys(options).reduce((match, option) => {\n          return match && matchers[option](config);\n        }, true);\n      }\n\n      return this.configuration.menuOptions.filter(configMatches)[0];\n    },\n\n    setViewFromTimeSystem(timeSystem) {\n      this.selectedTimeSystem = timeSystem;\n    },\n\n    setViewFromClock(clock) {\n      let activeClock = this.openmct.time.getClock();\n      this.timeSystems = this.getValidTimesystemsForClock(activeClock);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/DatePicker.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"calendarHolder\"\n    class=\"c-ctrl-wrapper c-datetime-picker__wrapper\"\n    :class=\"{\n      'c-ctrl-wrapper--menus-up': bottom !== true,\n      'c-ctrl-wrapper--menus-down': bottom === true\n    }\"\n  >\n    <a class=\"c-icon-button icon-calendar\" @click=\"toggle\"></a>\n    <div v-if=\"open\" role=\"dialog\" class=\"c-menu c-menu--mobile-modal c-datetime-picker\">\n      <div class=\"c-datetime-picker__close-button\">\n        <button class=\"c-click-icon icon-x-in-circle\" @click=\"toggle\"></button>\n      </div>\n      <div class=\"c-datetime-picker__pager c-pager l-month-year-pager\">\n        <div\n          class=\"c-pager__prev c-icon-button icon-arrow-left\"\n          @click.stop=\"changeMonth(-1)\"\n        ></div>\n        <div class=\"c-pager__month-year\">{{ model.month }} {{ model.year }}</div>\n        <div\n          class=\"c-pager__next c-icon-button icon-arrow-right\"\n          @click.stop=\"changeMonth(1)\"\n        ></div>\n      </div>\n      <div class=\"c-datetime-picker__calendar c-calendar\">\n        <div class=\"c-calendar__row--header l-cal-row\">\n          <div\n            v-for=\"day in ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']\"\n            :key=\"day\"\n            class=\"c-calendar-cell\"\n          >\n            {{ day }}\n          </div>\n        </div>\n        <div v-for=\"(row, tableIndex) in table\" :key=\"tableIndex\" class=\"c-calendar__row--body\">\n          <div\n            v-for=\"(cell, rowIndex) in row\"\n            :key=\"rowIndex\"\n            :class=\"{ 'is-in-month': isInCurrentMonth(cell), selected: isSelected(cell) }\"\n            class=\"c-calendar-cell\"\n            @click=\"select(cell)\"\n          >\n            <div class=\"c-calendar__day--prime\">\n              {{ cell.day }}\n            </div>\n            <div class=\"c-calendar__day--sub\">\n              {{ cell.dayOfYear }}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport moment from 'moment';\n\nimport toggleMixin from '../../ui/mixins/toggle-mixin.js';\n\nconst TIME_NAMES = {\n  hours: 'Hour',\n  minutes: 'Minute',\n  seconds: 'Second'\n};\nconst MONTHS = moment.months();\nconst TIME_OPTIONS = (function makeRanges() {\n  let arr = [];\n  while (arr.length < 60) {\n    arr.push(arr.length);\n  }\n\n  return {\n    hours: arr.slice(0, 24),\n    minutes: arr,\n    seconds: arr\n  };\n})();\n\nexport default {\n  mixins: [toggleMixin],\n  inject: ['openmct'],\n  props: {\n    defaultDateTime: {\n      type: String,\n      default: undefined\n    },\n    bottom: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['date-selected'],\n  data: function () {\n    return {\n      picker: {\n        year: undefined,\n        month: undefined,\n        interacted: false\n      },\n      model: {\n        year: undefined,\n        month: undefined\n      },\n      table: undefined,\n      date: undefined,\n      time: undefined\n    };\n  },\n  watch: {\n    defaultDateTime() {\n      this.updateFromModel(this.defaultDateTime);\n    }\n  },\n  mounted: function () {\n    this.updateFromModel(this.defaultDateTime);\n    this.updateViewForMonth();\n  },\n  methods: {\n    generateTable() {\n      let m = moment\n        .utc({\n          year: this.picker.year,\n          month: this.picker.month\n        })\n        .day(0);\n      let table = [];\n      let row;\n      let col;\n\n      for (row = 0; row < 6; row += 1) {\n        table.push([]);\n        for (col = 0; col < 7; col += 1) {\n          table[row].push({\n            year: m.year(),\n            month: m.month(),\n            day: m.date(),\n            dayOfYear: m.dayOfYear()\n          });\n          m.add(1, 'days'); // Next day!\n        }\n      }\n\n      return table;\n    },\n\n    updateViewForMonth() {\n      this.model.month = MONTHS[this.picker.month];\n      this.model.year = this.picker.year;\n      this.table = this.generateTable();\n    },\n\n    updateFromModel(defaultDateTime) {\n      let m = moment.utc(defaultDateTime);\n\n      this.date = {\n        year: m.year(),\n        month: m.month(),\n        day: m.date()\n      };\n      this.time = {\n        hours: m.hour(),\n        minutes: m.minute(),\n        seconds: m.second()\n      };\n\n      // Zoom to that date in the picker, but\n      // only if the user hasn't interacted with it yet.\n      if (!this.picker.interacted) {\n        this.picker.year = m.year();\n        this.picker.month = m.month();\n        this.updateViewForMonth();\n      }\n    },\n\n    updateFromView() {\n      let m = moment.utc({\n        year: this.date.year,\n        month: this.date.month,\n        day: this.date.day,\n        hour: this.time.hours,\n        minute: this.time.minutes,\n        second: this.time.seconds\n      });\n      this.$emit('date-selected', m.valueOf());\n    },\n\n    isInCurrentMonth(cell) {\n      return cell.month === this.picker.month;\n    },\n\n    isSelected(cell) {\n      let date = this.date || {};\n\n      return cell.day === date.day && cell.month === date.month && cell.year === date.year;\n    },\n\n    select(cell) {\n      this.date = this.date || {};\n      this.date.month = cell.month;\n      this.date.year = cell.year;\n      this.date.day = cell.day;\n      this.updateFromView();\n    },\n\n    dateEquals(d1, d2) {\n      return d1.year === d2.year && d1.month === d2.month && d1.day === d2.day;\n    },\n\n    changeMonth(delta) {\n      this.picker.month += delta;\n      if (this.picker.month > 11) {\n        this.picker.month = 0;\n        this.picker.year += 1;\n      }\n\n      if (this.picker.month < 0) {\n        this.picker.month = 11;\n        this.picker.year -= 1;\n      }\n\n      this.picker.interacted = true;\n      this.updateViewForMonth();\n    },\n\n    nameFor(key) {\n      return TIME_NAMES[key];\n    },\n\n    optionsFor(key) {\n      return TIME_OPTIONS[key];\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/DateTimePopupFixed.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <form ref=\"fixedDeltaInput\">\n    <div class=\"c-tc-input-popup__input-grid-utc\">\n      <div class=\"pr-time-label pr-time-label-start-date\"><em>Start</em> Date</div>\n      <div class=\"pr-time-label pr-time-label-start-time\">Time</div>\n      <div class=\"pr-time-label pr-time-label-end-date\"><em>End</em> Date</div>\n      <div class=\"pr-time-label pr-time-label-end-time\">Time</div>\n\n      <div\n        class=\"pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date\"\n      >\n        <DatePicker\n          class=\"c-ctrl-wrapper--menus-right\"\n          :default-date-time=\"formattedBounds.startDate\"\n          @date-selected=\"startDateSelected\"\n        />\n        <input\n          ref=\"startDate\"\n          v-model=\"formattedBounds.startDate\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"Start date\"\n          @input=\"validateInput('startDate')\"\n          @change=\"reportValidity('startDate')\"\n          @copy.prevent.stop=\"copyToClipboard('start')\"\n          @paste.prevent.stop=\"pasteFromClipboard('start')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input--time pr-time-input-start-time\">\n        <input\n          ref=\"startTime\"\n          v-model=\"formattedBounds.startTime\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"Start time\"\n          @input=\"validateInput('startTime')\"\n          @change=\"reportValidity('startTime')\"\n          @copy.prevent.stop=\"copyToClipboard('start')\"\n          @paste.prevent.stop=\"pasteFromClipboard('start')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input__start-end-sep icon-arrows-right-left\"></div>\n\n      <div\n        class=\"pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date\"\n      >\n        <DatePicker\n          class=\"c-ctrl-wrapper--menus-left\"\n          :default-date-time=\"formattedBounds.endDate\"\n          @date-selected=\"endDateSelected\"\n        />\n        <input\n          ref=\"endDate\"\n          v-model=\"formattedBounds.endDate\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"End date\"\n          @input=\"validateInput('endDate')\"\n          @change=\"reportValidity('endDate')\"\n          @copy.prevent.stop=\"copyToClipboard('end')\"\n          @paste.prevent.stop=\"pasteFromClipboard('end')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input--time pr-time-input-end-time\">\n        <input\n          ref=\"endTime\"\n          v-model=\"formattedBounds.endTime\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"End time\"\n          @input=\"validateInput('endTime')\"\n          @change=\"reportValidity('endTime')\"\n          @copy.prevent.stop=\"copyToClipboard('end')\"\n          @paste.prevent.stop=\"pasteFromClipboard('end')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input--buttons\">\n        <button\n          class=\"c-button c-button--major icon-check\"\n          :disabled=\"hasInputValidityError\"\n          aria-label=\"Submit time bounds\"\n          @click.prevent=\"submitForm(true)\"\n        ></button>\n        <button\n          class=\"c-button icon-x\"\n          aria-label=\"Discard changes and close time popup\"\n          @click.prevent=\"hide\"\n        ></button>\n      </div>\n    </div>\n  </form>\n</template>\n\n<script>\nimport DatePicker from './DatePicker.vue';\n\nexport default {\n  components: {\n    DatePicker\n  },\n  inject: [\n    'openmct',\n    'configuration',\n    'isTimeSystemUTCBased',\n    'timeContext',\n    'timeSystemKey',\n    'timeSystemFormatter',\n    'timeSystemDurationFormatter',\n    'bounds'\n  ],\n  emits: ['update', 'dismiss'],\n  data() {\n    return {\n      formattedBounds: {},\n      inputValidityMap: {\n        startDate: { valid: true },\n        startTime: { valid: true },\n        endDate: { valid: true },\n        endTime: { valid: true }\n      },\n      logicalValidityMap: {\n        limit: { valid: true },\n        bounds: { valid: true }\n      }\n    };\n  },\n  computed: {\n    hasInputValidityError() {\n      return Object.values(this.inputValidityMap).some((inputValidity) => !inputValidity.valid);\n    },\n    hasLogicalValidationErrors() {\n      return Object.values(this.logicalValidityMap).some(\n        (logicalValidity) => !logicalValidity.valid\n      );\n    },\n    isValid() {\n      return !this.hasInputValidityError && !this.hasLogicalValidationErrors;\n    }\n  },\n  watch: {\n    bounds: {\n      handler() {\n        this.setViewFromBounds();\n      }\n    }\n  },\n  mounted() {\n    this.setViewFromBounds();\n  },\n  beforeUnmount() {\n    this.clearInputValidation();\n    this.clearLogicalValidation();\n  },\n  methods: {\n    async copyToClipboard(startOrEnd) {\n      if (startOrEnd !== 'start' && startOrEnd !== 'end') {\n        console.warn('Invalid startOrEnd value');\n        return;\n      }\n\n      const bound =\n        this.timeSystemFormatter.parse(this.formattedBounds[`${startOrEnd}Date`]) +\n        this.timeSystemDurationFormatter.parse(this.formattedBounds[`${startOrEnd}Time`]);\n      const timeStampString = this.timeSystemFormatter.format(bound);\n\n      try {\n        await navigator.clipboard.writeText(timeStampString);\n      } catch (err) {\n        this.openmct.notifications.error('Failed to copy timestamp to clipboard');\n        console.error(err);\n      }\n    },\n    async pasteFromClipboard(startOrEnd) {\n      if (startOrEnd !== 'start' && startOrEnd !== 'end') {\n        console.warn('Invalid startOrEnd value');\n        return;\n      }\n\n      let timeStampString;\n      try {\n        timeStampString = await navigator.clipboard.readText();\n      } catch (err) {\n        this.openmct.notifications.error('Failed to get timestamp from clipboard');\n        console.error(err);\n        return;\n      }\n\n      if (!this.timeSystemFormatter.validate(timeStampString)) {\n        this.openmct.notifications.warn(`\"${timeStampString}\" is not a valid timestamp format`);\n        return;\n      }\n\n      const bound = this.timeSystemFormatter.parse(timeStampString);\n      this.formattedBounds[`${startOrEnd}Date`] = this.timeSystemFormatter.formatDate(bound);\n      this.formattedBounds[`${startOrEnd}Time`] = this.timeSystemDurationFormatter.format(\n        Math.abs(bound),\n        'HH:mm:ss.SSS'\n      );\n\n      this.validateInput([`${startOrEnd}Date`, `${startOrEnd}Time`]);\n      this.reportValidity([`${startOrEnd}Date`, `${startOrEnd}Time`]);\n    },\n    setViewFromBounds() {\n      this.formattedBounds = {\n        startDate: this.timeSystemFormatter.formatDate(this.bounds.start),\n        startTime: this.timeSystemDurationFormatter.format(Math.abs(this.bounds.start)),\n        endDate: this.timeSystemFormatter.formatDate(this.bounds.end),\n        endTime: this.timeSystemDurationFormatter.format(Math.abs(this.bounds.end))\n      };\n    },\n    setBoundsFromView(shouldDismiss) {\n      if (this.$refs.fixedDeltaInput.checkValidity()) {\n        const start =\n          this.timeSystemFormatter.parse(this.formattedBounds.startDate) +\n          this.timeSystemDurationFormatter.parse(this.formattedBounds.startTime);\n        const end =\n          this.timeSystemFormatter.parse(this.formattedBounds.endDate) +\n          this.timeSystemDurationFormatter.parse(this.formattedBounds.endTime);\n\n        const bounds = { start, end };\n        this.timeContext.setBounds(bounds);\n      }\n\n      if (shouldDismiss) {\n        this.$emit('dismiss');\n\n        return false;\n      }\n    },\n    clearInputValidation() {\n      Object.keys(this.inputValidityMap).forEach((refName) => {\n        const input = this.getInput(refName);\n\n        input.setCustomValidity('');\n        input.title = '';\n      });\n    },\n    clearLogicalValidation() {\n      Object.keys(this.logicalValidityMap).forEach((refName) => {\n        const input = this.getInput(refName);\n\n        input.setCustomValidity('');\n        input.title = '';\n\n        if (this.logicalValidityMap[refName] !== undefined) {\n          this.logicalValidityMap[refName] = { valid: true };\n        }\n      });\n    },\n    submitForm(shouldDismiss) {\n      this.clearLogicalValidation();\n\n      this.validateBounds();\n      this.reportValidity('bounds');\n\n      if (!this.isValid) {\n        return;\n      }\n\n      this.validateLimit();\n      this.reportValidity('limit');\n\n      if (!this.isValid) {\n        return;\n      }\n\n      this.setBoundsFromView(shouldDismiss);\n    },\n    validateInput(refNames) {\n      if (!Array.isArray(refNames)) {\n        refNames = [refNames];\n      }\n\n      this.clearInputValidation();\n\n      refNames.forEach((refName) => {\n        const inputType = refName.includes('Date') ? 'Date' : 'Time';\n        const formatter =\n          inputType === 'Date' ? this.timeSystemFormatter : this.timeSystemDurationFormatter;\n        const validationResult = formatter.validate(this.formattedBounds[refName])\n          ? { valid: true }\n          : { valid: false, message: `Invalid ${inputType}` };\n\n        this.inputValidityMap[refName] = validationResult;\n      });\n    },\n    validateBounds() {\n      const start =\n        this.timeSystemFormatter.parse(this.formattedBounds.startDate) +\n        this.timeSystemDurationFormatter.parse(this.formattedBounds.startTime);\n      const end =\n        this.timeSystemFormatter.parse(this.formattedBounds.endDate) +\n        this.timeSystemDurationFormatter.parse(this.formattedBounds.endTime);\n\n      const bounds = { start, end };\n\n      this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);\n    },\n    validateLimit() {\n      const start =\n        this.timeSystemFormatter.parse(this.formattedBounds.startDate) +\n        this.timeSystemDurationFormatter.parse(this.formattedBounds.startTime);\n      const end =\n        this.timeSystemFormatter.parse(this.formattedBounds.endDate) +\n        this.timeSystemDurationFormatter.parse(this.formattedBounds.endTime);\n\n      const bounds = { start, end };\n\n      const limit = this.configuration?.menuOptions\n        ?.filter((option) => option.timeSystem === this.timeSystemKey)\n        ?.find((option) => option.limit)?.limit;\n\n      if (limit && bounds.end - bounds.start > limit) {\n        this.logicalValidityMap.limit = {\n          valid: false,\n          message: 'Start and end difference exceeds allowable limit'\n        };\n      } else {\n        this.logicalValidityMap.limit = { valid: true };\n      }\n    },\n    reportValidity(refNames) {\n      if (!Array.isArray(refNames)) {\n        refNames = [refNames];\n      }\n\n      refNames.forEach((refName) => {\n        const input = this.getInput(refName);\n        const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];\n\n        if (validationResult.valid !== true) {\n          input.setCustomValidity(validationResult.message);\n          input.title = validationResult.message;\n        } else {\n          input.setCustomValidity('');\n          input.title = '';\n        }\n      });\n\n      this.$refs.fixedDeltaInput.reportValidity();\n    },\n    getInput(refName) {\n      if (Object.keys(this.inputValidityMap).includes(refName)) {\n        return this.$refs[refName];\n      }\n\n      return this.$refs.startDate;\n    },\n    startDateSelected(date) {\n      this.formattedBounds.startDate = this.timeSystemFormatter.formatDate(date);\n      this.validateInput('startDate');\n      this.reportValidity('startDate');\n    },\n    endDateSelected(date) {\n      this.formattedBounds.endDate = this.timeSystemFormatter.formatDate(date);\n      this.validateInput('endDate');\n      this.reportValidity('endDate');\n    },\n    hide($event) {\n      if ($event.target.className.indexOf('c-button icon-x') > -1) {\n        this.$emit('dismiss');\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/TimePopupFixed.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <form ref=\"fixedDeltaInput\">\n    <div class=\"c-tc-input-popup__input-grid\">\n      <div class=\"pr-time-label pr-time-label-start-time\">Start</div>\n      <div class=\"pr-time-label pr-time-label-end-time\">End</div>\n\n      <div class=\"pr-time-input pr-time-input-start\">\n        <input\n          ref=\"start\"\n          v-model=\"formattedBounds.start\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"Start time\"\n          @input=\"validateInput('start')\"\n          @change=\"reportValidity('start')\"\n          @copy.prevent.stop=\"copyToClipboard('start')\"\n          @paste.prevent.stop=\"pasteFromClipboard('start')\"\n        />\n        <DatePicker\n          v-if=\"isTimeSystemUTCBased\"\n          class=\"c-ctrl-wrapper--menus-left\"\n          :default-date-time=\"formattedBounds.start\"\n          @date-selected=\"dateSelected($event, 'start')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input__start-end-sep icon-arrows-right-left\"></div>\n\n      <div class=\"pr-time-input pr-time-input-end\">\n        <input\n          ref=\"end\"\n          v-model=\"formattedBounds.end\"\n          class=\"c-input--datetime\"\n          type=\"text\"\n          autocorrect=\"off\"\n          spellcheck=\"false\"\n          aria-label=\"End time\"\n          @input=\"validateInput('end')\"\n          @change=\"reportValidity('end')\"\n          @copy.prevent.stop=\"copyToClipboard('end')\"\n          @paste.prevent.stop=\"pasteFromClipboard('end')\"\n        />\n        <DatePicker\n          v-if=\"isTimeSystemUTCBased\"\n          class=\"c-ctrl-wrapper--menus-left\"\n          :default-date-time=\"formattedBounds.end\"\n          @date-selected=\"dateSelected($event, 'end')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input--buttons\">\n        <button\n          class=\"c-button c-button--major icon-check\"\n          :disabled=\"hasInputValidityError\"\n          aria-label=\"Submit time bounds\"\n          @click.prevent=\"submitForm(true)\"\n        ></button>\n        <button\n          class=\"c-button icon-x\"\n          aria-label=\"Discard changes and close time popup\"\n          @click.prevent=\"hide\"\n        ></button>\n      </div>\n    </div>\n  </form>\n</template>\n\n<script>\nimport DatePicker from './DatePicker.vue';\n\nexport default {\n  components: {\n    DatePicker\n  },\n  inject: [\n    'openmct',\n    'isTimeSystemUTCBased',\n    'timeContext',\n    'timeSystemFormatter',\n    'timeSystemDurationFormatter',\n    'bounds'\n  ],\n  emits: ['update', 'dismiss'],\n  data() {\n    return {\n      formattedBounds: {},\n      inputValidityMap: {\n        start: { valid: true },\n        end: { valid: true }\n      },\n      logicalValidityMap: {\n        limit: { valid: true },\n        bounds: { valid: true }\n      }\n    };\n  },\n  computed: {\n    hasInputValidityError() {\n      return Object.values(this.inputValidityMap).some((validity) => !validity.valid);\n    },\n    hasLogicalValidationErrors() {\n      return Object.values(this.logicalValidityMap).some((validity) => !validity.valid);\n    },\n    isValid() {\n      return !this.hasInputValidityError && !this.hasLogicalValidationErrors;\n    }\n  },\n  watch: {\n    bounds: {\n      handler() {\n        this.setViewFromBounds();\n      }\n    }\n  },\n  mounted() {\n    this.setViewFromBounds();\n  },\n  beforeUnmount() {\n    this.clearAllValidation();\n  },\n  methods: {\n    async copyToClipboard(startOrEnd) {\n      if (startOrEnd !== 'start' && startOrEnd !== 'end') {\n        console.warn('Invalid startOrEnd value');\n        return;\n      }\n\n      try {\n        await navigator.clipboard.writeText(this.formattedBounds[startOrEnd]);\n      } catch (err) {\n        this.openmct.notifications.error('Failed to copy timestamp to clipboard');\n        console.error(err);\n      }\n    },\n    async pasteFromClipboard(startOrEnd) {\n      if (startOrEnd !== 'start' && startOrEnd !== 'end') {\n        console.warn('Invalid startOrEnd value');\n        return;\n      }\n\n      try {\n        this.formattedBounds[startOrEnd] = await navigator.clipboard.readText();\n      } catch (err) {\n        this.openmct.notifications.error('Failed to get timestamp from clipboard');\n        console.error(err);\n        return;\n      }\n\n      this.validateInput(startOrEnd);\n      this.reportValidity(startOrEnd);\n    },\n    setViewFromBounds() {\n      const start = this.timeSystemFormatter.format(this.bounds.start);\n      const end = this.timeSystemFormatter.format(this.bounds.end);\n\n      this.formattedBounds = {\n        start,\n        end\n      };\n    },\n    setBoundsFromView(shouldDismiss) {\n      if (this.$refs.fixedDeltaInput.checkValidity()) {\n        const start = this.timeSystemFormatter.parse(this.formattedBounds.start);\n        const end = this.timeSystemFormatter.parse(this.formattedBounds.end);\n\n        this.timeContext.setBounds({\n          start,\n          end\n        });\n      }\n\n      if (shouldDismiss) {\n        this.$emit('dismiss');\n        return false;\n      }\n    },\n    clearAllValidation() {\n      Object.keys(this.inputValidityMap).forEach(this.clearValidation);\n    },\n    clearValidation(refName) {\n      const input = this.getInput(refName);\n\n      input.setCustomValidity('');\n      input.title = '';\n    },\n    submitForm(shouldDismiss) {\n      this.validateLimit();\n      this.reportValidity('limit');\n      this.validateBounds();\n      this.reportValidity('bounds');\n\n      if (this.isValid) {\n        this.setBoundsFromView(shouldDismiss);\n      }\n    },\n    validateInput(refName) {\n      this.clearAllValidation();\n      const validationResult = this.timeSystemFormatter.validate(this.formattedBounds[refName])\n        ? { valid: true }\n        : { valid: false, message: 'Invalid Date' };\n\n      this.inputValidityMap[refName] = validationResult;\n    },\n    validateBounds() {\n      const bounds = {\n        start: this.timeSystemFormatter.parse(this.formattedBounds.start),\n        end: this.timeSystemFormatter.parse(this.formattedBounds.end)\n      };\n\n      this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);\n    },\n    validateLimit(bounds) {\n      const limit = this.configuration?.menuOptions\n        ?.filter((option) => option.timeSystem === this.timeSystemKey)\n        ?.find((option) => option.limit)?.limit;\n\n      if (this.isUTCBased && limit && bounds.end - bounds.start > limit) {\n        this.logicalValidityMap.limit = {\n          valid: false,\n          message: 'Start and end difference exceeds allowable limit'\n        };\n      } else {\n        this.logicalValidityMap.limit = { valid: true };\n      }\n    },\n    reportValidity(refName) {\n      const input = this.getInput(refName);\n      const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];\n\n      if (validationResult.valid !== true) {\n        input.setCustomValidity(validationResult.message);\n        input.title = validationResult.message;\n      } else {\n        input.setCustomValidity('');\n        input.title = '';\n      }\n\n      this.$refs.fixedDeltaInput.reportValidity();\n    },\n    getInput(refName) {\n      if (Object.keys(this.inputValidityMap).includes(refName)) {\n        return this.$refs[refName];\n      }\n\n      return this.$refs.start;\n    },\n    dateSelected(date, refName) {\n      this.formattedBounds[refName] = this.timeSystemFormatter.format(date);\n      this.validateInput(refName);\n      this.reportValidity(refName);\n    },\n    hide($event) {\n      if ($event.target.className.indexOf('c-button icon-x') > -1) {\n        this.$emit('dismiss');\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/TimePopupRealtime.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <form ref=\"deltaInput\">\n    <div class=\"c-tc-input-popup__input-grid-utc\">\n      <div class=\"pr-time-label icon-minus pr-time-label-minus-hrs\">Hrs</div>\n      <div class=\"pr-time-label pr-time-label-minus-mins\">Mins</div>\n      <div class=\"pr-time-label pr-time-label-minus-secs\">Secs</div>\n      <div class=\"pr-time-label icon-plus pr-time-label-plus-hrs\">Hrs</div>\n      <div class=\"pr-time-label pr-time-label-plus-mins\">Mins</div>\n      <div class=\"pr-time-label pr-time-label-plus-secs\">Secs</div>\n\n      <div class=\"pr-time-input pr-time-input-minus-hrs\">\n        <input\n          ref=\"startInputHrs\"\n          v-model=\"startInputHrs\"\n          class=\"pr-time-input__hrs\"\n          step=\"1\"\n          type=\"number\"\n          min=\"0\"\n          max=\"23\"\n          title=\"Enter 0 - 23\"\n          aria-label=\"Start offset hours\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('startInputHrs')\"\n          @wheel=\"increment($event, 'startInputHrs')\"\n        />\n        <b>:</b>\n      </div>\n      <div class=\"pr-time-input pr-time-input-minus-mins\">\n        <input\n          ref=\"startInputMins\"\n          v-model=\"startInputMins\"\n          type=\"number\"\n          class=\"pr-time-input__mins\"\n          min=\"0\"\n          max=\"59\"\n          title=\"Enter 0 - 59\"\n          step=\"1\"\n          aria-label=\"Start offset minutes\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('startInputMins')\"\n          @wheel=\"increment($event, 'startInputMins')\"\n        />\n        <b>:</b>\n      </div>\n      <div class=\"pr-time-input pr-time-input-minus-secs\">\n        <input\n          ref=\"startInputSecs\"\n          v-model=\"startInputSecs\"\n          type=\"number\"\n          class=\"pr-time-input__secs\"\n          min=\"0\"\n          max=\"59\"\n          title=\"Enter 0 - 59\"\n          step=\"1\"\n          aria-label=\"Start offset seconds\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('startInputSecs')\"\n          @wheel=\"increment($event, 'startInputSecs')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input__start-end-sep icon-arrows-right-left\"></div>\n\n      <div class=\"pr-time-input pr-time-input-plus-hrs\">\n        <input\n          ref=\"endInputHrs\"\n          v-model=\"endInputHrs\"\n          class=\"pr-time-input__hrs\"\n          step=\"1\"\n          type=\"number\"\n          min=\"0\"\n          max=\"23\"\n          title=\"Enter 0 - 23\"\n          aria-label=\"End offset hours\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('endInputHrs')\"\n          @wheel=\"increment($event, 'endInputHrs')\"\n        />\n        <b>:</b>\n      </div>\n      <div class=\"pr-time-input pr-time-input-plus-mins\">\n        <input\n          ref=\"endInputMins\"\n          v-model=\"endInputMins\"\n          type=\"number\"\n          class=\"pr-time-input__mins\"\n          min=\"0\"\n          max=\"59\"\n          title=\"Enter 0 - 59\"\n          step=\"1\"\n          aria-label=\"End offset minutes\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('endInputMins')\"\n          @wheel=\"increment($event, 'endInputMins')\"\n        />\n        <b>:</b>\n      </div>\n      <div class=\"pr-time-input pr-time-input-plus-secs\">\n        <input\n          ref=\"endInputSecs\"\n          v-model=\"endInputSecs\"\n          type=\"number\"\n          class=\"pr-time-input__secs\"\n          min=\"0\"\n          max=\"59\"\n          title=\"Enter 0 - 59\"\n          step=\"1\"\n          aria-label=\"End offset seconds\"\n          @change=\"validate()\"\n          @keyup=\"validate()\"\n          @focusin=\"selectAll($event)\"\n          @focusout=\"format('endInputSecs')\"\n          @wheel=\"increment($event, 'endInputSecs')\"\n        />\n      </div>\n\n      <div class=\"pr-time-input pr-time-input--buttons\">\n        <button\n          class=\"c-button c-button--major icon-check\"\n          :disabled=\"isDisabled\"\n          aria-label=\"Submit time offsets\"\n          @click.prevent=\"submit\"\n        ></button>\n        <button\n          class=\"c-button icon-x\"\n          aria-label=\"Discard changes and close time popup\"\n          @click.prevent=\"hide\"\n        ></button>\n      </div>\n    </div>\n  </form>\n</template>\n\n<script>\nimport { nextTick } from 'vue';\n\nexport default {\n  inject: ['timeContext', 'timeSystemDurationFormatter'],\n  props: {\n    offsets: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['dismiss'],\n  data() {\n    return {\n      startInputHrs: '00',\n      startInputMins: '00',\n      startInputSecs: '00',\n      endInputHrs: '00',\n      endInputMins: '00',\n      endInputSecs: '00',\n      isDisabled: false\n    };\n  },\n  watch: {\n    offsets: {\n      handler() {\n        this.setOffsets();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.setOffsets();\n    document.addEventListener('click', this.hide);\n  },\n  beforeUnmount() {\n    document.removeEventListener('click', this.hide);\n  },\n  methods: {\n    format(ref) {\n      const curVal = this[ref];\n      this[ref] = curVal.toString().padStart(2, '0');\n    },\n    validate() {\n      let disabled = false;\n      let refs = [\n        'startInputHrs',\n        'startInputMins',\n        'startInputSecs',\n        'endInputHrs',\n        'endInputMins',\n        'endInputSecs'\n      ];\n\n      for (let ref of refs) {\n        let min = Number(this.$refs[ref].min);\n        let max = Number(this.$refs[ref].max);\n        let value = Number(this.$refs[ref].value);\n\n        if (value > max || value < min) {\n          disabled = true;\n          break;\n        }\n      }\n\n      this.isDisabled = disabled;\n    },\n    submit() {\n      const formattedStartOffset = [\n        this.startInputHrs,\n        this.startInputMins,\n        this.startInputSecs\n      ].join(':');\n      const formattedEndOffset = [this.endInputHrs, this.endInputMins, this.endInputSecs].join(':');\n\n      let startOffset = 0 - this.timeSystemDurationFormatter.parse(formattedStartOffset);\n      let endOffset = this.timeSystemDurationFormatter.parse(formattedEndOffset);\n\n      const offsets = {\n        start: startOffset,\n        end: endOffset\n      };\n\n      this.timeContext.setClockOffsets(offsets);\n\n      this.$emit('dismiss');\n    },\n    hide($event) {\n      if ($event.target.className.indexOf('c-button icon-x') > -1) {\n        this.$emit('dismiss');\n      }\n    },\n    increment($ev, ref) {\n      $ev.preventDefault();\n      const step = ref === 'startInputHrs' || ref === 'endInputHrs' ? 1 : 5;\n      const maxVal = ref === 'startInputHrs' || ref === 'endInputHrs' ? 23 : 59;\n      let cv = Math.round(parseInt(this[ref], 10) / step) * step;\n      cv = Math.min(maxVal, Math.max(0, $ev.deltaY < 0 ? cv + step : cv - step));\n      this[ref] = cv.toString().padStart(2, '0');\n      this.validate();\n    },\n    async setOffsets() {\n      [this.startInputHrs, this.startInputMins, this.startInputSecs] =\n        this.offsets.start.split(':');\n      [this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':');\n\n      await nextTick();\n      this.numberSelect('startInputHrs');\n    },\n    numberSelect(input) {\n      if (this.$refs[input] === undefined || this.$refs[input] === null) {\n        return;\n      }\n      this.$refs[input].focus();\n      this.$refs[input].select();\n    },\n    selectAll($ev) {\n      $ev.target.select();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/clock-mixin.js",
    "content": "export default {\n  methods: {\n    loadClocks(menuOptions) {\n      let clocks;\n\n      if (menuOptions) {\n        clocks = menuOptions\n          .map((menuOption) => menuOption.clock)\n          .filter(isDefinedAndUnique)\n          .map(this.getClock);\n      } else {\n        clocks = this.openmct.time.getAllClocks();\n      }\n\n      this.clocks = clocks.map(this.getClockMetadata);\n\n      function isDefinedAndUnique(key, index, array) {\n        return key !== undefined && array.indexOf(key) === index;\n      }\n    },\n    getActiveClock() {\n      const activeClock = this.openmct.time.getClock();\n\n      //Create copy of active clock so the time API does not get reactified.\n      return Object.create(activeClock);\n    },\n    getClock(key) {\n      return this.openmct.time.getAllClocks().find((clock) => clock.key === key);\n    },\n    getClockMetadata(clock) {\n      const key = clock.key;\n      const clockOptions = {\n        key,\n        name: clock.name,\n        description: 'Uses the system clock as the current time basis. ' + clock.description,\n        cssClass: clock.cssClass || 'icon-clock',\n        onItemClicked: () => this.setClock(key)\n      };\n\n      return clockOptions;\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/timeConductor/conductor-axis.scss",
    "content": "@use 'sass:math';\n\n.c-conductor-axis {\n  $h: 18px;\n  $tickYPos: math.div($h, 2) + 12px;\n\n  @include userSelectNone();\n  @include bgTicks($c: rgba($colorBodyFg, 0.4));\n  background-position: 0 50%;\n  background-size: 5px 2px;\n  border-radius: $controlCr;\n  height: $h;\n\n  svg {\n    text-rendering: geometricPrecision;\n    width: 100%;\n    height: 100%;\n    > g.axis {\n      // Overall Tick holder\n      transform: translateY($tickYPos);\n      path {\n        // Domain line\n        display: none;\n      }\n\n      g {\n        // Each tick. These move on drag.\n        line {\n          // Line beneath ticks\n          display: none;\n        }\n      }\n    }\n\n    text {\n      // Tick labels\n      fill: $colorBodyFg;\n      font-size: 1em;\n      paint-order: stroke;\n      font-weight: bold;\n      stroke: $colorBodyBg;\n      stroke-linecap: butt;\n      stroke-linejoin: bevel;\n      stroke-width: 6px;\n    }\n  }\n\n  body.desktop .is-fixed-mode & {\n    background-size: 3px 30%;\n    background-color: $colorBodyBgSubtle;\n    box-shadow: inset rgba(black, 0.4) 0 1px 1px;\n\n    svg text {\n      fill: $colorBodyFg;\n      stroke: $colorBodyBgSubtle;\n    }\n  }\n\n  .is-realtime-mode & {\n    $c: 1px solid rgba($colorTimeRealtime, 0.7);\n    border-left: $c;\n    border-right: $c;\n    svg text {\n      fill: $colorTimeRealtime;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/conductor-mode-icon.scss",
    "content": "@keyframes clock-hands {\n  0% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  100% {\n    transform: translate(-50%, -50%) rotate(360deg);\n  }\n}\n\n@keyframes clock-hands-sticky {\n  0% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  7% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  8% {\n    transform: translate(-50%, -50%) rotate(30deg);\n  }\n  15% {\n    transform: translate(-50%, -50%) rotate(30deg);\n  }\n  16% {\n    transform: translate(-50%, -50%) rotate(60deg);\n  }\n  24% {\n    transform: translate(-50%, -50%) rotate(60deg);\n  }\n  25% {\n    transform: translate(-50%, -50%) rotate(90deg);\n  }\n  32% {\n    transform: translate(-50%, -50%) rotate(90deg);\n  }\n  33% {\n    transform: translate(-50%, -50%) rotate(120deg);\n  }\n  40% {\n    transform: translate(-50%, -50%) rotate(120deg);\n  }\n  41% {\n    transform: translate(-50%, -50%) rotate(150deg);\n  }\n  49% {\n    transform: translate(-50%, -50%) rotate(150deg);\n  }\n  50% {\n    transform: translate(-50%, -50%) rotate(180deg);\n  }\n  57% {\n    transform: translate(-50%, -50%) rotate(180deg);\n  }\n  58% {\n    transform: translate(-50%, -50%) rotate(210deg);\n  }\n  65% {\n    transform: translate(-50%, -50%) rotate(210deg);\n  }\n  66% {\n    transform: translate(-50%, -50%) rotate(240deg);\n  }\n  74% {\n    transform: translate(-50%, -50%) rotate(240deg);\n  }\n  75% {\n    transform: translate(-50%, -50%) rotate(270deg);\n  }\n  82% {\n    transform: translate(-50%, -50%) rotate(270deg);\n  }\n  83% {\n    transform: translate(-50%, -50%) rotate(300deg);\n  }\n  90% {\n    transform: translate(-50%, -50%) rotate(300deg);\n  }\n  91% {\n    transform: translate(-50%, -50%) rotate(330deg);\n  }\n  99% {\n    transform: translate(-50%, -50%) rotate(330deg);\n  }\n  100% {\n    transform: translate(-50%, -50%) rotate(360deg);\n  }\n}\n\n.c-clock-symbol {\n  $c: rgba($colorBodyFg, 0.5);\n  $d: 16px;\n  height: $d;\n  width: $d;\n  position: relative;\n\n  &__outer {\n    // SVG brackets shape\n    width: 100%;\n    height: 100%;\n    fill: $c;\n  }\n\n  // Clock hands\n  div[class*='hand'] {\n    $handW: 2px;\n    $handH: $d * 0.4;\n    animation-iteration-count: infinite;\n    animation-timing-function: steps(12);\n    transform-origin: bottom;\n    position: absolute;\n    height: $handW;\n    width: $handW;\n    left: 50%;\n    top: 50%;\n    z-index: 2;\n\n    &:before {\n      background: $c;\n      content: '';\n      display: block;\n      position: absolute;\n      width: 100%;\n      bottom: -1px;\n    }\n\n    &.hand-little {\n      z-index: 2;\n      animation-duration: 12s;\n      transform: translate(-50%, -50%) rotate(120deg);\n\n      &:before {\n        height: ceil($handH * 0.6);\n      }\n    }\n\n    &.hand-big {\n      z-index: 1;\n      animation-duration: 1s;\n      transform: translate(-50%, -50%);\n\n      &:before {\n        height: $handH;\n      }\n    }\n  }\n\n  // Modes\n  .is-realtime-mode &,\n  .is-lad-mode & {\n    $c: $colorTimeRealtimeFgSubtle;\n\n    .c-clock-symbol__outer {\n      // Brackets icon\n      fill: $c;\n    }\n\n    div[class*='hand'] {\n      animation-name: clock-hands;\n\n      &:before {\n        background: $c;\n      }\n    }\n  }\n}\n\n// Contexts\n.c-so-view--no-frame {\n  .c-compact-tc:not(.is-expanded) {\n    .c-clock-symbol {\n      $c: $frameControlsColorFg;\n\n      &__outer {\n        fill: $c;\n      }\n\n      div[class*='hand']:before {\n        background: $c;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/conductor.scss",
    "content": ".c-input--submit {\n    // Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work\n    visibility: hidden;\n    height: 0;\n    width: 0;\n    padding: 0;\n}\n\n/*********************************************** CONDUCTOR LAYOUT */\n.c-conductor {\n    &__inputs {\n        display: flex;\n        flex: 0 0 auto;\n\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    &__ticks {\n        flex: 1 1 auto;\n    }\n\n    &__controls {\n        grid-area: tc-controls;\n        display: flex;\n        align-items: center;\n\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    /************************************ FIXED MODE STYLING */\n    &.is-fixed-mode {\n        .c-conductor-axis {\n            &__zoom-indicator {\n                border: 1px solid transparent;\n                display: none; // Hidden by default\n            }\n        }\n\n        &:not(.is-panning),\n        &:not(.is-zooming) {\n            .c-conductor-axis {\n                &:hover,\n                &:active {\n                    cursor: col-resize;\n                }\n            }\n        }\n\n        &.is-panning,\n        &.is-zooming {\n            .c-conductor-input input {\n                // Styles for inputs while zooming or panning\n                background: rgba($timeConductorActiveBg, 0.4);\n            }\n        }\n\n        &.alt-pressed {\n            .c-conductor-axis:hover {\n                // When alt is being pressed and user is hovering over the axis, set the cursor\n                @include cursorGrab();\n            }\n        }\n\n        &.is-panning {\n            .c-conductor-axis {\n                @include cursorGrab();\n                background-color: $timeConductorActivePanBg;\n                transition: $transIn;\n\n                svg text {\n                    stroke: $timeConductorActivePanBg;\n                    transition: $transIn;\n                }\n            }\n        }\n\n        &.is-zooming {\n            .c-conductor-axis__zoom-indicator {\n                display: block;\n                position: absolute;\n                background: rgba($timeConductorActiveBg, 0.4);\n                border-left-color: $timeConductorActiveBg;\n                border-right-color: $timeConductorActiveBg;\n                top: 0;\n                bottom: 0;\n            }\n        }\n    }\n\n    /************************************ REAL-TIME MODE STYLING */\n    &.is-realtime-mode {\n        .c-conductor__time-bounds {\n            grid-template-columns: 20px auto 1fr auto auto;\n            grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end';\n        }\n\n        .c-conductor__end-fixed {\n            grid-area: tc-updated;\n        }\n    }\n}\n\n.c-conductor-holder--compact {\n    flex: 0 1 auto;\n    overflow: hidden;\n\n    .c-conductor {\n        &__inputs,\n        &__time-bounds {\n            display: flex;\n            flex: 0 1 auto;\n            overflow: hidden;\n        }\n\n        &__inputs {\n            > * + * {\n                margin-left: $interiorMarginSm;\n            }\n        }\n    }\n\n    .is-realtime-mode .c-conductor__end-fixed {\n        display: none !important;\n    }\n}\n\n.c-conductor-input {\n    color: $colorInputFg;\n    display: flex;\n    align-items: center;\n    justify-content: flex-start;\n\n    > * + * {\n        margin-left: $interiorMarginSm;\n    }\n\n    &:before {\n        // Realtime-mode clock icon symbol\n        margin-right: $interiorMarginSm;\n    }\n\n    input:invalid {\n        background: rgba($colorFormInvalid, 0.5);\n    }\n}\n\n.is-realtime-mode {\n    .c-conductor__delta-button {\n        color: $colorTimeRealtimeFg;\n    }\n\n    .c-conductor-input {\n        &:before {\n            color: $colorTimeRealtimeFgSubtle;\n        }\n    }\n\n    .c-conductor__end-fixed {\n        // Displays last RT update\n        color: $colorTimeRealtimeFgSubtle;\n\n        input {\n            // Remove input look\n            background: none;\n            box-shadow: none;\n            color: $colorTimeRealtimeFgSubtle;\n            pointer-events: none;\n\n            &[disabled] {\n                opacity: 1 !important;\n            }\n        }\n    }\n}\n\n.pr-tc-input-menu--start,\n.pr-tc-input-menu--end {\n    background: $colorBodyBg;\n    border-radius: $controlCr;\n    display: grid;\n    grid-template-columns: 1fr 1fr 2fr;\n    grid-column-gap: 3px;\n    grid-row-gap: 4px;\n    align-items: start;\n    box-shadow: $shdwMenu;\n    padding: $interiorMarginLg;\n    position: absolute;\n    left: 8px;\n    bottom: 24px;\n    z-index: 99;\n\n    &[class*='--bottom'] {\n        bottom: auto;\n        top: 24px;\n    }\n}\n\n.pr-tc-input-menu {\n    &__options {\n        display: flex;\n\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    &__input-grid {\n        display: grid;\n        grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;\n        grid-column-gap: 3px;\n        grid-row-gap: $interiorMargin;\n        align-items: start;\n    }\n}\n\n.l-shell__time-conductor .pr-tc-input-menu--end {\n    left: auto;\n    right: 0;\n}\n\n.pr-time-label {\n    font-size: 0.9em;\n    text-transform: uppercase;\n\n    &:before {\n        font-size: 0.8em;\n        margin-right: $interiorMarginSm;\n    }\n}\n\n.pr-time-input {\n    display: flex;\n    align-items: center;\n    white-space: nowrap;\n\n    > * + * {\n        margin-left: $interiorMarginSm;\n    }\n\n    input {\n        height: 22px;\n        line-height: 1em;\n        font-size: 1.25em;\n    }\n\n    &--date input {\n        width: 85px;\n    }\n\n    &--time input {\n        width: 70px;\n    }\n\n    &--buttons {\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    &__start-end-sep {\n        height: 100%;\n    }\n\n    &--input-and-button {\n        @include wrappedInput();\n        padding-right: unset;\n    }\n}\n\n/*********************************************** COMPACT TIME CONDUCTOR */\n.c-compact-tc,\n.c-tc-input-popup {\n    [class*='start-end-sep'] {\n        opacity: 0.5;\n    }\n}\n\n.c-compact-tc {\n    border-radius: $controlCr;\n    display: flex;\n    flex: 0 1 auto;\n    align-items: center;\n    padding: 2px 0;\n\n    &__setting-wrapper {\n        display: contents;\n    }\n\n    &__setting-value {\n        border-right: 1px solid rgba($colorTimeCommonFg, 0.3);\n        cursor: pointer;\n        color: $colorTimeCommonFg;\n        align-items: center;\n        display: flex;\n        flex: 0 1 auto;\n        overflow: hidden;\n        padding: 0 $fadeTruncateW;\n        position: relative;\n        max-width: max-content;\n        text-transform: uppercase;\n        white-space: nowrap;\n\n        > * + * {\n            margin-left: $interiorMarginSm;\n\n            &:before {\n                content: \" - \";\n                display: inline-block;\n                opacity: 0.4;\n            }\n        }\n\n        &[class*=\"icon\"] {\n            &:before {\n                font-size: 0.75em;\n                line-height: 80%;\n                margin-right: $interiorMarginSm;\n            }\n        }\n    }\n\n    .c-toggle-switch,\n    .c-clock-symbol,\n    .c-conductor__mode-icon {\n        // Used in independent Time Conductor\n        flex: 0 0 auto;\n    }\n\n    .c-toggle-switch {\n        margin-right: $interiorMarginSm;\n    }\n\n    .c-conductor__mode-icon {\n        margin-left: $interiorMargin;\n    }\n\n    .c-so-view & {\n        // Time Conductor in a Layout frame\n        padding: 3px 0;\n\n        .c-clock-symbol {\n            $h: 13px;\n            height: $h;\n            width: $h;\n        }\n\n    [class*='button'] {\n      $p: 0px;\n      padding: $p $p + 2;\n    }\n  }\n\n  &.is-fixed-mode {\n    .c-conductor-axis {\n      &__zoom-indicator {\n        border: 1px solid transparent;\n        display: none; // Hidden by default\n      }\n    }\n\n    &:not(.is-panning),\n    &:not(.is-zooming) {\n      .c-conductor-axis {\n        &:hover,\n        &:active {\n          cursor: col-resize;\n        }\n      }\n    }\n\n    &.is-panning,\n    &.is-zooming {\n      .c-conductor-input input {\n        // Styles for inputs while zooming or panning\n        background: rgba($timeConductorActiveBg, 0.4);\n      }\n    }\n\n    &.alt-pressed {\n      .c-conductor-axis:hover {\n        // When alt is being pressed and user is hovering over the axis, set the cursor\n        @include cursorGrab();\n      }\n    }\n\n    &.is-panning {\n      .c-conductor-axis {\n        @include cursorGrab();\n        background-color: $timeConductorActivePanBg;\n        transition: $transIn;\n\n        svg text {\n          stroke: $timeConductorActivePanBg;\n          transition: $transIn;\n        }\n      }\n    }\n\n    &.is-zooming {\n      .c-conductor-axis__zoom-indicator {\n        display: block;\n        position: absolute;\n        background: rgba($timeConductorActiveBg, 0.4);\n        border-left-color: $timeConductorActiveBg;\n        border-right-color: $timeConductorActiveBg;\n        top: 0;\n        bottom: 0;\n      }\n    }\n  }\n}\n\n.u-fade-truncate,\n.u-fade-truncate--lg {\n    .is-fixed-mode & {\n        &:after {\n            @include fadeTruncate($color: $colorTimeFixedBg);\n        }\n    }\n\n    .is-realtime-mode & {\n        &:after {\n            @include fadeTruncate($color: $colorTimeRealtimeBg);\n        }\n    }\n}\n\n.itc-popout.c-tc-input-popup {\n    &--fixed-mode {\n        background: $colorTimeFixedBg;\n        color: $colorTimeFixedFgSubtle;\n\n        em,\n        .pr-time-label:before {\n            color: $colorTimeFixedFg;\n        }\n\n        &__bounds__valuelue {\n            color: $colorTimeFixedFg;\n        }\n\n        &__time-value {\n            color: $colorTimeFixedFg;\n        }\n\n        [class*='c-button--'] {\n            color: $colorTimeFixedBtnFg;\n\n            [class*='label'] {\n                color: $colorTimeRealtimeFg;\n            }\n        }\n        .c-ctrl-wrapper--menus-up{ // A bit hacky, but we are rewriting the CSS class here for ITC such that the calendar opens at the bottom to avoid cutoff\n            .c-menu {\n                top: auto;\n                bottom: revert !important;\n              };\n        }\n    }\n}\n\n.is-fixed-mode.is-expanded {\n    &.c-compact-tc,\n    .c-tc-input-popup {\n        background: $colorTimeFixedBg;\n        color: $colorTimeFixedFgSubtle;\n\n        em,\n        .pr-time-label:before {\n            color: $colorTimeFixedFg;\n        }\n\n        &__bounds__valuelue {\n            color: $colorTimeFixedFg;\n        }\n\n        &__time-value {\n            color: $colorTimeFixedFg;\n        }\n\n        [class*='c-button--'] {\n            color: $colorTimeFixedBtnFg;\n\n            [class*='label'] {\n                color: $colorTimeRealtimeFg;\n            }\n        }\n    }\n\n    &.c-compact-tc {\n        @include hover {\n            $c: $colorTimeFixedHov;\n            background: $c;\n\n            [class*='u-fade-truncate']:after {\n                @include fadeTruncate($color: $c);\n            }\n        }\n    }\n}\n\n.itc-popout.c-tc-input-popup {\n    &--realtime-mode {\n        background: rgba($colorTimeRealtimeBg, 1);\n        color: $colorTimeRealtimeFgSubtle;\n\n        em,\n        .pr-time-label:before {\n            color: $colorTimeRealtimeFg;\n        }\n\n        &__bounds__valuelue {\n            color: $colorTimeRealtimeFg;\n        }\n\n        &__time-value {\n            color: $colorTimeRealtimeFg;\n        }\n\n        [class*='c-button--'] {\n            color: $colorTimeRealtimeBtnFg;\n\n            [class*='label'] {\n                color: $colorTimeRealtimeFg;\n            }\n        }\n    }\n}\n\n.is-realtime-mode.is-expanded {\n    &.c-compact-tc,\n    .c-tc-input-popup {\n        background: rgba($colorTimeRealtimeBg, 1);\n        color: $colorTimeRealtimeFgSubtle;\n\n        em,\n        .pr-time-label:before {\n            color: $colorTimeRealtimeFg;\n        }\n\n        &__bounds__valuelue {\n            color: $colorTimeRealtimeFg;\n        }\n\n        &__time-value {\n            color: $colorTimeRealtimeFg;\n        }\n\n        [class*='c-button--'] {\n            color: $colorTimeRealtimeBtnFg;\n\n            [class*='label'] {\n                color: $colorTimeRealtimeFg;\n            }\n        }\n    }\n\n    &.c-compact-tc {\n        @include hover {\n            $c: $colorTimeRealtimeHov;\n            background: $c;\n\n            [class*='u-fade-truncate']:after {\n                @include fadeTruncate($color: $c);\n            }\n        }\n    }\n\n    .pr-time-input input {\n        width: 3.5em; // Needed for Firefox\n    }\n}\n\n.c-compact-tc {\n    &.l-shell__time-conductor {\n        // Main view\n        min-height: 24px;\n    }\n}\n\n/*********************************************** INPUTS POPUP DIALOG */\n.c-tc-input-popup {\n    @include menuOuter();\n    padding: $interiorMarginLg;\n    position: absolute;\n    width: min-content;\n    bottom: 35px;\n\n    > * + * {\n        margin-top: $interiorMarginLg;\n    }\n\n    &[class*='--bottom'] {\n        bottom: auto;\n        top: 35px;\n    }\n\n    &__options {\n        display: flex;\n\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n\n        .c-button--menu {\n            padding: cButtonPadding($compact: true);\n        }\n    }\n    .pr-time{\n        // FIXED TIME MODE\n        &-label-start-date{\n            grid-area: sDate;\n        }\n        &-label-start-time{\n            grid-area: sTime;\n        }\n        &-input-start-date{\n            grid-area: sDateInput;\n        }\n        &-input-start-time{\n            grid-area: sTimeInput;\n        }\n        &-label-end-date{\n            grid-area: eDate;\n        }\n        &-label-end-time{\n            grid-area: eTime;\n        }\n        &-input-end-date{\n            grid-area: eDateInput;\n        }\n        &-input-end-time{\n            grid-area: eTimeInput;\n        }\n        &-label-blank-grid{\n            grid-area: blank;\n        }\n\n        // FIXED TIME MODE non utc\n        &-input-start{\n            grid-area: sInput;\n        }\n        &-input-end{\n            grid-area: eInput;\n        }\n\n        //REAL TIME MODE\n        &-label-minus-hrs{\n            grid-area: labelMinusHrs;\n        }\n        &-label-minus-mins{\n            grid-area: labelMinusMins;\n        }\n        &-label-minus-secs{\n            grid-area: labelMinusSecs;\n        }\n        &-label-plus-hrs{\n            grid-area: labelPlusHrs;\n        }\n        &-label-plus-mins{\n            grid-area: labelPlusMins;\n        }\n        &-label-plus-secs{\n            grid-area: labelPlusSecs;\n        }\n        &-input-minus-hrs{\n            grid-area: inputMinusHrs;\n        }\n        &-input-minus-mins{\n            grid-area: inputMinusMins;\n        }\n        &-input-minus-secs{\n            grid-area: inputMinusSecs;\n        }\n        &-input-plus-hrs{\n            grid-area: inputPlusHrs;\n        }\n        &-input-plus-mins{\n            grid-area: inputPlusMins;\n        }\n        &-input-plus-secs{\n            grid-area: inputPlusSecs;\n        }\n        // USED FOR BOTH \n        &-label-blank-grid{\n            grid-area: empty;\n        }\n        &-input__start-end-sep{\n            grid-area: arrowIcon;\n        }\n        &-input--buttons{\n            grid-area: buttons;\n        }\n    }\n\n    &--fixed-mode {\n        .c-tc-input-popup__input-grid-utc {\n            grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;\n            grid-template-areas: \n               \"sDate sTime . eDate eTime .\"\n               \"sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons\";\n        }\n        .c-tc-input-popup__input-grid {\n            grid-template-columns: 1fr 1fr 1fr 2fr;\n            grid-template-areas: \n               \"sTime . eTime .\"\n               \"sInput arrowIcon eInput buttons\";\n        }\n        @include phonePortrait(){ \n            .c-tc-input-popup__input-grid-utc {\n                grid-template-columns: repeat(2, max-content) 1fr;\n                grid-template-areas:\n                    \"sDate sTime .\"\n                    \"sDateInput sTimeInput .\"\n                    \"eDate eTime .\"\n                    \"eDateInput eTimeInput buttons\";\n                padding: 2px; \n                overflow: hidden;\n            }\n            .c-tc-input-popup__input-grid {\n              grid-template-columns: repeat(2, max-content) 1fr;\n              grid-template-areas:\n                  \"sTime .\"\n                  \"sInput .\"\n                  \"eTime .\"\n                  \"eInput buttons\";\n              padding: 2px; \n              overflow: hidden;\n          }\n        }\n\n    }\n\n    &--realtime-mode {\n        .c-tc-input-popup__input-grid, .c-tc-input-popup__input-grid-utc {\n            grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;\n            grid-template-areas:\n                \"labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs .\"\n                \"inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons\";\n        }\n        @include phonePortrait(){ \n            .c-tc-input-popup__input-grid, .c-tc-input-popup__input-grid-utc {\n                grid-template-columns: repeat(3, max-content) 1fr;\n                grid-template-areas:\n                    \"labelMinusHrs labelMinusMins labelMinusSecs .\"\n                    \"inputMinusHrs inputMinusMins inputMinusSecs .\"\n                    \"labelPlusHrs labelPlusMins labelPlusSecs .\"\n                    \"inputPlusHrs inputPlusMins inputPlusSecs buttons\";\n                padding: 2px;\n                overflow: hidden;\n            }\n        }\n    }\n\n    &__input-grid, &__input-grid-utc {\n        display: grid;\n        grid-row-gap: $interiorMargin;\n        grid-column-gap: $interiorMarginSm;\n        align-items: start;\n    }\n}\n@include phonePortrait(){ // Additional styling for mobile portrait.\n    .c-tc-input-popup{\n        width: 100%;\n        &__options{\n            > * {\n                overflow: hidden;\n                [class*= 'ctrl-wrapper']{\n                    [class*='--menu'] {\n                        width: 100%;\n                        [class*='__label'] {\n                            @include ellipsize();\n                        }\n\n                    } \n                }\n            }\n        }\n    }\n\n    .pr-time-input-end-time, .pr-time-input-start-time{\n        > * {\n            margin-right: $interiorMargin;\n        } \n    }\n    .pr-time-input--buttons{\n        justify-content: flex-end;\n    }\n    .pr-time-input__start-end-sep{\n        margin: auto;\n    }\n    .pr-time-input__start-end-sep{\n        display: none;\n    }\n    .pr-time-input-start-date, .pr-time-input-end-date{\n        width: max-content;\n    }\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/conductorPopUpManager.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport raf from '@/utils/raf';\n\nexport default {\n  inject: ['openmct', 'configuration'],\n  data() {\n    return {\n      showConductorPopup: false,\n      positionX: 0,\n      positionY: 0,\n      conductorPopup: null\n    };\n  },\n  mounted() {\n    this.positionBox = raf(this.positionBox);\n    this.timeConductorOptionsHolder = this.$el;\n    this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);\n  },\n  methods: {\n    initializePopup() {\n      this.conductorPopup = this.$refs.conductorPopup.$el;\n      this.$nextTick(() => {\n        window.addEventListener('resize', this.positionBox);\n        document.addEventListener('click', this.handleClickAway);\n        this.positionBox();\n      });\n    },\n    showPopup(clickEvent) {\n      const isAxis = clickEvent.target.closest('.c-conductor-axis') !== null;\n\n      if (isAxis || this.conductorPopup) {\n        return;\n      }\n\n      this.showConductorPopup = true;\n    },\n    positionBox() {\n      if (!this.conductorPopup) {\n        return;\n      }\n\n      const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();\n      const offsetTop = this.conductorPopup.getBoundingClientRect().height;\n\n      this.positionY = timeConductorOptionsBox.top - offsetTop;\n      this.positionX = 0;\n    },\n    clearPopup() {\n      this.showConductorPopup = false;\n      this.conductorPopup = null;\n\n      document.removeEventListener('click', this.handleClickAway);\n      window.removeEventListener('resize', this.positionBox);\n    },\n    handleClickAway(clickAwayEvent) {\n      if (this.canClose(clickAwayEvent)) {\n        clickAwayEvent.stopPropagation();\n        this.clearPopup();\n      }\n    },\n    canClose(clickAwayEvent) {\n      const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;\n      const isPopupElementItem = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);\n\n      return !isChildMenu && !isPopupElementItem;\n    }\n  }\n};\n"
  },
  {
    "path": "src/plugins/timeConductor/date-picker.scss",
    "content": "/******************************************************** PICKER */\n.c-datetime-picker {\n    @include userSelectNone();\n    padding: $interiorMarginLg !important;\n    display: flex !important; // Override .c-menu display: block;\n    flex-direction: column;\n\n    > * + * {\n        margin-top: $interiorMargin;\n    }\n\n    &__close-button {\n        display: none; // Only show when body.phone, see below.\n    }\n\n    &__pager {\n        flex: 0 0 auto;\n    }\n\n    &__calendar {\n        border-top: 1px solid $colorInteriorBorder;\n        flex: 1 1 auto;\n    }\n}\n\n.c-pager {\n    display: grid;\n    grid-column-gap: $interiorMargin;\n    grid-template-rows: 1fr;\n    grid-template-columns: auto 1fr auto;\n    align-items: center;\n\n    .c-icon-button {\n        font-size: 0.8em;\n    }\n\n    &__month-year {\n        text-align: center;\n    }\n}\n\n/******************************************************** CALENDAR */\n.c-calendar {\n    $mutedOpacity: 0.5;\n    display: grid;\n    grid-template-columns: repeat(7, min-content);\n    grid-template-rows: auto;\n    grid-gap: 1px;\n\n    [class*=\"__row\"] {\n        display: contents;\n    }\n\n    .c-calendar__row--header {\n        pointer-events: none;\n\n        .c-calendar-cell {\n            opacity: $mutedOpacity;\n        }\n    }\n\n    .c-calendar-cell {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        padding: $interiorMargin;\n        cursor: pointer;\n\n        @include hover {\n            background: $colorMenuHovBg;\n        }\n\n        &.is-in-month {\n            background: $colorMenuElementHilite;\n        }\n\n        &.selected {\n            background: $colorKeyBg;\n            color: $colorKeyFg;\n        }\n    }\n\n    &__day {\n        &--sub {\n            opacity: $mutedOpacity;\n            font-size: 0.8em;\n        }\n    }\n}\n\n/******************************************************** MOBILE */\nbody.phone {\n    .c-datetime-picker {\n        &.c-menu {\n            @include modalFullScreen();\n        }\n\n        &__close-button {\n            display: flex;\n            justify-content: flex-end;\n        }\n    }\n\n    .c-calendar {\n        grid-template-columns: repeat(7, auto);\n    }\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/independent/IndependentClock.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"clockMenuButton\" class=\"c-ctrl-wrapper c-ctrl-wrapper--menus-up\">\n    <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        v-if=\"selectedClock\"\n        class=\"c-icon-button c-button--menu js-clock-button\"\n        :class=\"selectedClock.cssClass\"\n        aria-label=\"Independent Time Conductor Clock Menu\"\n        @click.prevent.stop=\"showClocksMenu\"\n      >\n        <span class=\"c-button__label\">{{ selectedClock.name }}</span>\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'clock', 'getAllClockMetadata', 'getClockMetadata'],\n  props: {\n    enabled: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  computed: {\n    selectedClock() {\n      return this.getClockMetadata(this.clock);\n    }\n  },\n  watch: {\n    enabled(newValue, oldValue) {\n      if (newValue !== undefined && newValue !== oldValue && newValue === true) {\n        this.setViewFromClock(this.clock);\n      }\n    }\n  },\n  mounted() {\n    this.clocks = this.getAllClockMetadata();\n  },\n  methods: {\n    showClocksMenu() {\n      const elementBoundingClientRect = this.$refs.clockMenuButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y + elementBoundingClientRect.height;\n\n      const menuOptions = {\n        menuClass: 'c-conductor__clock-menu c-super-menu--sm',\n        placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT\n      };\n\n      this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);\n    },\n    setClock(clockKey) {\n      this.setViewFromClock(clockKey);\n    },\n    setViewFromClock(clockOrKey) {\n      let clock = clockOrKey;\n\n      if (!clock.key) {\n        clock = this.getClock(clockOrKey);\n      }\n\n      // if global clock changes, reload and pull it\n      this.selectedClock = this.getClockMetadata(clock);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/independent/IndependentMode.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"modeMenuButton\" class=\"c-ctrl-wrapper c-ctrl-wrapper--menus-up\">\n    <div class=\"c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left\">\n      <button\n        class=\"c-button--menu js-mode-button c-icon-button\"\n        :class=\"selectedMode.cssClass\"\n        aria-label=\"Independent Time Conductor Mode Menu\"\n        @click.prevent.stop=\"showModesMenu\"\n      >\n        <span class=\"c-button__label\">{{ selectedMode.name }}</span>\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'timeMode', 'getAllModeMetadata', 'getModeMetadata'],\n  props: {\n    enabled: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  data() {\n    return {\n      selectedMode: this.getModeMetadata(this.timeMode)\n    };\n  },\n  watch: {\n    timeMode: {\n      handler() {\n        this.setView();\n      }\n    },\n    enabled(newValue, oldValue) {\n      if (newValue !== undefined && newValue !== oldValue && newValue === true) {\n        this.setView();\n      }\n    }\n  },\n  mounted: function () {\n    this.modes = this.getAllModeMetadata();\n  },\n  methods: {\n    showModesMenu() {\n      const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y + elementBoundingClientRect.height;\n\n      const menuOptions = {\n        menuClass: 'c-conductor__mode-menu c-super-menu--sm',\n        placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT\n      };\n      this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);\n    },\n    setView() {\n      this.selectedMode = this.getModeMetadata(this.timeMode);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/independent/IndependentTimeConductor.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"timeConductorOptionsHolder\"\n    class=\"c-compact-tc\"\n    :class=\"[\n      isFixedTimeMode\n        ? 'is-fixed-mode'\n        : independentTCEnabled\n          ? 'is-realtime-mode'\n          : 'is-fixed-mode',\n      { 'is-expanded': independentTCEnabled }\n    ]\"\n    aria-label=\"Independent Time Conductor Panel\"\n  >\n    <ToggleSwitch\n      id=\"independentTCToggle\"\n      class=\"c-toggle-switch--mini\"\n      :checked=\"independentTCEnabled\"\n      :name=\"toggleTitle\"\n      @change=\"toggleIndependentTC\"\n    />\n\n    <ConductorModeIcon />\n\n    <ConductorInputsFixed\n      v-if=\"showFixedInputs\"\n      class=\"c-compact-tc__bounds--fixed\"\n      :read-only=\"true\"\n      :compact=\"true\"\n    />\n\n    <ConductorInputsRealtime\n      v-if=\"showRealtimeInputs\"\n      class=\"c-compact-tc__bounds--real-time\"\n      :read-only=\"true\"\n      :compact=\"true\"\n    />\n\n    <ConductorPopUp\n      v-if=\"showConductorPopup\"\n      ref=\"conductorPopupComponent\"\n      :is-independent=\"true\"\n      :bottom=\"true\"\n      :position-x=\"positionX\"\n      :position-y=\"positionY\"\n      @popup-loaded=\"initializePopup\"\n      @dismiss=\"clearPopup\"\n    />\n  </div>\n</template>\n\n<script>\nimport { computed, inject, onBeforeMount, onBeforeUnmount, provide, ref, watch } from 'vue';\n\nimport ConductorModeIcon from '@/plugins/timeConductor/ConductorModeIcon.vue';\n\nimport ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';\nimport ConductorInputsFixed from '../ConductorInputsFixed.vue';\nimport ConductorInputsRealtime from '../ConductorInputsRealtime.vue';\nimport ConductorPopUp from '../ConductorPopUp.vue';\nimport { useTime } from '../useTime.js';\nimport { useIndependentTimeConductorPopUp } from './useIndependentTimeConductorPopUp.js';\n\nexport default {\n  components: {\n    ConductorModeIcon,\n    ConductorInputsRealtime,\n    ConductorInputsFixed,\n    ConductorPopUp,\n    ToggleSwitch\n  },\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  setup(props) {\n    let unregisterIndependentTimeContext;\n    const timeConductorOptionsHolder = ref(null);\n    const conductorPopupComponent = ref(null);\n    const openmct = inject('openmct');\n    const configuration = openmct.layout?.conductorComponent?.provide?.configuration;\n\n    const keyString = ref(openmct.objects.makeKeyString(props.domainObject.identifier));\n    const independentTCEnabled = ref(props.domainObject.configuration?.useIndependentTime === true);\n    const timeOptions = ref(props.domainObject.configuration?.timeOptions);\n\n    const {\n      timeContext,\n      timeSystemKey,\n      timeSystemFormatter,\n      timeSystemDurationFormatter,\n      isTimeSystemUTCBased,\n      timeMode,\n      isFixedTimeMode,\n      isRealTimeMode,\n      getAllModeMetadata,\n      getModeMetadata,\n      currentValue,\n      bounds,\n      isTick,\n      offsets,\n      clock,\n      getAllClockMetadata,\n      getClockMetadata\n    } = useTime(openmct, () => props.objectPath, configuration, timeOptions, independentTCEnabled);\n\n    const { positionX, positionY, showConductorPopup, initializePopup, clearPopup } =\n      useIndependentTimeConductorPopUp(\n        conductorPopupComponent,\n        timeConductorOptionsHolder,\n        independentTCEnabled\n      );\n\n    const toggleTitle = computed(() => {\n      return `${independentTCEnabled.value ? 'Disable' : 'Enable'} Independent Time Conductor`;\n    });\n    const showFixedInputs = computed(() => {\n      return isFixedTimeMode.value && independentTCEnabled.value;\n    });\n    const showRealtimeInputs = computed(() => {\n      return isRealTimeMode.value && independentTCEnabled.value;\n    });\n\n    function initialize() {\n      timeOptions.value = props.domainObject.configuration?.timeOptions;\n      if (independentTCEnabled.value) {\n        registerIndependentTimeContext();\n      }\n    }\n\n    function handleIndependentTimeConductorChange() {\n      if (independentTCEnabled.value) {\n        registerIndependentTimeContext();\n      } else {\n        clearPopup();\n        unregisterIndependentTimeContext?.();\n        unregisterIndependentTimeContext = null;\n      }\n    }\n\n    function registerIndependentTimeContext() {\n      let shouldUpdateTimeOptions = false;\n\n      if (timeOptions.value === undefined) {\n        timeOptions.value = {};\n        shouldUpdateTimeOptions = true;\n      }\n\n      if (timeOptions.value.fixedOffsets === undefined) {\n        timeOptions.value.fixedOffsets = bounds.value;\n        shouldUpdateTimeOptions = true;\n      }\n\n      if (timeOptions.value.clockOffsets === undefined) {\n        timeOptions.value.clockOffsets = offsets.value;\n        shouldUpdateTimeOptions = true;\n      }\n\n      if (timeOptions.value.mode === undefined) {\n        timeOptions.value.mode = timeMode.value;\n        shouldUpdateTimeOptions = true;\n\n        // check for older configurations that stored a key\n        if (timeOptions.value.mode.key) {\n          timeOptions.value.mode = timeOptions.value.mode.key;\n        }\n      }\n\n      if (timeOptions.value.clock === undefined && timeOptions.value.mode === 'realtime') {\n        timeOptions.value.clock = clock.value.key;\n        shouldUpdateTimeOptions = true;\n      }\n\n      if (timeOptions.value.clock !== undefined && timeOptions.value.mode === 'fixed') {\n        timeOptions.value.clock = undefined;\n        shouldUpdateTimeOptions = true;\n      }\n\n      if (shouldUpdateTimeOptions) {\n        updateTimeOptions();\n      }\n\n      const _isFixedTimeMode = timeOptions.value.mode === 'fixed';\n      const independentTimeContextBoundsOrOffsets = _isFixedTimeMode\n        ? timeOptions.value.fixedOffsets\n        : timeOptions.value.clockOffsets;\n\n      const independentTimeContext = openmct.time.getIndependentContext(keyString.value);\n\n      if (!independentTimeContext.hasOwnContext()) {\n        unregisterIndependentTimeContext = openmct.time.addIndependentContext(\n          keyString.value,\n          independentTimeContextBoundsOrOffsets,\n          timeOptions.value.clock\n        );\n      } else {\n        if (!_isFixedTimeMode) {\n          independentTimeContext.setClock(timeOptions.value.clock);\n        }\n        independentTimeContext.setMode(\n          timeOptions.value.mode,\n          independentTimeContextBoundsOrOffsets\n        );\n      }\n    }\n\n    function toggleIndependentTC() {\n      independentTCEnabled.value = !independentTCEnabled.value;\n\n      openmct.objects.mutate(\n        props.domainObject,\n        'configuration.useIndependentTime',\n        independentTCEnabled.value\n      );\n    }\n\n    function saveFixedBounds() {\n      timeOptions.value.fixedOffsets = bounds.value;\n      updateTimeOptions();\n    }\n\n    function saveClockOffsets() {\n      timeOptions.value.clockOffsets = offsets.value;\n      updateTimeOptions();\n    }\n\n    function saveMode() {\n      timeOptions.value.mode = timeMode.value;\n      updateTimeOptions();\n    }\n\n    function saveClock() {\n      timeOptions.value.clock = clock.value.key;\n      updateTimeOptions();\n    }\n\n    function updateTimeOptions() {\n      openmct.objects.mutate(props.domainObject, 'configuration.timeOptions', timeOptions.value);\n    }\n\n    onBeforeMount(() => {\n      initialize();\n    });\n\n    onBeforeUnmount(() => {\n      unregisterIndependentTimeContext?.();\n      unregisterIndependentTimeContext = null;\n    });\n\n    watch(independentTCEnabled, () => {\n      handleIndependentTimeConductorChange();\n    });\n\n    watch(timeContext, () => {\n      const _keyString = openmct.objects.makeKeyString(props.domainObject.identifier);\n\n      if (_keyString !== keyString.value) {\n        // domain object in object view has changed (via tree navigation)\n        unregisterIndependentTimeContext?.();\n        unregisterIndependentTimeContext = null;\n        keyString.value = _keyString;\n        independentTCEnabled.value = props.domainObject.configuration.useIndependentTime === true;\n\n        initialize();\n      }\n    });\n\n    watch(clock, () => {\n      if (independentTCEnabled.value) {\n        saveClock();\n      }\n    });\n\n    watch(timeMode, () => {\n      if (independentTCEnabled.value && timeMode.value) {\n        saveMode();\n      }\n    });\n\n    watch(offsets, () => {\n      if (independentTCEnabled.value) {\n        saveClockOffsets();\n      }\n    });\n\n    watch(bounds, () => {\n      if (independentTCEnabled.value && isTick.value === false && isFixedTimeMode.value === true) {\n        saveFixedBounds();\n      }\n    });\n\n    provide('timeContext', timeContext);\n    provide('timeSystemKey', timeSystemKey);\n    provide('timeSystemFormatter', timeSystemFormatter);\n    provide('timeSystemDurationFormatter', timeSystemDurationFormatter);\n    provide('isTimeSystemUTCBased', isTimeSystemUTCBased);\n    provide('timeMode', timeMode);\n    provide('isFixedTimeMode', isFixedTimeMode);\n    provide('isRealTimeMode', isRealTimeMode);\n    provide('getAllModeMetadata', getAllModeMetadata);\n    provide('getModeMetadata', getModeMetadata);\n    provide('currentValue', currentValue);\n    provide('bounds', bounds);\n    provide('isTick', isTick);\n    provide('offsets', offsets);\n    provide('clock', clock);\n    provide('getAllClockMetadata', getAllClockMetadata);\n    provide('getClockMetadata', getClockMetadata);\n\n    return {\n      toggleTitle,\n      toggleIndependentTC,\n      showFixedInputs,\n      showRealtimeInputs,\n      keyString,\n      independentTCEnabled,\n      openmct,\n      timeContext,\n      timeMode,\n      clock,\n      timeSystemFormatter,\n      isFixedTimeMode,\n      isRealTimeMode,\n      bounds,\n      isTick,\n      offsets,\n      timeConductorOptionsHolder,\n      conductorPopupComponent,\n      positionX,\n      positionY,\n      showConductorPopup,\n      initializePopup,\n      clearPopup\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeConductor/independent/useIndependentTimeConductorPopUp.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';\n\nimport debounce from '@/utils/debounce';\nimport raf from '@/utils/raf';\n\nexport function useIndependentTimeConductorPopUp(component, parentElement, enabled) {\n  const showConductorPopup = ref(false);\n  const positionX = ref(-10000); // prevents initial flash after appending to body element\n  const positionY = ref(0);\n\n  onMounted(() => {\n    parentElement.value.addEventListener('click', showPopup);\n  });\n\n  onBeforeUnmount(() => {\n    parentElement.value.removeEventListener('click', showPopup);\n    if (component.value?.conductorPopupElement?.parentNode === document.body) {\n      document.body.removeChild(component.value.conductorPopupElement);\n      component.value.conductorPopupElement = null;\n    }\n  });\n\n  const debouncedPositionBox = debounce(raf(positionBox), 250);\n\n  function positionBox() {\n    if (!component.value?.conductorPopupElement) {\n      return;\n    }\n\n    const timeConductorOptionsBox = parentElement.value.getBoundingClientRect();\n    const topHalf = timeConductorOptionsBox.top < window.innerHeight / 2;\n    const padding = 5;\n    const offsetTop = component.value.conductorPopupElement.getBoundingClientRect().height;\n    const popupRight =\n      timeConductorOptionsBox.left + component.value.conductorPopupElement.clientWidth;\n    const offsetLeft = Math.min(window.innerWidth - popupRight, 0);\n\n    if (topHalf) {\n      positionY.value =\n        timeConductorOptionsBox.bottom +\n        component.value.conductorPopupElement.clientHeight +\n        padding;\n    } else {\n      positionY.value = timeConductorOptionsBox.top - padding;\n    }\n\n    positionX.value = timeConductorOptionsBox.left + offsetLeft;\n    positionY.value = positionY.value - offsetTop;\n  }\n\n  function initializePopup() {\n    // we need to append it the first time since the popup has overflow:hidden\n    // then we show/hide based on the flag\n    if (component.value.conductorPopupElement.parentNode !== document.body) {\n      document.body.appendChild(component.value.conductorPopupElement);\n    }\n    nextTick(() => {\n      window.addEventListener('resize', debouncedPositionBox);\n      document.addEventListener('click', handleClickAway);\n      debouncedPositionBox();\n    });\n  }\n\n  function showPopup(clickEvent) {\n    const isToggle = clickEvent.target.classList.contains('c-toggle-switch__slider');\n\n    // no current popup,\n    // itc toggled,\n    // something is emitting a dupe event with pointer id = -1, want to ignore those\n    // itc is currently enabled\n    if (!isToggle && clickEvent.pointerId !== -1 && enabled.value) {\n      showConductorPopup.value = true;\n    }\n  }\n\n  function handleClickAway(clickEvent) {\n    if (canClose(clickEvent)) {\n      clearPopup();\n    }\n  }\n\n  function clearPopup() {\n    if (!component.value?.conductorPopupElement) {\n      return;\n    }\n    showConductorPopup.value = false;\n    positionX.value = -10000; // reset it off screen\n\n    document.removeEventListener('click', handleClickAway);\n    window.removeEventListener('resize', debouncedPositionBox);\n  }\n\n  function canClose(clickAwayEvent) {\n    const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;\n    const isPopupOrChild = clickAwayEvent.target.closest('.c-tc-input-popup') !== null;\n    const isTimeConductor = parentElement.value.contains(clickAwayEvent.target);\n    const isToggle = clickAwayEvent.target.classList.contains('c-toggle-switch__slider');\n\n    return !isTimeConductor && !isChildMenu && !isToggle && !isPopupOrChild;\n  }\n\n  return {\n    showConductorPopup,\n    positionX,\n    positionY,\n    initializePopup,\n    clearPopup\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { markRaw } from 'vue';\n\nimport { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants.js';\nimport Conductor from './ConductorComponent.vue';\n\nfunction isTruthy(a) {\n  return Boolean(a);\n}\n\nfunction validateMenuOption(menuOption, index) {\n  if (menuOption.clock && !menuOption.clockOffsets) {\n    return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\\r\\n${JSON.stringify(\n      menuOption\n    )}`;\n  }\n\n  if (!menuOption.timeSystem) {\n    return `Conductor menu option is missing required property 'timeSystem'\\r\\n${JSON.stringify(\n      menuOption\n    )}`;\n  }\n\n  if (!menuOption.bounds && !menuOption.clock) {\n    return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\\r\\n${JSON.stringify(\n      menuOption\n    )}`;\n  }\n}\n\nfunction hasRequiredOptions(config) {\n  if (config === undefined || config.menuOptions === undefined || config.menuOptions.length === 0) {\n    return \"You must specify one or more 'menuOptions'.\";\n  }\n\n  if (config.menuOptions.some(validateMenuOption)) {\n    return config.menuOptions.map(validateMenuOption).filter(isTruthy).join('\\n');\n  }\n\n  return undefined;\n}\n\nfunction validateConfiguration(config, openmct) {\n  const systems = openmct.time.getAllTimeSystems().reduce(function (m, ts) {\n    m[ts.key] = ts;\n\n    return m;\n  }, {});\n  const clocks = openmct.time.getAllClocks().reduce(function (m, c) {\n    m[c.key] = c;\n\n    return m;\n  }, {});\n\n  return config.menuOptions\n    .map(function (menuOption) {\n      let message = '';\n      if (menuOption.timeSystem && !systems[menuOption.timeSystem]) {\n        message = `Time system '${\n          menuOption.timeSystem\n        }' has not been registered: \\r\\n ${JSON.stringify(menuOption)}`;\n      }\n\n      if (menuOption.clock && !clocks[menuOption.clock]) {\n        message = `Clock '${menuOption.clock}' has not been registered: \\r\\n ${JSON.stringify(\n          menuOption\n        )}`;\n      }\n\n      return message;\n    })\n    .filter(isTruthy)\n    .join('\\n');\n}\n\nfunction throwIfError(configResult) {\n  if (configResult) {\n    throw new Error(\n      `Invalid Time Conductor Configuration. ${configResult} \\r\\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor`\n    );\n  }\n}\n\nfunction mountComponent(openmct, configuration) {\n  const conductorApp = {\n    components: {\n      Conductor\n    },\n    provide: {\n      openmct: openmct,\n      configuration: configuration\n    },\n    template: '<Conductor />'\n  };\n  openmct.layout.conductorComponent = markRaw(conductorApp);\n}\n\n/**\n * @param {TimeConductorConfig} config\n * @returns {Function} install the function to install the time conductor plugin\n */\nexport default function (config) {\n  return function install(openmct) {\n    let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct);\n    throwIfError(configResult);\n\n    const defaults = config.menuOptions[0];\n    const defaultClock = defaults.clock;\n    const defaultMode = defaultClock ? REALTIME_MODE_KEY : FIXED_MODE_KEY;\n    const defaultBounds = defaults?.bounds;\n    let clockOffsets = openmct.time.getClockOffsets();\n\n    if (defaultClock) {\n      openmct.time.setClock(defaults.clock);\n      clockOffsets = defaults.clockOffsets;\n    } else {\n      // always have an active clock, regardless of mode\n      const firstClock = config.menuOptions.find((option) => option.clock);\n\n      if (firstClock) {\n        openmct.time.setClock(firstClock.clock);\n        clockOffsets = firstClock.clockOffsets;\n      }\n    }\n\n    openmct.time.setMode(defaultMode, defaultClock ? clockOffsets : defaultBounds);\n    openmct.time.setTimeSystem(defaults.timeSystem, defaultBounds);\n\n    //We are going to set the clockOffsets in fixed time mode since the conductor components down the line need these\n    if (clockOffsets && defaultMode === FIXED_MODE_KEY) {\n      openmct.time.setClockOffsets(clockOffsets);\n    }\n    //We are going to set the fixed time bounds in realtime time mode since the conductor components down the line need these\n    if (defaultBounds && defaultMode === REALTIME_MODE_KEY) {\n      openmct.time.setBounds(clockOffsets);\n    }\n\n    openmct.on('start', function () {\n      mountComponent(openmct, config);\n    });\n  };\n}\n\n/**\n * @typedef {Object} TimeConductorConfig\n * @property {MenuOptions} menuOptions\n * @property {number?} throttleRate the rate in milliseconds in which to throttle time conductor visual updates\n * @property {number?} records the maximum number of records to keep in the history\n */\n\n/**\n * @typedef {Array<FixedMenuOption | RealtimeMenuOption>} MenuOptions\n */\n\n/**\n * @typedef {Object} FixedMenuOption\n * @property {string?} name the name of this option\n * @property {string} timeSystem the key of the time system to use for this option\n * @property {TimeConductorBounds} bounds the starting bounds for this option\n * @deprecated {number?} records use {@link TimeConductorConfig.records} instead\n * @property {number?} limit\n * @property {Array<FixedPreset>} presets\n */\n\n/**\n * @typedef {Object} RealtimeMenuOption\n * @property {string?} name the name of this option\n * @property {string} timeSystem the key of the time system for this option\n * @property {string} clock the key of the clock for this option\n * @property {ClockOffsets} clockOffsets the starting bounds for this option\n * @property {Array<RealtimePreset>} presets\n */\n\n/**\n * Commonly used fixed bounds can be preloaded for convenient re-use (ie. Yesterday, Mission Time, etc.)\n * @typedef {Object} FixedPreset\n * @property {string} label the display label for this preset\n * @property {TimeConductorBounds} bounds the bounds for this preset\n */\n\n/**\n * Commonly used realtime offsets can be preloaded for convenient re-use (ie. 1 hour, 15 minutes, etc.)\n * @typedef {Object} RealtimePreset\n * @property {string} label the display label for this preset\n * @property {ClockOffsets} clockOffsets the clock offsets for this preset\n * @deprecated {TimeConductorBounds} bounds use {@link RealtimePreset.clockOffsets} instead\n */\n\n/**\n * @typedef {import('../../api/time/TimeContext').TimeConductorBounds} TimeConductorBounds\n * @typedef {import('../../api/time/TimeContext').ClockOffsets} ClockOffsets\n */\n"
  },
  {
    "path": "src/plugins/timeConductor/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { FIXED_MODE_KEY } from '../../api/time/constants.js';\nimport { getPreciseDuration, millisecondsToDHMS } from '../../utils/duration.js';\nimport ConductorPlugin from './plugin.js';\n\nconst THIRTY_SECONDS = 30 * 1000;\nconst ONE_MINUTE = THIRTY_SECONDS * 2;\nconst FIVE_MINUTES = ONE_MINUTE * 5;\nconst FIFTEEN_MINUTES = FIVE_MINUTES * 3;\nconst THIRTY_MINUTES = FIFTEEN_MINUTES * 2;\nconst date = new Date(Date.UTC(78, 0, 20, 0, 0, 0)).getTime();\n\ndescribe('time conductor', () => {\n  let element;\n  let child;\n  let appHolder;\n  let openmct;\n  let config = {\n    menuOptions: [\n      {\n        name: 'FixedTimeRange',\n        timeSystem: 'utc',\n        bounds: {\n          start: date - THIRTY_MINUTES,\n          end: date\n        },\n        presets: [],\n        records: 2\n      },\n      {\n        name: 'LocalClock',\n        timeSystem: 'utc',\n        clock: 'local',\n        clockOffsets: {\n          start: -THIRTY_MINUTES,\n          end: THIRTY_SECONDS\n        },\n        presets: []\n      }\n    ]\n  };\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.install(new ConductorPlugin(config));\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', () => {\n      openmct.time.setMode(FIXED_MODE_KEY, {\n        start: config.menuOptions[0].bounds.start,\n        end: config.menuOptions[0].bounds.end\n      });\n      nextTick(() => {\n        done();\n      });\n    });\n    appHolder = document.createElement('div');\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    appHolder = undefined;\n    openmct = undefined;\n\n    return resetApplicationState(openmct);\n  });\n\n  describe('in fixed time mode', () => {\n    it('shows delta inputs', () => {\n      const fixedModeEl = appHolder.querySelector('.is-fixed-mode');\n      const dateTimeInputs = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');\n      expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Fixed Timespan');\n      expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');\n      expect(dateTimeInputs[2].innerHTML.trim()).toEqual('UTC');\n      const dateTimes = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value');\n      expect(dateTimes[1].innerHTML.trim()).toEqual('1978-01-19 23:30:00.000Z');\n      expect(dateTimes[2].innerHTML.trim()).toEqual('1978-01-20 00:00:00.000Z');\n    });\n  });\n\n  describe('in realtime mode', () => {\n    beforeEach(async () => {\n      openmct.time.setClockOffsets({\n        start: -THIRTY_MINUTES,\n        end: THIRTY_SECONDS\n      });\n\n      const switcher = appHolder.querySelector('.is-fixed-mode');\n      const clickEvent = createMouseEvent('click');\n      switcher.dispatchEvent(clickEvent);\n      await nextTick();\n      const modeButton = switcher.querySelector('.c-tc-input-popup .c-button--menu');\n      const clickEvent1 = createMouseEvent('click');\n      modeButton.dispatchEvent(clickEvent1);\n      await nextTick();\n      const clockItem = document.querySelectorAll(\n        '.c-conductor__mode-menu .c-super-menu__menu li'\n      )[1];\n      const clickEvent2 = createMouseEvent('click');\n      clockItem.dispatchEvent(clickEvent2);\n      await nextTick();\n      await nextTick();\n    });\n\n    it('shows delta inputs', () => {\n      const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');\n      const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');\n      expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Real-Time');\n\n      const dateTimes = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value');\n      expect(dateTimes[1].innerHTML.replace(/[^(\\d|:)]/g, '')).toEqual('00:30:00');\n      expect(dateTimes[2].innerHTML.replace(/[^(\\d|:)]/g, '')).toEqual('00:00:30');\n    });\n\n    it('shows clock options', () => {\n      const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');\n      const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');\n      expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');\n    });\n\n    it('shows the current time', () => {\n      const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');\n      const currentTimeEl = realtimeModeEl.querySelector('.c-compact-tc__current-update');\n      const currentTime = openmct.time.clock().currentValue();\n      const { start, end } = openmct.time.bounds();\n\n      expect(currentTime).toBeGreaterThan(start);\n      expect(currentTime).toBeLessThanOrEqual(end);\n      expect(currentTimeEl.innerHTML.trim().length).toBeGreaterThan(0);\n    });\n  });\n});\n\ndescribe('duration functions', () => {\n  it('should transform milliseconds to DHMS', () => {\n    const functionResults = [\n      millisecondsToDHMS(0),\n      millisecondsToDHMS(86400000),\n      millisecondsToDHMS(129600000),\n      millisecondsToDHMS(661824000),\n      millisecondsToDHMS(213927028)\n    ];\n    const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];\n    expect(validResults).toEqual(functionResults);\n  });\n\n  it('should get precise duration', () => {\n    const functionResults = [\n      getPreciseDuration(0),\n      getPreciseDuration(643680000),\n      getPreciseDuration(1605312000),\n      getPreciseDuration(213927028)\n    ];\n    const validResults = [\n      '00:00:00:00:000',\n      '07:10:48:00:000',\n      '18:13:55:12:000',\n      '02:11:25:27:028'\n    ];\n    expect(validResults).toEqual(functionResults);\n  });\n});\n"
  },
  {
    "path": "src/plugins/timeConductor/useClock.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { onBeforeUnmount, shallowRef, watch } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\n/**\n * Provides reactive `clock` which is reactive to a time context,\n * as well as a function to observe and update the component's clock,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - the time context\n * @returns {{\n *   clock: import('vue').Ref<import('src/api/time/TimeContext.js').Clock>,\n *   getAllClockMetadata: () => Object,\n *   getClockMetadata: () => Object\n * }}\n */\nexport function useClock(openmct, timeContext) {\n  let stopObservingClock;\n\n  const clock = shallowRef(timeContext.value.getClock());\n\n  onBeforeUnmount(() => stopObservingClock?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.clockChanged, updateClock);\n      observeClock();\n    },\n    { immediate: true }\n  );\n\n  function observeClock() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.clockChanged, updateClock);\n    stopObservingClock = () => timeContext.value.off(TIME_CONTEXT_EVENTS.clockChanged, updateClock);\n  }\n\n  function getAllClockMetadata(menuOptions) {\n    const clocks = menuOptions\n      ? menuOptions\n          .map((menuOption) => menuOption.clock)\n          .filter((key, index, array) => key !== undefined && array.indexOf(key) === index)\n          .map((clockKey) => openmct.time.getAllClocks().find((_clock) => _clock.key === clockKey))\n      : openmct.time.getAllClocks();\n\n    const clockMetadata = clocks.map(getClockMetadata);\n\n    return clockMetadata;\n  }\n\n  function getClockMetadata(_clock) {\n    if (_clock === undefined) {\n      return;\n    }\n\n    const clockMetadata = {\n      key: _clock.key,\n      name: _clock.name,\n      description: 'Uses the system clock as the current time basis. ' + _clock.description,\n      cssClass: _clock.cssClass || 'icon-clock',\n      onItemClicked: () => setClock(_clock.key)\n    };\n\n    return clockMetadata;\n  }\n\n  function setClock(key) {\n    timeContext.value.setClock(key);\n  }\n\n  function updateClock(_clock) {\n    clock.value = _clock;\n  }\n\n  return {\n    clock,\n    getAllClockMetadata,\n    getClockMetadata\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useClockOffsets.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { onBeforeUnmount, shallowRef, watch } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\n/**\n * @typedef {import('src/api/time/TimeContext.js').ClockOffsets} ClockOffsets\n */\n\n/**\n * Provides reactive `offsets`,\n * as well as a function to observe and update offsets changes,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - The time context\n * @returns {{\n *   offsets: import('vue').Ref<ClockOffsets>,\n * }}\n */\nexport function useClockOffsets(openmct, timeContext) {\n  let stopObservingClockOffsets;\n\n  const offsets = shallowRef(timeContext.value.getClockOffsets());\n\n  onBeforeUnmount(() => stopObservingClockOffsets?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);\n      observeClockOffsets();\n    },\n    { immediate: true }\n  );\n\n  function observeClockOffsets() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);\n    stopObservingClockOffsets = () =>\n      timeContext.value.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);\n  }\n\n  function updateClockOffsets(_offsets) {\n    offsets.value = _offsets;\n  }\n\n  return {\n    offsets\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTick.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { throttle } from 'lodash';\nimport { onBeforeUnmount, ref, watch } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\n/**\n * @typedef {Number} currentValue current timestamp of time context clock\n */\n\n/**\n * Provides reactive `currentValue` based on observed `tick` event,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - the time context\n * @returns {{\n *   currentValue: import('vue').Ref<currentValue>\n * }}\n */\nexport function useTick(openmct, timeContext, throttleRate) {\n  let stopObservingTick;\n\n  const currentValue = ref(timeContext.value.now());\n\n  const updateTick = throttleRate ? throttle(_updateTick, throttleRate) : _updateTick;\n  onBeforeUnmount(() => stopObservingTick?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.tick, updateTick);\n      observeTick();\n    },\n    { immediate: true }\n  );\n\n  function observeTick() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.tick, updateTick);\n    stopObservingTick = () => timeContext.value.off(TIME_CONTEXT_EVENTS.tick, updateTick);\n  }\n\n  function _updateTick(timestamp) {\n    currentValue.value = timestamp;\n  }\n\n  return {\n    currentValue\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTime.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { watch } from 'vue';\n\nimport { useClock } from './useClock.js';\nimport { useClockOffsets } from './useClockOffsets.js';\nimport { useTick } from './useTick.js';\nimport { useTimeBounds } from './useTimeBounds.js';\nimport { useTimeContext } from './useTimeContext.js';\nimport { useTimeMode } from './useTimeMode.js';\nimport { useTimeSystem } from './useTimeSystem.js';\n\n/**\n * @typedef {import('src/api/time/TimeContext.js').default} TimeContext\n * @typedef {import('src/api/time/GlobalTimeContext.js').default} GlobalTimeContext\n * @typedef {import('src/api/telemetry/TelemetryValueFormatter.js').default} TelemetryValueFormatter\n * @typedef {import('src/api/time/TimeContext.js').Mode} Mode\n * @typedef {import('src/api/time/TimeContext.js').Clock} Clock\n * @typedef {import('src/api/time/TimeContext.js').ClockOffsets} ClockOffsets\n * @typedef {import('src/api/time/TimeContext.js').Bounds} Bounds\n */\n\n/**\n * Provides a reactive interface to the time context based on the current object path\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('openmct').ObjectPath} objectPath - The path of the object\n * @returns {{\n *   timeContext: TimeContext | GlobalTimeContext,\n *   timeSystemFormatter: import('vue').Ref<TelemetryValueFormatter>,\n *   timeSystemDurationFormatter: import('vue').Ref<TelemetryValueFormatter>,\n *   isTimeSystemUTCBased: import('vue').Ref<boolean>,\n *   timeMode: import('vue').Ref<Mode>,\n *   isFixedTimeMode: import('vue').Ref<boolean>,\n *   isRealTimeMode: import('vue').Ref<boolean>,\n *   getAllModeMetadata: import('vue').Ref<() => void>,\n *   getModeMetadata: import('vue').Ref<() => void>,\n *   currentValue: import('vue').Ref<number>,\n *   bounds: import('vue').Ref<Bounds>,\n *   isTick: import('vue').Ref<boolean>,\n *   offsets: import('vue').Ref<ClockOffsets>,\n *   clock: import('vue').Ref<Clock>,\n *   getAllClockMetadata: import('vue').Ref<() => void>,\n *   getClockMetadata: import('vue').Ref<() => void>\n * }}\n */\nexport function useTime(\n  openmct,\n  objectPath,\n  configuration,\n  independentTimeOptions,\n  useIndependentTime\n) {\n  const throttleRate = configuration?.throttleRate ?? 300;\n  const { timeContext } = useTimeContext(openmct, objectPath);\n  const { timeSystemKey, timeSystemFormatter, timeSystemDurationFormatter, isTimeSystemUTCBased } =\n    useTimeSystem(openmct, timeContext);\n  const { timeMode, isFixedTimeMode, isRealTimeMode, getAllModeMetadata, getModeMetadata } =\n    useTimeMode(openmct, timeContext, independentTimeOptions, useIndependentTime);\n  const { bounds, isTick } = useTimeBounds(openmct, timeContext, throttleRate);\n  const { clock, getAllClockMetadata, getClockMetadata } = useClock(openmct, timeContext);\n  const { offsets } = useClockOffsets(openmct, timeContext);\n  const { currentValue } = useTick(openmct, timeContext, throttleRate);\n\n  watch(clock, () => {\n    const optionsMatchingClock = configuration.menuOptions.filter(\n      (option) => option.clock === clock.value.key\n    );\n\n    const clockMatchesTimeSystem = optionsMatchingClock.find(\n      (option) => option.timeSystem === timeSystemKey.value\n    );\n\n    if (!clockMatchesTimeSystem) {\n      const firstMatchingTimeSystem = optionsMatchingClock[0].timeSystem;\n      const optionMatchingTimeSystemWithBounds = configuration.menuOptions.find(\n        (option) => option.timeSystem === firstMatchingTimeSystem && option.bounds && !option.clock\n      );\n\n      timeContext.value.setClockOffsets(\n        optionsMatchingClock[0].clockOffsets ?? optionsMatchingClock[0].bounds\n      );\n\n      if (!optionMatchingTimeSystemWithBounds?.bounds) {\n        console.warn(\n          `No default bounds configured for time system: ${optionMatchingTimeSystemWithBounds?.timeSystem}`\n        );\n      }\n\n      timeContext.value.setTimeSystem(\n        firstMatchingTimeSystem,\n        optionMatchingTimeSystemWithBounds?.bounds\n      );\n    }\n  });\n\n  watch(timeSystemKey, () => {\n    const optionsMatchingTimeSystem = configuration.menuOptions.filter(\n      (option) => option.timeSystem === timeSystemKey.value\n    );\n\n    const timeSystemMatchesClock = optionsMatchingTimeSystem.find(\n      (option) => option.clock === clock.value.key\n    );\n\n    if (!timeSystemMatchesClock) {\n      const optionsWithClock = optionsMatchingTimeSystem.find((option) => option.clock);\n\n      timeContext.value.setClock(optionsWithClock.clock);\n      timeContext.value.setClockOffsets(optionsWithClock.clockOffsets);\n    }\n  });\n\n  return {\n    timeContext,\n    timeSystemKey,\n    timeSystemFormatter,\n    timeSystemDurationFormatter,\n    isTimeSystemUTCBased,\n    timeMode,\n    isFixedTimeMode,\n    isRealTimeMode,\n    getAllModeMetadata,\n    getModeMetadata,\n    currentValue,\n    bounds,\n    isTick,\n    offsets,\n    clock,\n    getAllClockMetadata,\n    getClockMetadata\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTimeBounds.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { throttle } from 'lodash';\nimport { onBeforeUnmount, ref, shallowRef, watch } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\n/**\n * @typedef {import('src/api/time/TimeContext.js').Bounds} Bounds\n */\n\n/**\n * Provides reactive `bounds`,\n * as well as a function to observe and update bounds changes,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - the time context\n * @returns {{\n *   bounds: import('vue').Ref<Bounds>,\n *   isTick: import('vue').Ref<boolean>\n * }}\n */\nexport function useTimeBounds(openmct, timeContext, throttleRate) {\n  let stopObservingTimeBounds;\n\n  const bounds = shallowRef(timeContext.value.getBounds());\n  const isTick = ref(false);\n\n  const updateTimeBounds = throttleRate\n    ? throttle(_updateTimeBounds, throttleRate, {\n        leading: true,\n        trailing: true\n      })\n    : _updateTimeBounds;\n  onBeforeUnmount(() => stopObservingTimeBounds?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.boundsChanged, updateTimeBounds);\n      observeTimeBounds();\n    },\n    { immediate: true }\n  );\n\n  function observeTimeBounds() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.boundsChanged, updateTimeBounds);\n    stopObservingTimeBounds = () =>\n      timeContext.value.off(TIME_CONTEXT_EVENTS.boundsChanged, updateTimeBounds);\n  }\n\n  function _updateTimeBounds(_timeBounds, _isTick) {\n    bounds.value = _timeBounds;\n    isTick.value = _isTick;\n  }\n\n  return {\n    isTick,\n    bounds\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTimeContext.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { shallowRef, toValue, watchEffect } from 'vue';\n\n/**\n * @typedef {import('src/api/time/TimeContext.js').default} TimeContext\n * @typedef {import('src/api/time/GlobalTimeContext.js').default} GlobalTimeContext\n */\n\n/**\n * Provides the reactive TimeContext\n * for the view's objectPath,\n * or the GlobalTimeContext if objectPath is undefined.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('openmct').ObjectPath} objectPath - The path of the object\n * @returns {{\n *   timeContext: TimeContext | GlobalTimeContext\n * }}\n */\nexport function useTimeContext(openmct, objectPath) {\n  const timeContext = shallowRef(null);\n\n  watchEffect(() => getTimeContext());\n\n  function getTimeContext() {\n    const path = toValue(objectPath);\n\n    timeContext.value = path !== undefined ? openmct.time.getContextForView(path) : openmct.time;\n  }\n\n  return { timeContext };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTimeMode.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { computed, onBeforeUnmount, ref, watch } from 'vue';\n\nimport {\n  FIXED_MODE_KEY,\n  REALTIME_MODE_KEY,\n  TIME_CONTEXT_EVENTS\n} from '../../api/time/constants.js';\n\n/**\n * @typedef {import('src/api/time/TimeContext.js').Mode} Mode\n */\n\n/**\n * Provides reactive `timeMode` which is reactive to a time context,\n * as well as a function to observe and update the component's time mode,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - The time context\n * @param {import('vue').Ref<Object>} independentTimeOptions - time options for independent time conductor\n * @param {import('vue').Ref<boolean>} useIndependentTime - Whether to follow independent time conductor\n * @returns {{\n *   timeMode: import('vue').Ref<Mode>,\n *   getAllModeMetadata: import('vue').Ref<() => void>,\n *   getModeMetadata: import('vue').Ref<() => void>,\n *   isFixedTimeMode: import('vue').Ref<boolean>,\n *   isRealTimeMode: import('vue').Ref<boolean>\n * }}\n */\nexport function useTimeMode(openmct, timeContext, independentTimeOptions, useIndependentTime) {\n  let stopObservingTimeMode;\n\n  const timeMode = ref(timeContext.value.getMode());\n  const isFixedTimeMode = computed(() => timeMode.value === FIXED_MODE_KEY);\n  const isRealTimeMode = computed(() => timeMode.value === REALTIME_MODE_KEY);\n\n  onBeforeUnmount(() => stopObservingTimeMode?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);\n      observeTimeMode();\n    },\n    { immediate: true }\n  );\n\n  function observeTimeMode() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);\n    stopObservingTimeMode = () =>\n      timeContext.value.off(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);\n  }\n\n  function getAllModeMetadata() {\n    return [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(getModeMetadata);\n  }\n\n  function getModeMetadata(key) {\n    const fixedModeMetadata = {\n      key: FIXED_MODE_KEY,\n      name: 'Fixed Timespan',\n      description: 'Query and explore data that falls between two fixed datetimes.',\n      cssClass: 'icon-tabular',\n      onItemClicked: () => setTimeMode(key)\n    };\n\n    const realTimeModeMetadata = {\n      key: REALTIME_MODE_KEY,\n      name: 'Real-Time',\n      description:\n        'Monitor streaming data in real-time. The Time Conductor and displays will automatically advance themselves based on the active clock.',\n      cssClass: 'icon-clock',\n      onItemClicked: () => setTimeMode(key)\n    };\n\n    return key === FIXED_MODE_KEY ? fixedModeMetadata : realTimeModeMetadata;\n  }\n\n  function setTimeMode(_timeMode) {\n    if (useIndependentTime?.value === true) {\n      const boundsOrOffsets =\n        _timeMode === FIXED_MODE_KEY\n          ? independentTimeOptions.value.fixedOffsets\n          : independentTimeOptions.value.clockOffsets;\n      timeContext.value.setMode(_timeMode, boundsOrOffsets);\n    } else {\n      timeContext.value.setMode(_timeMode);\n    }\n  }\n\n  function updateTimeMode(_timeMode) {\n    timeMode.value = _timeMode;\n  }\n\n  return {\n    timeMode,\n    getAllModeMetadata,\n    getModeMetadata,\n    isFixedTimeMode,\n    isRealTimeMode\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/useTimeSystem.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { onBeforeUnmount, ref, watch } from 'vue';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\n\nconst DEFAULT_DURATION_FORMATTER = 'duration';\n\n/**\n * @typedef {import('src/api/telemetry/TelemetryValueFormatter.js').default} TelemetryValueFormatter\n */\n\n/**\n * Provides a reactive destructuring of the component's current time system,\n * as well as a function to observe and update the component's time system,\n * which automatically stops observing when the component is unmounted.\n *\n * @param {import('openmct').OpenMCT} openmct - The Open MCT API\n * @param {import('src/api/time/TimeContext.js').default} timeContext - The time context\n * @returns {{\n *   timeSystemKey: import('vue').Ref<string>,\n *   timeSystemFormatter: import('vue').Ref<TelemetryValueFormatter>,\n *   timeSystemDurationFormatter: import('vue').Ref<TelemetryValueFormatter>,\n *   isTimeSystemUTCBased: import('vue').Ref<boolean>\n * }}\n */\nexport function useTimeSystem(openmct, timeContext) {\n  let stopObservingTimeSystem;\n\n  const initialTimeSystem = timeContext.value.getTimeSystem();\n\n  const timeSystemKey = ref(initialTimeSystem.key);\n  const timeSystemFormatter = ref(getFormatter(openmct, initialTimeSystem.timeFormat));\n  const timeSystemDurationFormatter = ref(\n    getFormatter(openmct, initialTimeSystem.durationFormat || DEFAULT_DURATION_FORMATTER)\n  );\n  const isTimeSystemUTCBased = ref(initialTimeSystem.isUTCBased);\n\n  onBeforeUnmount(() => stopObservingTimeSystem?.());\n\n  watch(\n    timeContext,\n    (newContext, oldContext) => {\n      oldContext?.off(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);\n      observeTimeSystem();\n    },\n    { immediate: true }\n  );\n\n  function observeTimeSystem() {\n    timeContext.value.on(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);\n    stopObservingTimeSystem = () =>\n      timeContext.value.off(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);\n  }\n\n  function updateTimeSystem(timeSystem) {\n    timeSystemKey.value = timeSystem.key;\n    timeSystemFormatter.value = getFormatter(openmct, timeSystem.timeFormat);\n    timeSystemDurationFormatter.value = getFormatter(\n      openmct,\n      timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER\n    );\n    isTimeSystemUTCBased.value = timeSystem.isUTCBased;\n  }\n\n  return {\n    timeSystemKey,\n    timeSystemFormatter,\n    timeSystemDurationFormatter,\n    isTimeSystemUTCBased\n  };\n}\n\nfunction getFormatter(openmct, key) {\n  return openmct.telemetry.getValueFormatter({\n    format: key\n  }).formatter;\n}\n"
  },
  {
    "path": "src/plugins/timeConductor/utcMultiTimeFormat.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport moment from 'moment';\n\nexport default function multiFormat(date) {\n  const momentified = moment.utc(date);\n  /**\n   * Uses logic from d3 Time-Scales, v3 of the API. See\n   * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md\n   *\n   * Licensed\n   */\n  const format = [\n    [\n      '.SSS',\n      function (m) {\n        return m.milliseconds();\n      }\n    ],\n    [\n      ':ss',\n      function (m) {\n        return m.seconds();\n      }\n    ],\n    [\n      'HH:mm',\n      function (m) {\n        return m.minutes();\n      }\n    ],\n    [\n      'HH:mm',\n      function (m) {\n        return m.hours();\n      }\n    ],\n    [\n      'ddd DD',\n      function (m) {\n        return m.days() && m.date() !== 1;\n      }\n    ],\n    [\n      'MMM DD',\n      function (m) {\n        return m.date() !== 1;\n      }\n    ],\n    [\n      'MMMM',\n      function (m) {\n        return m.month();\n      }\n    ],\n    [\n      'YYYY',\n      function () {\n        return true;\n      }\n    ]\n  ].filter(function (row) {\n    return row[1](momentified);\n  })[0][0];\n\n  if (format !== undefined) {\n    return moment.utc(date).format(format);\n  }\n}\n"
  },
  {
    "path": "src/plugins/timeline/Container.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * a sizing container for objects in a layout\n */\nclass Container {\n  constructor(domainObject, size) {\n    /**\n     * the identifier of the associated domain object\n     * @type {import('@/api/objects/ObjectAPI.js').Identifier}\n     */\n    this.domainObjectIdentifier = domainObject.identifier;\n    /**\n     * the size in percentage or pixels\n     * @type {number}\n     */\n    this.size = size;\n    /**\n     * the default percentage scale of an object\n     * @type {number}\n     */\n    this.scale = getContainerScale(domainObject);\n    /**\n     * true if the container should be a fixed pixel size\n     * false if the container should be a flexible percentage size\n     * containers are added as flex\n     * @type {boolean}\n     */\n    this.fixed = false;\n  }\n}\n\nfunction getContainerScale(domainObject) {\n  if (domainObject.type === 'telemetry.plot.stacked') {\n    return domainObject?.composition?.length;\n  }\n\n  return 1;\n}\n\nexport default Container;\n"
  },
  {
    "path": "src/plugins/timeline/ExtendedLinesBus.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport default class ExtendedLinesBus extends EventTarget {\n  updateExtendedLines(keyString, lines) {\n    this.dispatchEvent(\n      new CustomEvent('update-extended-lines', {\n        detail: { keyString, lines }\n      })\n    );\n  }\n\n  disableExtendEventLines(keyString) {\n    this.dispatchEvent(\n      new CustomEvent('disable-extended-lines', {\n        detail: keyString\n      })\n    );\n  }\n\n  enableExtendEventLines(keyString) {\n    this.dispatchEvent(\n      new CustomEvent('enable-extended-lines', {\n        detail: keyString\n      })\n    );\n  }\n\n  updateHoverExtendEventLine(keyString, id) {\n    this.dispatchEvent(\n      new CustomEvent('update-extended-hover', {\n        detail: { keyString, id }\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "src/plugins/timeline/ExtendedLinesOverlay.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-timeline__overlay-lines\">\n    <div\n      v-for=\"(lines, key) in extendedLinesPerKey\"\n      :key=\"key\"\n      class=\"c-timeline__overlay-lines__extended-line-container\"\n    >\n      <div\n        v-for=\"(line, index) in lines\"\n        :id=\"line.id\"\n        :key=\"`${index - line.id}`\"\n        class=\"c-timeline__event-line--extended\"\n        :class=\"[\n          line.limitClass,\n          {\n            '--hilite':\n              (hoveredLineId && hoveredKeyString === key && line.id === hoveredLineId) ||\n              (selectedLineId && selectedKeyString === key && line.id === selectedLineId)\n          }\n        ]\"\n        :style=\"{ left: `${line.x + leftOffset}px` }\"\n      ></div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'ExtendedLinesOverlay',\n  props: {\n    extendedLinesPerKey: {\n      type: Object,\n      required: true\n    },\n    height: {\n      type: Number,\n      required: true\n    },\n    leftOffset: {\n      type: Number,\n      default: 0\n    },\n    extendedLineHover: {\n      type: Object,\n      required: true\n    },\n    extendedLineSelection: {\n      type: Object,\n      required: true\n    }\n  },\n  computed: {\n    hoveredLineId() {\n      return this.extendedLineHover.id || null;\n    },\n    hoveredKeyString() {\n      return this.extendedLineHover.keyString || null;\n    },\n    selectedLineId() {\n      return this.extendedLineSelection.id || null;\n    },\n    selectedKeyString() {\n      return this.extendedLineSelection.keyString || null;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeline/TimelineCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst ALLOWED_TYPES = [\n  'telemetry.plot.overlay',\n  'telemetry.plot.stacked',\n  'plan',\n  'gantt-chart',\n  'eventGenerator',\n  'eventGeneratorWithAcknowledge',\n  'yamcs.events',\n  'yamcs.events.severity',\n  'yamcs.events.source',\n  'yamcs.events.source.severity',\n  'yamcs.commands',\n  'yamcs.commands.queue'\n];\nconst DISALLOWED_TYPES = ['telemetry.plot.bar-graph', 'telemetry.plot.scatter-plot'];\nexport default function TimelineCompositionPolicy(openmct) {\n  function hasImageTelemetry(domainObject, metadata) {\n    if (!metadata) {\n      return false;\n    }\n\n    return metadata.valuesForHints(['image']).length > 0;\n  }\n\n  return {\n    allow: function (parent, child) {\n      if (parent.type === 'time-strip') {\n        const metadata = openmct.telemetry.getMetadata(child);\n\n        if (child.type === 'yamcs.event.specific.severity') {\n          console.warn(\n            'Type yamcs.event.specific.severity is deprecated. Use yamcs.events.source.severity.'\n          );\n        }\n\n        if (\n          !DISALLOWED_TYPES.includes(child.type) &&\n          (openmct.telemetry.hasNumericTelemetry(child) ||\n            hasImageTelemetry(child, metadata) ||\n            ALLOWED_TYPES.includes(child.type))\n        ) {\n          return true;\n        }\n\n        return false;\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeline/TimelineElementsContent.vue",
    "content": "<template>\n  <template v-if=\"fixed\">\n    <input\n      :value=\"size\"\n      aria-labelledby=\"pixelSize\"\n      class=\"field control c-input--sm\"\n      :pattern=\"/\\d+/\"\n      type=\"number\"\n      name=\"value\"\n      min=\"0\"\n      @change=\"changeSize\"\n    />\n  </template>\n  <select v-model=\"fixed\" aria-label=\"fixedOrFlex\" @change=\"toggleFixed\">\n    <option :value=\"false\">flex</option>\n    <option :value=\"true\">px</option>\n  </select>\n</template>\n<script>\nimport { inject, ref, watch } from 'vue';\n\nexport default {\n  props: {\n    index: {\n      type: Number,\n      required: true\n    },\n    container: {\n      type: Object,\n      required: true\n    }\n  },\n  setup(props) {\n    const openmct = inject('openmct');\n    const domainObject = inject('domainObject');\n\n    const fixed = ref(props.container.fixed ?? false);\n    const size = ref(props.container.size);\n\n    watch(\n      () => props.container,\n      () => {\n        fixed.value = props.container.fixed;\n        size.value = props.container.size;\n      }\n    );\n\n    function toggleFixed() {\n      openmct.objectViews.emit(\n        `contextAction:${openmct.objects.makeKeyString(domainObject.identifier)}`,\n        'toggleFixedContextAction',\n        props.index,\n        fixed.value\n      );\n    }\n\n    function changeSize(event) {\n      const _size = Number(event.target.value);\n      openmct.objectViews.emit(\n        `contextAction:${openmct.objects.makeKeyString(domainObject.identifier)}`,\n        'changeSizeContextAction',\n        props.index,\n        _size\n      );\n    }\n\n    return {\n      openmct,\n      domainObject,\n      fixed,\n      size,\n      changeSize,\n      toggleFixed\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeline/TimelineElementsPool.vue",
    "content": "<template>\n  <ElementsPool>\n    <template #content=\"{ index }\">\n      <TimelineElementsContent :index=\"index\" :container=\"containers[index]\" />\n    </template>\n    <template #custom>\n      <div class=\"c-inspector__properties c-inspect-properties\" aria-label=\"Swimlane Column\">\n        <div class=\"c-inspect-properties__header\">Swimlane Column</div>\n        <div v-if=\"isEditing\" class=\"c-inspect-properties__row\">\n          <span class=\"c-inspect-properties__label\">Width</span>\n          <div class=\"c-inspect-properties__label align-right\">\n            <input\n              :value=\"swimLaneLabelWidth\"\n              aria-labelledby=\"Width in pixels\"\n              class=\"field control c-input--sm\"\n              :pattern=\"/\\d+/\"\n              type=\"number\"\n              name=\"value\"\n              min=\"0\"\n              @change=\"changeSwimLaneLabelWidth\"\n            />\n            <span>px</span>\n          </div>\n        </div>\n        <div v-else class=\"c-inspect-properties__row\">{{ swimLaneLabelWidth }}px</div>\n      </div>\n    </template>\n  </ElementsPool>\n</template>\n<script>\nimport useIsEditing from 'utils/vue/useIsEditing.js';\nimport { inject, onUnmounted, ref } from 'vue';\n\nimport ElementsPool from '@/plugins/inspectorViews/elements/ElementsPool.vue';\n\nimport getDefaultConfiguration from './configuration.js';\nimport TimelineElementsContent from './TimelineElementsContent.vue';\n\nexport default {\n  components: {\n    ElementsPool,\n    TimelineElementsContent\n  },\n  setup() {\n    const configuration = getDefaultConfiguration();\n    const openmct = inject('openmct');\n    const domainObject = inject('domainObject');\n    const { isEditing } = useIsEditing(openmct);\n\n    // get initial containers configuration from selection context,\n    // as domain.configuration.containers not resilient to composition modifications made outside of view\n    const initialContainers =\n      openmct.selection.get()?.[0]?.[0]?.context?.containers ??\n      domainObject.configuration.containers ??\n      configuration.containers;\n    const initialSwimLaneLabelWidth =\n      domainObject.configuration.swimLaneLabelWidth ?? configuration.swimLaneLabelWidth;\n\n    const containers = ref(initialContainers);\n    const swimLaneLabelWidth = ref(initialSwimLaneLabelWidth);\n    const unobserveContainers = openmct.objects.observe(\n      domainObject,\n      'configuration.containers',\n      updateContainers\n    );\n    const unobserveSwimLaneLabelWidth = openmct.objects.observe(\n      domainObject,\n      'configuration.swimLaneLabelWidth',\n      updateSwimLaneLabelWidth\n    );\n\n    onUnmounted(() => {\n      unobserveContainers?.();\n      unobserveSwimLaneLabelWidth?.();\n    });\n\n    function updateContainers(_containers) {\n      containers.value = _containers;\n    }\n\n    function updateSwimLaneLabelWidth(_swimLaneLabelWidth) {\n      swimLaneLabelWidth.value = _swimLaneLabelWidth;\n    }\n\n    function changeSwimLaneLabelWidth(event) {\n      const _size = Number(event.target.value);\n      openmct.objectViews.emit(\n        `contextAction:${openmct.objects.makeKeyString(domainObject.identifier)}`,\n        'changeSwimLaneLabelWidthContextAction',\n        _size\n      );\n    }\n\n    return { domainObject, containers, changeSwimLaneLabelWidth, swimLaneLabelWidth, isEditing };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeline/TimelineElementsViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport TimelineElementsPool from './TimelineElementsPool.vue';\n\nexport default function TimelineElementsViewProvider(openmct) {\n  return {\n    key: 'timelineElementsView',\n    name: 'Elements',\n    canView: function (selection) {\n      return selection?.[0]?.[0]?.context?.item?.type === 'time-strip';\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      const domainObject = selection?.[0]?.[0]?.context?.item;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                TimelineElementsPool\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: `<TimelineElementsPool />`\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        showTab: function (isEditing) {\n          const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));\n\n          return hasComposition;\n        },\n        priority: function () {\n          return openmct.priority.HIGH - 1;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeline/TimelineObjectView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <SwimLane\n    :icon-class=\"item.type.definition.cssClass\"\n    :status=\"status\"\n    :show-ucontents=\"isPlanLikeObject(item.domainObject)\"\n    :span-rows-count=\"item.rowCount\"\n    :domain-object=\"item.domainObject\"\n    :button-title=\"`Toggle extended event lines overlay for ${item.domainObject.name}`\"\n    button-icon=\"icon-arrows-up-down\"\n    :hide-button=\"!item.isEventTelemetry\"\n    :button-click-on=\"enableExtendEventLines\"\n    :button-click-off=\"disableExtendEventLines\"\n    :class=\"sizeClass\"\n    :style=\"sizeStyle\"\n  >\n    <template #label>\n      {{ item.domainObject.name }}\n    </template>\n    <template #object>\n      <ObjectView\n        ref=\"objectView\"\n        class=\"u-contents\"\n        :default-object=\"item.domainObject\"\n        :object-path=\"item.objectPath\"\n        @change-action-collection=\"setActionCollection\"\n      />\n    </template>\n  </SwimLane>\n</template>\n\n<script>\nimport ObjectView from '@/ui/components/ObjectView.vue';\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\n\nexport default {\n  components: {\n    ObjectView,\n    SwimLane\n  },\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    container: {\n      type: Object,\n      required: true\n    },\n    extendedLinesBus: {\n      type: Object,\n      required: true\n    }\n  },\n  data() {\n    return {\n      domainObject: null,\n      mutablePromise: null,\n      status: ''\n    };\n  },\n  computed: {\n    size() {\n      return this.container.size;\n    },\n    fixed() {\n      return this.container.fixed;\n    },\n    sizeClass() {\n      return `--${this.fixed ? 'fixed' : 'scales'}`;\n    },\n    sizeStyle() {\n      return `flex-basis: ${this.size}${this.fixed ? 'px' : '%'}`;\n    }\n  },\n  watch: {\n    item(newItem) {\n      if (!this.context) {\n        return;\n      }\n\n      this.context.item = newItem.domainObject;\n    }\n  },\n  mounted() {\n    if (this.openmct.objects.supportsMutation(this.item.domainObject.identifier)) {\n      this.mutablePromise = this.openmct.objects\n        .getMutable(this.item.domainObject.identifier)\n        .then(this.setObject);\n    } else {\n      this.openmct.objects.get(this.item.domainObject.identifier).then(this.setObject);\n    }\n  },\n  beforeUnmount() {\n    if (this.removeSelectable) {\n      this.removeSelectable();\n    }\n\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n\n    if (this.mutablePromise) {\n      this.mutablePromise.then(() => {\n        this.openmct.objects.destroyMutable(this.domainObject);\n      });\n    } else if (this?.domainObject?.isMutable) {\n      this.openmct.objects.destroyMutable(this.domainObject);\n    }\n  },\n  methods: {\n    async setObject(domainObject) {\n      this.domainObject = domainObject;\n      this.mutablePromise = null;\n      await this.$nextTick();\n      let reference = this.$refs.objectView;\n\n      if (reference) {\n        let childContext = this.$refs.objectView.getSelectionContext();\n        childContext.item = domainObject;\n        this.context = childContext;\n        if (this.removeSelectable) {\n          this.removeSelectable();\n        }\n\n        this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);\n      }\n\n      if (this.removeStatusListener) {\n        this.removeStatusListener();\n      }\n\n      this.removeStatusListener = this.openmct.status.observe(\n        this.domainObject.identifier,\n        this.setStatus\n      );\n      this.status = this.openmct.status.get(this.domainObject.identifier);\n    },\n    enableExtendEventLines() {\n      const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);\n      this.extendedLinesBus.enableExtendEventLines(keyString);\n    },\n    disableExtendEventLines() {\n      const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);\n      this.extendedLinesBus.disableExtendEventLines(keyString);\n    },\n    setActionCollection(actionCollection) {\n      this.openmct.menus.actionsToMenuItems(\n        actionCollection.getVisibleActions(),\n        actionCollection.objectPath,\n        actionCollection.view\n      );\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    isPlanLikeObject(domainObject) {\n      return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeline/TimelineViewLayout.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"timelineHolder\" class=\"c-timeline-holder\" aria-label=\"Time Strip\">\n    <SwimLane\n      v-for=\"timeSystemItem in timeSystems\"\n      :key=\"timeSystemItem.timeSystem.key\"\n      :can-show-resize-handle=\"true\"\n      :resize-handle-height=\"height\"\n      class=\"c-swimlane__time-axis\"\n      aria-label=\"Time Axis\"\n    >\n      <template #label>\n        {{ timeSystemItem.timeSystem.name }}\n      </template>\n      <template #object>\n        <TimelineAxis\n          :bounds=\"timeSystemItem.bounds\"\n          :time-system=\"timeSystemItem.timeSystem\"\n          :content-height=\"height\"\n          :rendering-engine=\"'svg'\"\n        />\n      </template>\n    </SwimLane>\n\n    <template v-if=\"isCompositionLoaded\">\n      <template v-for=\"(item, index) in items\" :key=\"item.keyString\">\n        <TimelineObjectView\n          class=\"c-timeline__content js-timeline__content\"\n          :class=\"`${'is-object-type-' + item.domainObject.type}`\"\n          :item=\"item\"\n          :container=\"containers[index]\"\n          :extended-lines-bus\n        />\n        <ResizeHandle\n          v-if=\"index !== items.length - 1\"\n          :index=\"index\"\n          drag-orientation=\"vertical\"\n          :is-editing=\"isEditing\"\n          @init-move=\"startContainerResizing\"\n          @move=\"containerResizing\"\n          @end-move=\"endContainerResizing\"\n        />\n      </template>\n    </template>\n\n    <ExtendedLinesOverlay\n      :extended-lines-per-key=\"extendedLinesPerKey\"\n      :height=\"height\"\n      :left-offset=\"extendedLinesLeftOffset\"\n      :extended-line-hover=\"extendedLineHover\"\n      :extended-line-selection=\"extendedLineSelection\"\n    />\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { useDragResizer } from 'utils/vue/useDragResizer.js';\nimport { useFlexContainers } from 'utils/vue/useFlexContainers.js';\nimport { inject, onBeforeUnmount, onMounted, provide, ref, toRaw, watch } from 'vue';\n\nimport SwimLane from '@/ui/components/swim-lane/SwimLane.vue';\nimport ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';\n\nimport TimelineAxis from '../../ui/components/TimeSystemAxis.vue';\nimport { useAlignment } from '../../ui/composables/alignmentContext.js';\nimport { getValidatedData, getValidatedGroups } from '../plan/util.js';\nimport Container from './Container.js';\nimport ExtendedLinesOverlay from './ExtendedLinesOverlay.vue';\nimport TimelineObjectView from './TimelineObjectView.vue';\n\nconst AXES_PADDING = 20;\n\nexport default {\n  components: {\n    ResizeHandle,\n    TimelineObjectView,\n    TimelineAxis,\n    SwimLane,\n    ExtendedLinesOverlay\n  },\n  props: {\n    isEditing: {\n      type: Boolean,\n      default: false\n    }\n  },\n  setup() {\n    const openmct = inject('openmct');\n    const domainObject = inject('domainObject');\n    const path = inject('path');\n\n    const items = ref([]);\n\n    // COMPOSABLE - Time Contexts\n    const timeSystems = ref([]);\n    let timeContext;\n\n    // returned from composition api setup()\n    const setupTimeContexts = {\n      timeSystems\n    };\n\n    onMounted(() => {\n      setTimeContext();\n    });\n\n    onBeforeUnmount(() => {\n      stopFollowingTimeContext();\n    });\n\n    function getTimeSystems() {\n      openmct.time.getAllTimeSystems().forEach((timeSystem) => {\n        timeSystems.value.push({\n          timeSystem,\n          bounds: getBoundsForTimeSystem(timeSystem)\n        });\n      });\n    }\n\n    function getBoundsForTimeSystem(timeSystem) {\n      const currentBounds = timeContext.getBounds();\n\n      //TODO: Some kind of translation via an offset? of current bounds to target timeSystem\n      return currentBounds;\n    }\n\n    function updateViewBounds() {\n      const bounds = timeContext.getBounds();\n      updateContentHeight();\n\n      let currentTimeSystemIndex = timeSystems.value.findIndex(\n        (item) => item.timeSystem.key === openmct.time.getTimeSystem().key\n      );\n      if (currentTimeSystemIndex > -1) {\n        let currentTimeSystem = {\n          ...timeSystems.value[currentTimeSystemIndex]\n        };\n        currentTimeSystem.bounds = bounds;\n        timeSystems.value.splice(currentTimeSystemIndex, 1, currentTimeSystem);\n      }\n    }\n\n    function setTimeContext() {\n      stopFollowingTimeContext();\n\n      timeContext = openmct.time.getContextForView(path);\n      getTimeSystems();\n      updateViewBounds();\n      timeContext.on('boundsChanged', updateViewBounds);\n      timeContext.on('clockChanged', updateViewBounds);\n    }\n\n    function stopFollowingTimeContext() {\n      if (timeContext) {\n        timeContext.off('boundsChanged', updateViewBounds);\n        timeContext.off('clockChanged', updateViewBounds);\n      }\n    }\n\n    // COMPOSABLE - Content Height\n    const timelineHolder = ref(null);\n    const height = ref(null);\n    let handleContentResize;\n    let contentResizeObserver;\n\n    // returned from composition api setup()\n    const setupContentHeight = {\n      timelineHolder,\n      height\n    };\n\n    onMounted(() => {\n      handleContentResize = _.debounce(updateContentHeight, 500);\n      contentResizeObserver = new ResizeObserver(handleContentResize);\n      contentResizeObserver.observe(timelineHolder.value);\n    });\n\n    onBeforeUnmount(() => {\n      handleContentResize.cancel();\n      contentResizeObserver.disconnect();\n    });\n\n    function updateContentHeight() {\n      const clientHeight = getClientHeight();\n      if (height.value !== clientHeight) {\n        height.value = clientHeight;\n      }\n      calculateExtendedLinesLeftOffset();\n    }\n\n    function getClientHeight() {\n      let clientHeight = timelineHolder.value.getBoundingClientRect().height;\n\n      if (!clientHeight) {\n        //this is a hack - need a better way to find the parent of this component\n        let parent = timelineHolder.value.closest('.c-object-view');\n        if (parent) {\n          clientHeight = parent.getBoundingClientRect().height;\n        }\n      }\n\n      return clientHeight;\n    }\n\n    // COMPOSABLE - Composition\n    const composition = ref(null);\n    const isCompositionLoaded = ref(false);\n\n    const compositionCollection = openmct.composition.get(toRaw(domainObject));\n\n    onMounted(() => {\n      compositionCollection.on('add', addItem);\n      compositionCollection.on('remove', removeItem);\n      compositionCollection.on('reorder', reorder);\n    });\n\n    onBeforeUnmount(() => {\n      compositionCollection.off('add', addItem);\n      compositionCollection.off('remove', removeItem);\n      compositionCollection.off('reorder', reorder);\n    });\n\n    const setupComposition = {\n      composition,\n      isCompositionLoaded\n    };\n\n    // COMPOSABLE - Extended Lines\n    const extendedLinesBus = inject('extendedLinesBus');\n    const extendedLinesPerKey = ref({});\n    const extendedLinesLeftOffset = ref(0);\n    const extendedLineHover = ref({});\n    const extendedLineSelection = ref({});\n\n    const { alignment: alignmentData, reset: resetAlignment } = useAlignment(\n      domainObject,\n      path,\n      openmct\n    );\n\n    // returned from composition api setup()\n    const setupExtendedLines = {\n      extendedLinesBus,\n      extendedLinesPerKey,\n      extendedLinesLeftOffset,\n      extendedLineHover,\n      extendedLineSelection,\n      calculateExtendedLinesLeftOffset\n    };\n\n    onMounted(() => {\n      openmct.selection.on('change', checkForLineSelection);\n      extendedLinesBus.addEventListener('update-extended-lines', updateExtendedLines);\n      extendedLinesBus.addEventListener('update-extended-hover', updateExtendedHover);\n    });\n\n    onBeforeUnmount(() => {\n      openmct.selection.off('change', checkForLineSelection);\n      extendedLinesBus.removeEventListener('update-extended-lines', updateExtendedLines);\n      extendedLinesBus.removeEventListener('update-extended-hover', updateExtendedHover);\n      resetAlignment();\n    });\n\n    watch(alignmentData, () => calculateExtendedLinesLeftOffset(), { deep: true });\n\n    function calculateExtendedLinesLeftOffset() {\n      extendedLinesLeftOffset.value = alignmentData.leftWidth + calculateSwimlaneOffset();\n    }\n\n    function calculateSwimlaneOffset() {\n      const firstSwimLane = timelineHolder.value.querySelector('.c-swimlane__lane-object');\n      if (firstSwimLane) {\n        const timelineHolderRect = timelineHolder.value.getBoundingClientRect();\n        const laneObjectRect = firstSwimLane.getBoundingClientRect();\n        const offset = laneObjectRect.left - timelineHolderRect.left;\n        const hasAxes = alignmentData.axes && Object.keys(alignmentData.axes).length > 0;\n        const swimLaneOffset = hasAxes ? offset + AXES_PADDING : offset;\n        return swimLaneOffset;\n      } else {\n        return 0;\n      }\n    }\n\n    function updateExtendedLines(event) {\n      const { keyString, lines } = event.detail;\n      extendedLinesPerKey.value[keyString] = lines;\n    }\n    function updateExtendedHover(event) {\n      const { keyString, id } = event.detail;\n      extendedLineHover.value = { keyString, id };\n    }\n\n    function checkForLineSelection(selection) {\n      const selectionContext = selection?.[0]?.[0]?.context;\n      const eventType = selectionContext?.type;\n      if (eventType === 'time-strip-event-selection') {\n        const event = selectionContext.event;\n        const selectedObject = selectionContext.item;\n        const keyString = openmct.objects.makeKeyString(selectedObject.identifier);\n        extendedLineSelection.value = { keyString, id: event?.time };\n      } else {\n        extendedLineSelection.value = {};\n      }\n    }\n\n    // COMPOSABLE - Swimlane label width\n    const { x: swimLaneLabelWidth, mousedown } = useDragResizer({\n      initialX: domainObject.configuration.swimLaneLabelWidth,\n      callback: mutateSwimLaneLabelWidth\n    });\n\n    provide('swimLaneLabelWidth', swimLaneLabelWidth);\n    provide('mousedown', mousedown);\n\n    // returned from composition api setup()\n    const setupSwimLaneLabelWidth = {\n      changeSwimLaneLabelWidthContextAction\n    };\n\n    function mutateSwimLaneLabelWidth() {\n      openmct.objects.mutate(\n        domainObject,\n        'configuration.swimLaneLabelWidth',\n        swimLaneLabelWidth.value\n      );\n    }\n\n    // context action called from outside component\n    function changeSwimLaneLabelWidthContextAction(size) {\n      swimLaneLabelWidth.value = size;\n      mutateSwimLaneLabelWidth();\n    }\n\n    // COMPOSABLE - flexible containers for swimlane vertical resizing\n    const existingContainers = [];\n\n    const {\n      addContainer,\n      removeContainer,\n      reorderContainers,\n      setContainers,\n      containers,\n      startContainerResizing,\n      containerResizing,\n      endContainerResizing,\n      toggleFixed,\n      sizeFixedContainer\n    } = useFlexContainers(timelineHolder, {\n      containers: domainObject.configuration.containers,\n      rowsLayout: true,\n      callback: mutateContainers\n    });\n\n    // returned from composition api setup()\n    const setupFlexContainers = {\n      containers,\n      startContainerResizing,\n      containerResizing,\n      endContainerResizing,\n      toggleFixedContextAction,\n      changeSizeContextAction\n    };\n\n    compositionCollection.load().then((loadedComposition) => {\n      composition.value = loadedComposition;\n      isCompositionLoaded.value = true;\n\n      // check if containers configuration matches composition\n      // in case composition has been modified outside of view\n      // if so, rebuild containers to match composition\n      // sync containers to composition,\n      // in case composition modified outside of view\n      // but do not mutate until user makes a change\n      let isConfigurationChanged = false;\n      composition.value.forEach((object, index) => {\n        const containerIndex = domainObject.configuration.containers.findIndex((container) =>\n          openmct.objects.areIdsEqual(container.domainObjectIdentifier, object.identifier)\n        );\n\n        if (containerIndex !== index) {\n          isConfigurationChanged = true;\n        }\n\n        if (containerIndex > -1) {\n          existingContainers.push(domainObject.configuration.containers[containerIndex]);\n        } else {\n          const container = new Container(object);\n          existingContainers.push(container);\n        }\n      });\n\n      // add check for total size not equal to 100? if comp and containers same, probably safe\n\n      if (isConfigurationChanged) {\n        setContainers(existingContainers);\n      }\n\n      setSelectionContext();\n    });\n\n    function setSelectionContext() {\n      const selection = openmct.selection.get()[0];\n      const selectionContext = selection?.[0]?.context;\n      const selectionDomainObject = selectionContext?.item;\n      const selectionType = selectionDomainObject?.type;\n\n      if (selectionType === 'time-strip') {\n        selectionContext.containers = containers.value;\n        selectionContext.swimLaneLabelWidth = swimLaneLabelWidth.value;\n        openmct.selection.select(selection);\n      }\n    }\n\n    function addItem(_domainObject) {\n      let rowCount = 0;\n\n      const typeKey = _domainObject.type;\n      const type = openmct.types.get(typeKey);\n      const keyString = openmct.objects.makeKeyString(_domainObject.identifier);\n      const objectPath = [_domainObject].concat(path.slice());\n\n      if (typeKey === 'plan') {\n        const planData = getValidatedData(_domainObject);\n        rowCount = getValidatedGroups(_domainObject, planData).length;\n      } else if (typeKey === 'gantt-chart') {\n        rowCount = Object.keys(_domainObject.configuration.swimlaneVisibility).length;\n      }\n      const isEventTelemetry = hasEventTelemetry(_domainObject);\n\n      const item = {\n        domainObject: _domainObject,\n        objectPath,\n        type,\n        keyString,\n        rowCount,\n        isEventTelemetry\n      };\n\n      items.value.push(item);\n\n      if (isCompositionLoaded.value) {\n        const container = new Container(domainObject);\n        addContainer(container);\n      }\n    }\n\n    function removeItem(identifier) {\n      const index = items.value.findIndex((item) =>\n        openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)\n      );\n\n      items.value.splice(index, 1);\n      removeContainer(index);\n\n      delete extendedLinesPerKey.value[openmct.objects.makeKeyString(identifier)];\n    }\n\n    function reorder(reorderPlan) {\n      const oldItems = items.value.slice();\n      reorderPlan.forEach((reorderEvent) => {\n        items.value[reorderEvent.newIndex] = oldItems[reorderEvent.oldIndex];\n      });\n\n      reorderContainers(reorderPlan);\n    }\n\n    function hasEventTelemetry(_domainObject) {\n      const metadata = openmct.telemetry.getMetadata(_domainObject);\n      if (!metadata) {\n        return false;\n      }\n      const hasDomain = metadata.valuesForHints(['domain']).length > 0;\n      const hasNoRange = !metadata.valuesForHints(['range'])?.length;\n      // for the moment, let's also exclude telemetry with images\n      const hasNoImages = !metadata.valuesForHints(['image']).length;\n\n      return hasDomain && hasNoRange && hasNoImages;\n    }\n\n    function mutateContainers() {\n      openmct.objects.mutate(domainObject, 'configuration.containers', containers.value);\n    }\n\n    // context action called from outside component\n    function toggleFixedContextAction(index, fixed) {\n      toggleFixed(index, fixed);\n    }\n\n    // context action called from outside component\n    function changeSizeContextAction(index, size) {\n      sizeFixedContainer(index, size);\n    }\n\n    return {\n      openmct,\n      domainObject,\n      path,\n      items,\n      ...setupComposition,\n      ...setupTimeContexts,\n      ...setupContentHeight,\n      ...setupExtendedLines,\n      ...setupSwimLaneLabelWidth,\n      ...setupFlexContainers\n    };\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timeline/TimelineViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport TimelineViewLayout from './TimelineViewLayout.vue';\n\nexport default function TimelineViewProvider(openmct, extendedLinesBus) {\n  return {\n    key: 'time-strip.view',\n    name: 'TimeStrip',\n    cssClass: 'icon-clock',\n    canView(domainObject) {\n      return domainObject.type === 'time-strip';\n    },\n\n    canEdit(domainObject) {\n      return domainObject.type === 'time-strip';\n    },\n\n    view: function (domainObject, objectPath) {\n      let component = null;\n      let _destroy = null;\n\n      return {\n        show: function (element, isEditing) {\n          const { vNode, destroy } = mount(\n            {\n              el: element,\n              components: {\n                TimelineViewLayout\n              },\n              provide: {\n                openmct,\n                domainObject,\n                path: objectPath,\n                composition: openmct.composition.get(domainObject),\n                extendedLinesBus\n              },\n              data() {\n                return {\n                  isEditing\n                };\n              },\n              template:\n                '<timeline-view-layout ref=\"timeline\" :is-editing=\"isEditing\"></timeline-view-layout>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          component = vNode.componentInstance;\n          _destroy = destroy;\n        },\n        contextAction(action, ...args) {\n          if (component?.$refs?.timeline?.[action]) {\n            component.$refs.timeline[action](...args);\n          }\n        },\n        onEditModeChange(isEditing) {\n          component.isEditing = isEditing;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeline/configuration.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * @typedef {Object} TimeStripConfig configuration for Time Strip views\n * @property {boolean} useIndependentTime true for independent time, false for global time\n * @property {Array<import('./Container').default>} containers\n * @property {number} swimLaneLabelWidth\n */\n\n/**\n * @returns {TimeStripConfig} configuration\n */\nexport default function getDefaultConfiguration() {\n  return {\n    useIndependentTime: false,\n    containers: [],\n    swimLaneLabelWidth: 200\n  };\n}\n"
  },
  {
    "path": "src/plugins/timeline/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport getDefaultConfiguration from './configuration.js';\nimport ExtendedLinesBus from './ExtendedLinesBus.js';\nimport TimelineCompositionPolicy from './TimelineCompositionPolicy.js';\nimport TimelineElementsViewProvider from './TimelineElementsViewProvider.js';\nimport timelineInterceptor from './timelineInterceptor.js';\nimport TimelineViewProvider from './TimelineViewProvider.js';\nconst extendedLinesBus = new ExtendedLinesBus();\n\nexport { extendedLinesBus };\n\nexport default function () {\n  function install(openmct) {\n    openmct.types.addType('time-strip', {\n      name: 'Time Strip',\n      key: 'time-strip',\n      description:\n        'Compose and display time-based telemetry and other object types in a timeline-like view.',\n      creatable: true,\n      cssClass: 'icon-timeline',\n      initialize: function (domainObject) {\n        domainObject.composition = [];\n        domainObject.configuration = getDefaultConfiguration();\n      }\n    });\n    timelineInterceptor(openmct);\n    openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);\n\n    openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus));\n    openmct.inspectorViews.addProvider(new TimelineElementsViewProvider(openmct));\n  }\n\n  install.extendedLinesBus = extendedLinesBus;\n\n  return install;\n}\n"
  },
  {
    "path": "src/plugins/timeline/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { nextTick } from 'vue';\n\nimport { createOpenMct, resetApplicationState } from '@/utils/testing';\n\nimport TimelinePlugin from './plugin.js';\n\ndescribe('the plugin', function () {\n  let objectDef;\n  let appHolder;\n  let element;\n  let child;\n  let openmct;\n  let mockObjectPath;\n  let mockCompositionForTimelist;\n  let planObject = {\n    identifier: {\n      key: 'test-plan-object',\n      namespace: ''\n    },\n    type: 'plan',\n    id: 'test-plan-object',\n    selectFile: {\n      body: JSON.stringify({\n        'TEST-GROUP': [\n          {\n            name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n            start: 1597170002854,\n            end: 1597171032854,\n            type: 'TEST-GROUP',\n            color: 'fuchsia',\n            textColor: 'black'\n          },\n          {\n            name: 'Sed ut perspiciatis',\n            start: 1597171132854,\n            end: 1597171232854,\n            type: 'TEST-GROUP',\n            color: 'fuchsia',\n            textColor: 'black'\n          }\n        ]\n      })\n    }\n  };\n  let timelineObject = {\n    composition: [],\n    configuration: {\n      useIndependentTime: false,\n      timeOptions: {\n        mode: {\n          key: 'fixed'\n        },\n        fixedOffsets: {\n          start: 10,\n          end: 11\n        },\n        clockOffsets: {\n          start: -(30 * 60 * 1000),\n          end: 30 * 60 * 1000\n        }\n      }\n    },\n    name: 'Some timestrip',\n    type: 'time-strip',\n    location: 'mine',\n    modified: 1631005183584,\n    persisted: 1631005183502,\n    identifier: {\n      namespace: '',\n      key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9'\n    }\n  };\n\n  beforeEach((done) => {\n    // Mock clientWidth value\n    Object.defineProperty(HTMLElement.prototype, 'clientWidth', {\n      configurable: true,\n      value: 500\n    });\n\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'time-strip',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n\n    const timeSystem = {\n      timeSystemKey: 'utc',\n      bounds: {\n        start: 1597160002854,\n        end: 1597181232854\n      }\n    };\n\n    openmct = createOpenMct(timeSystem);\n    openmct.install(new TimelinePlugin());\n\n    objectDef = openmct.types.get('time-strip').definition;\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    delete HTMLElement.prototype.clientWidth;\n    return resetApplicationState(openmct);\n  });\n\n  let mockObject = {\n    name: 'Time Strip',\n    key: 'time-strip',\n    creatable: true\n  };\n\n  it('defines a time-strip object type with the correct key', () => {\n    expect(objectDef.key).toEqual(mockObject.key);\n  });\n\n  describe('the time-strip object', () => {\n    it('is creatable', () => {\n      expect(objectDef.creatable).toEqual(mockObject.creatable);\n    });\n  });\n\n  describe('the timeline view', () => {\n    let timelineView;\n    let testViewObject;\n\n    beforeEach(() => {\n      testViewObject = {\n        ...timelineObject\n      };\n\n      const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);\n      timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');\n      let view = timelineView.view(testViewObject, mockObjectPath);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('provides a view', async () => {\n      await nextTick();\n      expect(timelineView).toBeDefined();\n    });\n\n    it('displays a time axis', () => {\n      const el = element.querySelector('.c-timesystem-axis');\n      expect(el).toBeDefined();\n    });\n\n    it('does not show the independent time conductor based on configuration', () => {\n      const independentTimeConductorEl = element.querySelector(\n        '.c-timeline-holder > .c-conductor__controls'\n      );\n      expect(independentTimeConductorEl).toBeNull();\n    });\n  });\n\n  describe('the timeline composition', () => {\n    let timelineDomainObject;\n    let timelineView;\n\n    beforeEach(() => {\n      timelineDomainObject = {\n        ...timelineObject,\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      mockCompositionForTimelist = new EventEmitter();\n      mockCompositionForTimelist.load = () => {\n        mockCompositionForTimelist.emit('add', planObject);\n\n        return [planObject];\n      };\n\n      spyOn(openmct.composition, 'get')\n        .withArgs(timelineDomainObject)\n        .and.returnValue(mockCompositionForTimelist);\n\n      openmct.router.path = [timelineDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelineDomainObject, [timelineDomainObject]);\n      timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');\n      let view = timelineView.view(timelineDomainObject, [timelineDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    xit('loads the plan from composition', async () => {\n      await nextTick();\n      await nextTick();\n      const items = element.querySelectorAll('.js-timeline__content');\n      expect(items.length).toEqual(1);\n    });\n  });\n\n  describe('the independent time conductor', () => {\n    let timelineView;\n    let testViewObject = {\n      ...timelineObject,\n      configuration: {\n        ...timelineObject.configuration,\n        useIndependentTime: true\n      }\n    };\n\n    beforeEach((done) => {\n      const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);\n      timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');\n      let view = timelineView.view(testViewObject, mockObjectPath);\n      view.show(child, true);\n\n      nextTick(done);\n    });\n\n    xit('displays an independent time conductor with saved options - local clock', () => {\n      return nextTick(() => {\n        const independentTimeConductorEl = element.querySelector(\n          '.c-timeline-holder > .c-conductor__controls'\n        );\n        expect(independentTimeConductorEl).toBeDefined();\n\n        const independentTimeContext = openmct.time.getIndependentContext(\n          testViewObject.identifier.key\n        );\n        expect(independentTimeContext.clockOffsets()).toEqual(\n          testViewObject.configuration.timeOptions.clockOffsets\n        );\n      });\n    });\n  });\n\n  describe('the independent time conductor - fixed', () => {\n    let timelineView;\n    let testViewObject2 = {\n      ...timelineObject,\n      id: 'test-object2',\n      identifier: {\n        key: 'test-object2',\n        namespace: ''\n      },\n      configuration: {\n        ...timelineObject.configuration,\n        useIndependentTime: true\n      }\n    };\n\n    beforeEach((done) => {\n      const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath);\n      timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');\n      let view = timelineView.view(testViewObject2, mockObjectPath);\n      view.show(child, true);\n\n      nextTick(done);\n    });\n\n    xit('displays an independent time conductor with saved options - fixed timespan', () => {\n      return nextTick(() => {\n        const independentTimeConductorEl = element.querySelector(\n          '.c-timeline-holder > .c-conductor__controls'\n        );\n        expect(independentTimeConductorEl).toBeDefined();\n\n        const independentTimeContext = openmct.time.getIndependentContext(\n          testViewObject2.identifier.key\n        );\n        expect(independentTimeContext.bounds()).toEqual(\n          testViewObject2.configuration.timeOptions.fixedOffsets\n        );\n      });\n    });\n  });\n\n  describe('The timestrip composition policy', () => {\n    let testObject;\n    beforeEach(() => {\n      testObject = {\n        ...timelineObject,\n        composition: []\n      };\n    });\n\n    it('allows composition for plots', () => {\n      const testTelemetryObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'test-object',\n        name: 'Test Object',\n        telemetry: {\n          values: [\n            {\n              key: 'some-key',\n              name: 'Some attribute',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              key: 'some-other-key',\n              name: 'Another attribute',\n              hints: {\n                range: 1\n              }\n            }\n          ]\n        }\n      };\n      const composition = openmct.composition.get(testObject);\n      expect(() => {\n        composition.add(testTelemetryObject);\n      }).not.toThrow();\n      expect(testObject.composition.length).toBe(1);\n    });\n\n    it('allows composition for plans', () => {\n      const composition = openmct.composition.get(testObject);\n      expect(() => {\n        composition.add(planObject);\n      }).not.toThrow();\n      expect(testObject.composition.length).toBe(1);\n    });\n\n    it('disallows composition for non time-based plots', () => {\n      const barGraphObject = {\n        identifier: {\n          namespace: '',\n          key: 'test-object'\n        },\n        type: 'telemetry.plot.bar-graph',\n        name: 'Test Object'\n      };\n      const composition = openmct.composition.get(testObject);\n      expect(() => {\n        composition.add(barGraphObject);\n      }).toThrow();\n      expect(testObject.composition.length).toBe(0);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/timeline/timeline.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/********************************************* TIME STRIP */\n.c-timeline-holder {\n  overflow: auto;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  gap: 1px;\n  height: 100%;\n\n  // Plot view overrides\n  .gl-plot-display-area,\n  .gl-plot-axis-area.gl-plot-y {\n    bottom: $interiorMargin !important;\n  }\n}\n\n.c-timeline {\n  &__objects {\n    display: contents;\n\n    .c-swimlane {\n      overflow-x: hidden;\n      overflow-y: hidden;\n    }\n\n    .c-object-view {\n      overflow-x: hidden;\n      overflow-y: scroll !important; // `scroll` ensures that right edges align in time\n    }\n  }\n\n  &__content {\n    overflow: auto;\n    &.--scales {\n      flex-grow: 1;\n      flex-shrink: 1;\n    }\n\n    &.--fixed {\n      flex-grow: 0;\n      flex-shrink: 0;\n    }\n  }\n\n  &__overlay-lines {\n    @include abs();\n    opacity: 0.5;\n    top: 20px; // Offset down to line up with time axis ticks line\n    pointer-events: none; // Allows clicks to pass through\n    z-index: 10; // Ensure it sits atop swimlanes\n  }\n\n  &__no-items {\n    font-style: italic;\n    position: absolute;\n    left: $interiorMargin;\n    top: 50%;\n    transform: translateY(-50%);\n    white-space: nowrap;\n  }\n}\n"
  },
  {
    "path": "src/plugins/timeline/timelineInterceptor.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport getDefaultConfiguration from './configuration.js';\n\nexport default function timelineInterceptor(openmct) {\n  openmct.objects.addGetInterceptor({\n    appliesTo: (identifier, domainObject) => {\n      return domainObject && domainObject.type === 'time-strip';\n    },\n    invoke: (identifier, object) => {\n      const configuration = getDefaultConfiguration();\n      if (object && object.configuration === undefined) {\n        object.configuration = configuration;\n      }\n\n      Object.keys(configuration).forEach((key) => {\n        if (object.configuration[key] === undefined) {\n          object.configuration[key] = configuration[key];\n        }\n      });\n\n      return object;\n    }\n  });\n}\n"
  },
  {
    "path": "src/plugins/timelist/ExpandedViewItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-tli\" role=\"row\">\n    <div class=\"c-tli__activity-color\" :style=\"styleClass\"></div>\n    <div class=\"c-tli__contents\" :class=\"listItemClass\">\n      <div class=\"c-tli-row c-tli__title-and-status\">\n        <div class=\"c-tli__title\">{{ formattedItem.title }}</div>\n        <div class=\"c-tli__status-and-icon-graphic\">\n          <div class=\"c-tli__status\">{{ formattedExecutionLabel }}</div>\n          <div class=\"c-tli__graphic\"></div>\n        </div>\n      </div>\n      <div v-if=\"showTimeHero\" class=\"c-tli-row c-tli__time-hero\">\n        <div class=\"c-tli__time-hero-time\" :class=\"countdownClass\">\n          {{ formattedItem.countdown }}\n        </div>\n        <div class=\"c-tli__time-hero-context --subtle\">{{ formattedTimeContextLabel }}</div>\n      </div>\n      <div\n        class=\"c-tli-row c-tli__bounds-and-duration\"\n        :class=\"{ '--has-duration': eventHasDuration }\"\n      >\n        <div class=\"c-tli__start-time\">{{ formattedItem.start }}</div>\n        <div v-if=\"eventHasDuration\" class=\"c-tli__end-time\">{{ formattedItem.end }}</div>\n        <div v-if=\"eventHasDuration\" class=\"c-tli__duration\">\n          <span class=\"--subtle\">DUR</span>\n          {{ formattedItem.duration }}\n        </div>\n        <div v-else class=\"c-tli__duration --subtle\">EVENT TIME</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\nimport { PAST_CSS_SUFFIX } from './constants.js';\nimport { updateProgress } from './svg-progress.js';\n\nconst EXECUTION_STATES = {\n  notStarted: 'Not started',\n  'in-progress': 'In progress',\n  completed: 'Completed',\n  aborted: 'Aborted',\n  skipped: 'Skipped'\n};\n\nconst TIME_CONTEXTS = {\n  start: 'Planned Start',\n  end: 'Planned End',\n  event: 'Planned Event'\n};\n\nconst INFERRED_EXECUTION_STATES = {\n  incomplete: 'Incomplete',\n  overdue: 'Overdue',\n  runningLong: 'Running Long',\n  starts: 'Planned Start',\n  occurs: 'Occurs',\n  occurred: 'Occurred',\n  ends: 'Planned End',\n  ended: 'Ended'\n};\n\nexport default {\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    name: {\n      type: String,\n      default: ''\n    },\n    start: {\n      type: Number,\n      default: 0\n    },\n    end: {\n      type: Number,\n      default: 0\n    },\n    duration: {\n      type: Number,\n      default: 0\n    },\n    activityColor: {\n      type: String,\n      default: 'transparent'\n    },\n    countdown: {\n      type: Number,\n      default: 0\n    },\n    cssClass: {\n      type: String,\n      default: ''\n    },\n    itemProperties: {\n      type: Array,\n      required: true\n    },\n    executionState: {\n      type: String,\n      default: 'notStarted'\n    }\n  },\n  data() {\n    return {\n      formattedExecutionLabel: '',\n      formattedTimeContextLabel: ''\n    };\n  },\n  computed: {\n    countdownClass() {\n      let cssClass = '';\n      if (this.countdown < 0) {\n        cssClass = '--is-countup';\n      } else if (this.countdown > 0) {\n        cssClass = '--is-countdown';\n      }\n      return cssClass;\n    },\n    styleClass() {\n      return { backgroundColor: this.activityColor };\n    },\n    isInProgress() {\n      return this.executionState === 'in-progress';\n    },\n    eventHasDuration() {\n      return this.start !== this.end;\n    },\n    listItemClass() {\n      const timeRelationClass = this.cssClass;\n      const executionStateClass = `--is-${this.executionState}`;\n      return `${timeRelationClass} ${executionStateClass}`;\n    },\n    formattedItem() {\n      let itemValue = {\n        title: this.name\n      };\n      this.itemProperties.forEach((itemProperty) => {\n        let value = this[itemProperty.key];\n        let formattedValue;\n        if (itemProperty.format) {\n          formattedValue = itemProperty.format(value, undefined, itemProperty.key, this.openmct, {\n            skipPrefix: true\n          });\n        }\n        itemValue[itemProperty.key] = formattedValue;\n      });\n\n      return itemValue;\n    },\n    showTimeHero() {\n      // Always show the count up/down \"time hero\" element if activity is in progress\n      if (this.executionState === 'in-progress') {\n        return true;\n      }\n      // Otherwise, show it if the activity is not in the past.\n      return !(this.cssClass === PAST_CSS_SUFFIX);\n    }\n  },\n  created() {\n    this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);\n    this.setTimeContext();\n    this.timestamp = this.timeContext.now();\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.path);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);\n      this.updateTimestamp(this.timeContext.now());\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);\n      }\n    },\n    updateTimestamp(time) {\n      this.timestamp = time;\n      const progressElement = this.$refs.progressElement;\n      if (this.isInProgress && progressElement) {\n        updateProgress(this.start, this.end, this.timestamp, progressElement);\n      }\n      this.formatExecutionLabel();\n      this.formatTimeContextLabel();\n    },\n    formatExecutionLabel() {\n      let label;\n      if (this.executionState !== 'notStarted') {\n        label = EXECUTION_STATES[this.executionState];\n      }\n      if (this.executionState === 'in-progress') {\n        if (this.end < this.timestamp) {\n          label = INFERRED_EXECUTION_STATES.runningLong;\n        }\n      }\n      this.formattedExecutionLabel = label;\n    },\n    formatTimeContextLabel() {\n      let label = this.start < this.timestamp ? TIME_CONTEXTS.end : TIME_CONTEXTS.start;\n      if (!this.eventHasDuration) {\n        label = TIME_CONTEXTS.event;\n      }\n      this.formattedTimeContextLabel = label;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timelist/TimelistComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"timelistHolder\" :class=\"listTypeClass\">\n    <template v-if=\"isExpanded\">\n      <ExpandedViewItem\n        v-for=\"item in sortedItems\"\n        :key=\"item.key\"\n        :name=\"item.name\"\n        :start=\"item.start\"\n        :end=\"item.end\"\n        :duration=\"item.duration\"\n        :countdown=\"item.countdown\"\n        :css-class=\"item.cssClass\"\n        :activity-color=\"item.color\"\n        :item-properties=\"itemProperties\"\n        :execution-state=\"persistedActivityStates[item.id]\"\n        @click.stop=\"setSelectionForActivity(item, $event.currentTarget)\"\n      />\n    </template>\n    <template v-else>\n      <div class=\"c-table c-table--sortable c-list-view c-list-view--sticky-header sticky\">\n        <table class=\"c-table__body js-table__body\">\n          <thead class=\"c-table__header\">\n            <tr>\n              <ListHeader\n                v-for=\"headerItem in headerItems\"\n                :key=\"headerItem.property\"\n                :direction=\"getSortDirection(headerItem)\"\n                :is-sortable=\"headerItem.isSortable\"\n                :aria-label=\"headerItem.name\"\n                :title=\"headerItem.name\"\n                :property=\"headerItem.property\"\n                :current-sort=\"defaultSort.property\"\n                @sort=\"sort\"\n              />\n            </tr>\n          </thead>\n          <tbody>\n            <ListItem\n              v-for=\"item in sortedItems\"\n              :key=\"item.key\"\n              :class=\"{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }\"\n              :item=\"item\"\n              :item-properties=\"itemProperties\"\n              @click.stop=\"setSelectionForActivity(item, $event.currentTarget)\"\n            />\n          </tbody>\n        </table>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { v4 as uuid } from 'uuid';\n\nimport { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';\nimport ListHeader from '../../ui/components/List/ListHeader.vue';\nimport ListItem from '../../ui/components/List/ListItem.vue';\nimport { getPreciseDuration } from '../../utils/duration.js';\nimport { getFilteredValues, getValidatedData, getValidatedGroups } from '../plan/util.js';\nimport { SORT_ORDER_OPTIONS } from './constants.js';\nimport ExpandedViewItem from './ExpandedViewItem.vue';\n\nconst TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';\nconst SAME_DAY_PRECISION_SECONDS = 'HH:mm:ss';\n\nconst CURRENT_CSS_SUFFIX = '--is-current';\nconst PAST_CSS_SUFFIX = '--is-past';\nconst FUTURE_CSS_SUFFIX = '--is-future';\n\nconst headerItems = [\n  {\n    defaultDirection: true,\n    isSortable: true,\n    property: 'start',\n    name: 'Start Time',\n    format: function (value, object, key, openmct, options = {}) {\n      const timeFormat = openmct.time.getTimeSystem().timeFormat;\n      const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n      if (options.skipDateForToday) {\n        return timeFormatter.format(value, SAME_DAY_PRECISION_SECONDS);\n      } else {\n        return timeFormatter.format(value, TIME_FORMAT);\n      }\n    }\n  },\n  {\n    defaultDirection: true,\n    isSortable: true,\n    property: 'end',\n    name: 'End Time',\n    format: function (value, object, key, openmct, options = {}) {\n      const timeFormat = openmct.time.getTimeSystem().timeFormat;\n      const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n      if (options.skipDateForToday) {\n        return timeFormatter.format(value, SAME_DAY_PRECISION_SECONDS);\n      } else {\n        return timeFormatter.format(value, TIME_FORMAT);\n      }\n    }\n  },\n  {\n    defaultDirection: false,\n    property: 'countdown',\n    name: 'Time To/From',\n    format: function (value, object, key, openmct, options = {}) {\n      let result;\n      if (value < 0) {\n        const prefix = options.skipPrefix ? '' : '+';\n        result = `${prefix}${getPreciseDuration(Math.abs(value), {\n          excludeMilliSeconds: true,\n          useDayFormat: true\n        })}`;\n      } else if (value > 0) {\n        const prefix = options.skipPrefix ? '' : '-';\n        result = `${prefix}${getPreciseDuration(value, {\n          excludeMilliSeconds: true,\n          useDayFormat: true\n        })}`;\n      } else {\n        result = 'Now';\n      }\n\n      return result;\n    }\n  },\n  {\n    defaultDirection: false,\n    property: 'duration',\n    name: 'Duration',\n    format: function (value, object, key, openmct) {\n      return `${getPreciseDuration(value, { excludeMilliSeconds: true, useDayFormat: true })}`;\n    }\n  },\n  {\n    defaultDirection: true,\n    property: 'name',\n    name: 'Activity'\n  }\n];\n\nconst defaultSort = {\n  property: 'start',\n  defaultDirection: true\n};\n\nexport default {\n  components: {\n    ExpandedViewItem,\n    ListHeader,\n    ListItem\n  },\n  inject: ['openmct', 'domainObject', 'path', 'composition'],\n  data() {\n    return {\n      planObjects: [],\n      viewBounds: undefined,\n      height: 0,\n      planActivities: [],\n      groups: [],\n      headerItems: headerItems,\n      defaultSort: defaultSort,\n      isExpanded: false,\n      persistedActivityStates: {},\n      sortedItems: []\n    };\n  },\n  computed: {\n    listTypeClass() {\n      if (this.isExpanded) {\n        return 'c-timelist c-timelist--large';\n      }\n      return 'c-timelist';\n    },\n    itemProperties() {\n      return this.headerItems.map((headerItem) => {\n        return {\n          key: headerItem.property,\n          format: headerItem.format\n        };\n      });\n    }\n  },\n  created() {\n    this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);\n\n    this.setTimeContext();\n    this.timestamp = this.timeContext.now();\n  },\n  mounted() {\n    this.isEditing = this.openmct.editor.isEditing();\n\n    this.getPlanDataAndSetConfig(this.domainObject);\n    this.getActivityStates();\n\n    this.unlisten = this.openmct.objects.observe(\n      this.domainObject,\n      'selectFile',\n      this.planFileUpdated\n    );\n    this.unlistenConfig = this.openmct.objects.observe(\n      this.domainObject,\n      'configuration',\n      this.setViewFromConfig\n    );\n    this.removeStatusListener = this.openmct.status.observe(\n      this.domainObject.identifier,\n      this.setStatus\n    );\n    this.status = this.openmct.status.get(this.domainObject.identifier);\n\n    this.openmct.editor.on('isEditing', this.setEditState);\n\n    if (this.composition) {\n      this.composition.on('add', this.addToComposition);\n      this.composition.on('remove', this.removeItem);\n      this.composition.load();\n    }\n\n    this.setFixedTime(this.timeContext.getMode());\n  },\n  beforeUnmount() {\n    if (this.unlisten) {\n      this.unlisten();\n    }\n\n    if (this.unlistenConfig) {\n      this.unlistenConfig();\n    }\n\n    if (this.stopObservingPlan) {\n      this.stopObservingPlan();\n    }\n\n    if (this.stopObservingActivityStatesObject) {\n      this.stopObservingActivityStatesObject();\n    }\n\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n\n    this.openmct.editor.off('isEditing', this.setEditState);\n    this.stopFollowingTimeContext();\n\n    if (this.composition) {\n      this.composition.off('add', this.addToComposition);\n      this.composition.off('remove', this.removeItem);\n    }\n  },\n  methods: {\n    setTimeContext() {\n      this.stopFollowingTimeContext();\n      this.timeContext = this.openmct.time.getContextForView(this.path);\n      this.followTimeContext();\n    },\n    followTimeContext() {\n      this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);\n      this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);\n    },\n    stopFollowingTimeContext() {\n      if (this.timeContext) {\n        this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);\n        this.timeContext.off(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);\n      }\n    },\n    planFileUpdated(selectFile) {\n      this.getPlanData({\n        selectFile,\n        sourceMap: this.domainObject.sourceMap\n      });\n    },\n    async getActivityStates() {\n      const activityStatesObject = await this.openmct.objects.get('activity-states');\n      this.setActivityStates(activityStatesObject);\n      this.stopObservingActivityStatesObject = this.openmct.objects.observe(\n        activityStatesObject,\n        '*',\n        this.setActivityStates\n      );\n    },\n    setActivityStates(activityStatesObject) {\n      this.persistedActivityStates = activityStatesObject.activities;\n    },\n    getPlanDataAndSetConfig(mutatedObject) {\n      this.getPlanData(mutatedObject);\n      this.setViewFromConfig(mutatedObject.configuration);\n    },\n    setViewFromConfig(configuration) {\n      this.filterValue = configuration.filter || '';\n      this.filterMetadataValue = configuration.filterMetadata || '';\n      if (this.isEditing) {\n        this.hideAll = false;\n      } else {\n        this.setSort();\n        this.isExpanded = configuration.isExpanded;\n      }\n      this.listActivities();\n    },\n    updateTimestamp(timestamp) {\n      //The clock never stops ticking\n      this.updateTimeStampAndListActivities(timestamp);\n    },\n    setFixedTime() {\n      this.filterValue = this.domainObject.configuration.filter || '';\n      this.filterMetadataValue = this.domainObject.configuration.filterMetadata || '';\n      this.isFixedTime = !this.timeContext.isRealTime();\n      if (this.isFixedTime) {\n        this.hideAll = false;\n      }\n    },\n    addItem(domainObject) {\n      this.planObjects = [domainObject];\n      if (domainObject.type === 'plan') {\n        this.getPlanDataAndSetConfig({\n          ...this.domainObject,\n          selectFile: domainObject.selectFile,\n          sourceMap: domainObject.sourceMap\n        });\n      }\n      //listen for changes to the plan\n      if (this.stopObservingPlan) {\n        this.stopObservingPlan();\n      }\n      this.stopObservingPlan = this.openmct.objects.observe(\n        this.planObjects[0],\n        '*',\n        this.handlePlanChange\n      );\n    },\n    handlePlanChange(planObject) {\n      this.getPlanData(planObject);\n      this.listActivities();\n    },\n    addToComposition(planObject) {\n      if (this.planObjects.length > 0) {\n        this.confirmReplacePlan(planObject);\n      } else {\n        this.addItem(planObject);\n      }\n    },\n    confirmReplacePlan(planObject) {\n      const dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'This action will replace the current plan. Do you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              const oldTelemetryObject = this.planObjects[0];\n              this.removeFromComposition(oldTelemetryObject);\n              this.addItem(planObject);\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              this.removeFromComposition(planObject);\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    removeFromComposition(planObject) {\n      this.composition.remove(planObject);\n    },\n    removeItem() {\n      this.planObjects = [];\n      this.resetPlanData();\n    },\n    resetPlanData() {\n      this.planData = {};\n      this.groups = [];\n      this.planActivities = [];\n      this.sortedItems = [];\n    },\n    getPlanData(domainObject) {\n      this.resetPlanData();\n      this.planData = getValidatedData(domainObject);\n      this.groups = getValidatedGroups(this.domainObject, this.planData);\n      this.groups.forEach((key) => {\n        if (this.planData[key] === undefined) {\n          return;\n        }\n        // Create new objects so Vue 3 can detect any changes\n        this.planActivities.push(...this.planData[key]);\n      });\n    },\n\n    listActivities() {\n      // filter activities first, then sort\n      const filteredItems = this.planActivities.filter(this.filterActivities);\n      const sortedItems = this.sortItems(filteredItems);\n      this.sortedItems = this.applyStyles(sortedItems);\n    },\n    updateTimeStampAndListActivities(time) {\n      this.timestamp = time;\n\n      this.listActivities();\n    },\n    isActivityInBounds(activity) {\n      const startInBounds =\n        activity.start >= this.timeContext.getBounds()?.start &&\n        activity.start <= this.timeContext.getBounds()?.end;\n      const endInBounds =\n        activity.end >= this.timeContext.getBounds()?.start &&\n        activity.end <= this.timeContext.getBounds()?.end;\n      const middleInBounds =\n        activity.start <= this.timeContext.getBounds()?.start &&\n        activity.end >= this.timeContext.getBounds()?.end;\n\n      return startInBounds || endInBounds || middleInBounds;\n    },\n    isActivityInProgress(activity) {\n      return this.persistedActivityStates[activity.id] === 'in-progress';\n    },\n    filterActivities(activity) {\n      if (this.isEditing) {\n        return true;\n      }\n\n      let hasNameMatch = false;\n      let hasMetadataMatch = false;\n      if (this.filterValue || this.filterMetadataValue) {\n        if (this.filterValue) {\n          hasNameMatch = this.filterByName(activity.name);\n        }\n        if (this.filterMetadataValue) {\n          hasMetadataMatch = this.filterByMetadata(activity);\n        }\n      } else {\n        hasNameMatch = true;\n        hasMetadataMatch = true;\n      }\n\n      const hasFilterMatch = hasNameMatch || hasMetadataMatch;\n      if (hasFilterMatch === false || this.hideAll === true) {\n        return false;\n      }\n\n      // An activity may be out of bounds, but if it is in-progress, we show it.\n      if (!this.isActivityInBounds(activity) && !this.isActivityInProgress(activity)) {\n        return false;\n      }\n      //current event or future start event or past end event\n      const showCurrentEvents = this.domainObject.configuration.currentEventsIndex > 0;\n\n      const isCurrent =\n        showCurrentEvents && this.timestamp >= activity.start && this.timestamp <= activity.end;\n      const isPast = this.timestamp > activity.end;\n      const isFuture = this.timestamp < activity.start;\n\n      return isCurrent || isPast || isFuture;\n    },\n    filterByName(name) {\n      const filters = this.filterValue.split(',');\n\n      return filters.some((search) => {\n        const normalized = search.trim().toLowerCase();\n        const regex = new RegExp(normalized);\n\n        return regex.test(name.toLowerCase());\n      });\n    },\n    filterByMetadata(activity) {\n      const filters = this.filterMetadataValue.split(',');\n\n      return filters.some((search) => {\n        const normalized = search.trim().toLowerCase();\n        const regex = new RegExp(normalized);\n        const activityValues = getFilteredValues(activity);\n\n        return regex.test(activityValues.join().toLowerCase());\n      });\n    },\n    // Add activity classes, increase activity counts by type,\n    styleActivity(activity, index) {\n      if (this.timestamp >= activity.start && this.timestamp <= activity.end) {\n        activity.cssClass = CURRENT_CSS_SUFFIX;\n      } else if (this.timestamp < activity.start) {\n        activity.cssClass = FUTURE_CSS_SUFFIX;\n      } else {\n        activity.cssClass = PAST_CSS_SUFFIX;\n      }\n\n      if (!activity.key) {\n        activity.key = uuid();\n      }\n\n      activity.duration = activity.end - activity.start;\n\n      if (activity.start < this.timestamp) {\n        //if the activity start time has passed, display the time to the end of the activity\n        activity.countdown = activity.end - this.timestamp;\n      } else {\n        activity.countdown = activity.start - this.timestamp;\n      }\n\n      return activity;\n    },\n    applyStyles(activities) {\n      return activities.map(this.styleActivity);\n    },\n    setSort() {\n      const { property, direction } =\n        SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];\n      this.defaultSort = {\n        property,\n        defaultDirection: direction.toLowerCase() === 'asc'\n      };\n    },\n    sortItems(activities) {\n      const sortedItems = _.sortBy(activities, this.defaultSort.property);\n      return this.defaultSort.defaultDirection ? sortedItems : sortedItems.reverse();\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n      this.setViewFromConfig(this.domainObject.configuration);\n    },\n    sort({ property, direction }) {\n      if (this.defaultSort.property === property) {\n        this.defaultSort.defaultDirection = !this.defaultSort.defaultDirection;\n      } else {\n        this.defaultSort.property = property;\n        this.defaultSort.defaultDirection = direction;\n      }\n    },\n    setSelectionForActivity(activity, element) {\n      const multiSelect = false;\n\n      this.openmct.selection.select(\n        [\n          {\n            element,\n            context: {\n              type: 'activity',\n              activity\n            }\n          },\n          {\n            element: this.openmct.layout.$refs.browseObject.$el,\n            context: {\n              item: this.domainObject,\n              supportsMultiSelect: false\n            }\n          }\n        ],\n        multiSelect\n      );\n    },\n    getSortDirection(headerItem) {\n      return this.defaultSort.property === headerItem.property\n        ? this.defaultSort.defaultDirection\n        : headerItem.defaultDirection;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timelist/TimelistCompositionPolicy.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { TIMELIST_TYPE } from '@/plugins/timelist/constants';\n\nexport default function TimelistCompositionPolicy(openmct) {\n  return {\n    allow: function (parent, child) {\n      if (parent.type === TIMELIST_TYPE && child.type !== 'plan') {\n        return false;\n      }\n\n      return true;\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timelist/TimelistViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { TIMELIST_TYPE } from './constants.js';\nimport Timelist from './TimelistComponent.vue';\n\nexport default function TimelistViewProvider(openmct) {\n  return {\n    key: 'timelist.view',\n    name: 'Time List',\n    cssClass: 'icon-timelist',\n    canView(domainObject) {\n      return domainObject.type === TIMELIST_TYPE;\n    },\n\n    canEdit(domainObject) {\n      return domainObject.type === TIMELIST_TYPE;\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Timelist\n              },\n              provide: {\n                openmct,\n                domainObject,\n                path: objectPath,\n                composition: openmct.composition.get(domainObject)\n              },\n              template: '<timelist></timelist>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timelist/constants.js",
    "content": "export const SORT_ORDER_OPTIONS = [\n  {\n    label: 'Start ascending',\n    property: 'start',\n    direction: 'ASC'\n  },\n  {\n    label: 'Start descending',\n    property: 'start',\n    direction: 'DESC'\n  },\n  {\n    label: 'End ascending',\n    property: 'end',\n    direction: 'ASC'\n  },\n  {\n    label: 'End descending',\n    property: 'end',\n    direction: 'DESC'\n  }\n];\n\nexport const TIMELIST_TYPE = 'timelist';\n\nexport const CURRENT_CSS_SUFFIX = '--is-current';\nexport const PAST_CSS_SUFFIX = '--is-past';\nexport const FUTURE_CSS_SUFFIX = '--is-future';\n"
  },
  {
    "path": "src/plugins/timelist/inspector/EventProperties.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <li class=\"c-inspect-properties__row\">\n    <div class=\"c-inspect-properties__label\" title=\"Options for future events.\">{{ label }}</div>\n    <div class=\"c-inspect-properties__value\">\n      <select v-if=\"canEdit\" v-model=\"index\" @change=\"updateForm('index')\">\n        <option\n          v-for=\"(activityOption, activityKey) in activitiesOptions\"\n          :key=\"activityKey\"\n          :value=\"activityKey\"\n        >\n          {{ activityOption }}\n        </option>\n      </select>\n      <span v-else>{{ activitiesOptions[index] }}</span>\n    </div>\n  </li>\n</template>\n\n<script>\nconst ACTIVITIES_OPTIONS = [\"Don't show\", 'Show all'];\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    label: {\n      type: String,\n      required: true\n    },\n    prefix: {\n      type: String,\n      required: true\n    }\n  },\n  emits: ['updated'],\n  data() {\n    return {\n      index: this.domainObject.configuration[`${this.prefix}Index`] % 2, //this is modulo since we previously had more options and index could have been > 1\n      activitiesOptions: ACTIVITIES_OPTIONS,\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    if (this.prefix === 'futureEvents') {\n      this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 3);\n    } else if (this.prefix === 'pastEvents') {\n      this.activitiesOptions = ACTIVITIES_OPTIONS.filter((item, index) => index !== 2);\n    } else if (this.prefix === 'currentEvents') {\n      this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 2);\n    }\n\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    updateForm(property) {\n      if (!this.isValid()) {\n        return;\n      }\n\n      const capitalized = property.charAt(0).toUpperCase() + property.substr(1);\n      this.$emit('updated', {\n        property: `${this.prefix}${capitalized}`,\n        value: this[property]\n      });\n    },\n    isValid() {\n      return this.index <= 1;\n    },\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timelist/inspector/FilteringComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <li class=\"c-inspect-properties__row\">\n    <div v-if=\"canEdit\" class=\"c-inspect-properties__hint span-all\">\n      Filter this view by comma-separated keywords. Filtering uses an 'OR' method.\n    </div>\n    <div class=\"c-inspect-properties__label\" aria-label=\"Activity Names\" title=\"Filter by keyword.\">\n      Activity Names\n    </div>\n    <div\n      v-if=\"canEdit\"\n      class=\"c-inspect-properties__value\"\n      :class=\"{ 'form-error': hasFilterError }\"\n    >\n      <textarea\n        v-model=\"filterValue\"\n        class=\"c-input--flex\"\n        type=\"text\"\n        @keydown.enter.exact.stop=\"forceBlur($event)\"\n        @keyup=\"updateNameFilter($event, 'filter')\"\n      ></textarea>\n    </div>\n    <div v-else class=\"c-inspect-properties__value\">\n      <template v-if=\"filterValue && filterValue.length > 0\">\n        {{ filterValue }}\n      </template>\n      <template v-else> No filters applied </template>\n    </div>\n  </li>\n  <li class=\"c-inspect-properties__row\">\n    <div\n      class=\"c-inspect-properties__label\"\n      aria-label=\"Meta-data Properties\"\n      title=\"Filter by keyword.\"\n    >\n      Meta-data Properties\n    </div>\n    <div\n      v-if=\"canEdit\"\n      class=\"c-inspect-properties__value\"\n      :class=\"{ 'form-error': hasMetadataFilterError }\"\n    >\n      <textarea\n        v-model=\"filterMetadataValue\"\n        class=\"c-input--flex\"\n        type=\"text\"\n        @keydown.enter.exact.stop=\"forceBlur($event)\"\n        @keyup=\"updateMetadataFilter($event, 'filterMetadata')\"\n      ></textarea>\n    </div>\n    <div v-else class=\"c-inspect-properties__value\">\n      <template v-if=\"filterMetadataValue && filterMetadataValue.length > 0\">\n        {{ filterMetadataValue }}\n      </template>\n      <template v-else> No filters applied </template>\n    </div>\n  </li>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct', 'domainObject'],\n  emits: ['updated'],\n  data() {\n    return {\n      isEditing: this.openmct.editor.isEditing(),\n      filterValue: this.domainObject.configuration.filter,\n      filterMetadataValue: this.domainObject.configuration.filterMetadata,\n      hasFilterError: false,\n      hasMetadataFilterError: false\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n      if (!this.isEditing) {\n        if (this.hasFilterError) {\n          this.filterValue = this.domainObject.configuration.filter;\n        }\n        if (this.hasMetadataFilterError) {\n          this.filterMetadataValue = this.domainObject.configuration.filterMetadata;\n        }\n        this.hasFilterError = false;\n        this.hasMetadataFilterError = false;\n      }\n    },\n    forceBlur(event) {\n      event.target.blur();\n    },\n    updateNameFilter(event, property) {\n      if (!this.isValid(this.filterValue)) {\n        this.hasFilterError = true;\n\n        return;\n      }\n      this.hasFilterError = false;\n\n      this.$emit('updated', {\n        property,\n        value: this.filterValue.replace(/,(\\s)*$/, '')\n      });\n    },\n    updateMetadataFilter(event, property) {\n      if (!this.isValid(this.filterMetadataValue)) {\n        this.hasMetadataFilterError = true;\n\n        return;\n      }\n      this.hasMetadataFilterError = false;\n\n      this.$emit('updated', {\n        property,\n        value: this.filterMetadataValue.replace(/,(\\s)*$/, '')\n      });\n    },\n    isValid(value) {\n      // Test for any word character, any whitespace character or comma\n      if (value === '') {\n        return true;\n      }\n\n      const regex = new RegExp(/^([a-zA-Z0-9_.\\-\\s,])+$/g);\n\n      return regex.test(value);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timelist/inspector/TimeListInspectorViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport { TIMELIST_TYPE } from '../constants.js';\nimport TimelistPropertiesView from './TimelistPropertiesView.vue';\n\nexport default function TimeListInspectorViewProvider(openmct) {\n  return {\n    key: 'timelist-inspector',\n    name: 'Config',\n    canView: function (selection) {\n      if (selection.length === 0 || selection[0].length === 0) {\n        return false;\n      }\n\n      let context = selection[0][0].context;\n\n      return context && context.item && context.item.type === TIMELIST_TYPE;\n    },\n    view: function (selection) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                TimelistPropertiesView: TimelistPropertiesView\n              },\n              provide: {\n                openmct,\n                domainObject: selection[0][0].context.item\n              },\n              template: '<timelist-properties-view></timelist-properties-view>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        priority: function () {\n          return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timelist/inspector/TimelistPropertiesView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-timelist-properties\">\n    <div class=\"c-inspect-properties\">\n      <ul class=\"c-inspect-properties__section\">\n        <div class=\"c-inspect-properties_header\" title=\"'Display options'\">Display Options</div>\n        <li class=\"c-inspect-properties__row\">\n          <div v-if=\"canEdit\" class=\"c-inspect-properties__hint span-all\">\n            These settings don't affect the view while editing, but will be applied after editing is\n            finished.\n          </div>\n          <div class=\"c-inspect-properties__label\" title=\"Display Style\">Display Style</div>\n          <div class=\"c-inspect-properties__value\">\n            <select\n              v-if=\"canEdit\"\n              v-model=\"isExpanded\"\n              aria-label=\"Display Style\"\n              @change=\"updateExpandedView\"\n            >\n              <option :key=\"'expanded-view-option-enabled'\" :value=\"true\">Expanded</option>\n              <option :key=\"'expanded-view-option-disabled'\" :value=\"false\">Compact</option>\n            </select>\n            <span v-else>{{ isExpanded ? 'Expanded' : 'Compact' }}</span>\n          </div>\n        </li>\n        <li class=\"c-inspect-properties__row\">\n          <div class=\"c-inspect-properties__label\" title=\"Sort order of the timelist.\">\n            Sort Order\n          </div>\n          <div class=\"c-inspect-properties__value\">\n            <select v-if=\"canEdit\" v-model=\"sortOrderIndex\" @change=\"updateSortOrder()\">\n              <option\n                v-for=\"(sortOrderOption, index) in sortOrderOptions\"\n                :key=\"index\"\n                :value=\"index\"\n              >\n                {{ sortOrderOption.label }}\n              </option>\n            </select>\n            <span v-else>{{ sortOrderOptions[sortOrderIndex].label }}</span>\n          </div>\n        </li>\n        <EventProperties\n          v-for=\"type in eventTypes\"\n          :key=\"type.prefix\"\n          :label=\"type.label\"\n          :prefix=\"type.prefix\"\n          @updated=\"eventPropertiesUpdated\"\n        />\n      </ul>\n    </div>\n    <div class=\"c-inspect-properties\">\n      <ul class=\"c-inspect-properties__section\">\n        <div class=\"c-inspect-properties_header\" title=\"'Filters'\">Filtering</div>\n        <Filtering @updated=\"eventPropertiesUpdated\" />\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { SORT_ORDER_OPTIONS } from '../constants.js';\nimport EventProperties from './EventProperties.vue';\nimport Filtering from './FilteringComponent.vue';\n\nconst EVENT_TYPES = [\n  {\n    label: 'Current Events',\n    prefix: 'currentEvents'\n  }\n];\n\nexport default {\n  components: {\n    Filtering,\n    EventProperties\n  },\n  inject: ['openmct', 'domainObject'],\n  data() {\n    return {\n      sortOrderIndex: this.domainObject.configuration.sortOrderIndex,\n      sortOrderOptions: SORT_ORDER_OPTIONS,\n      eventTypes: EVENT_TYPES,\n      isEditing: this.openmct.editor.isEditing(),\n      isExpanded: this.domainObject.configuration.isExpanded || false\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.setEditState);\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    },\n    updateSortOrder() {\n      this.updateProperty('sortOrderIndex', this.sortOrderIndex);\n    },\n    updateProperty(key, value) {\n      this.openmct.objects.mutate(this.domainObject, `configuration.${key}`, value);\n    },\n    eventPropertiesUpdated(data) {\n      const key = data.property;\n      const value = data.value;\n      this.updateProperty(key, value);\n    },\n    updateExpandedView() {\n      this.updateProperty('isExpanded', this.isExpanded);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timelist/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport TimelistCompositionPolicy from '@/plugins/timelist/TimelistCompositionPolicy';\n\nimport { TIMELIST_TYPE } from './constants.js';\nimport TimeListInspectorViewProvider from './inspector/TimeListInspectorViewProvider.js';\nimport TimelistViewProvider from './TimelistViewProvider.js';\n\nexport default function () {\n  return function install(openmct) {\n    openmct.types.addType(TIMELIST_TYPE, {\n      name: 'Time List',\n      key: TIMELIST_TYPE,\n      description:\n        'A configurable, time-ordered list view of activities for a compatible mission plan file.',\n      creatable: true,\n      cssClass: 'icon-timelist',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          sortOrderIndex: 0,\n          currentEventsIndex: 1,\n          filter: '',\n          filterMetadata: '',\n          isCompact: false\n        };\n        domainObject.composition = [];\n      }\n    });\n    openmct.objectViews.addProvider(new TimelistViewProvider(openmct));\n    openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));\n    openmct.composition.addPolicy(new TimelistCompositionPolicy(openmct).allow);\n  };\n}\n"
  },
  {
    "path": "src/plugins/timelist/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport { FIXED_MODE_KEY } from '../../api/time/constants.js';\nimport { TIMELIST_TYPE } from './constants.js';\nimport TimelistPlugin from './plugin.js';\n\nconst TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';\n\nconst LIST_ITEM_CLASS = '.js-table__body .js-list-item';\nconst LIST_ITEM_VALUE_CLASS = '.js-list-item__value';\nconst LIST_ITEM_BODY_CLASS = '.js-table__body th';\n\ndescribe('the plugin', function () {\n  let timelistDefinition;\n  let element;\n  let child;\n  let openmct;\n  let appHolder;\n  let originalRouterPath;\n  let mockComposition;\n  let now = Date.now();\n  let twoHoursPast = now - 1000 * 60 * 60 * 2;\n  let oneHourPast = now - 1000 * 60 * 60;\n  let twoHoursFuture = now + 1000 * 60 * 60 * 2;\n  let threeHoursFuture = now + 1000 * 60 * 60 * 3;\n  let planObject = {\n    identifier: {\n      key: 'test-plan-object',\n      namespace: ''\n    },\n    type: 'plan',\n    id: 'test-plan-object',\n    selectFile: {\n      body: JSON.stringify({\n        'TEST-GROUP': [\n          {\n            name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n            start: twoHoursPast,\n            end: oneHourPast,\n            type: 'TEST-GROUP',\n            color: 'fuchsia',\n            textColor: 'black'\n          },\n          {\n            name: 'Sed ut perspiciatis',\n            start: now,\n            end: twoHoursFuture,\n            type: 'TEST-GROUP',\n            color: 'fuchsia',\n            textColor: 'black',\n            properties: {\n              location: 'garden'\n            }\n          },\n          {\n            name: 'Sed ut perspiciatis two',\n            start: now,\n            end: threeHoursFuture,\n            type: 'TEST-GROUP',\n            color: 'fuchsia',\n            textColor: 'black',\n            properties: {\n              location: 'hallway'\n            }\n          }\n        ]\n      })\n    }\n  };\n\n  beforeEach((done) => {\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n\n    openmct = createOpenMct({\n      timeSystemKey: 'utc',\n      bounds: {\n        start: twoHoursFuture,\n        end: threeHoursFuture\n      }\n    });\n    openmct.time.setMode(FIXED_MODE_KEY, {\n      start: twoHoursFuture,\n      end: threeHoursFuture\n    });\n    openmct.install(new TimelistPlugin());\n\n    timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition;\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    originalRouterPath = openmct.router.path;\n\n    mockComposition = new EventEmitter();\n    // eslint-disable-next-line require-await\n    mockComposition.load = async () => {\n      return [planObject];\n    };\n\n    spyOn(openmct.composition, 'get').and.returnValue(mockComposition);\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    openmct.router.path = originalRouterPath;\n\n    return resetApplicationState(openmct);\n  });\n\n  let mockTimelistObject = {\n    name: 'Timelist',\n    key: TIMELIST_TYPE,\n    creatable: true\n  };\n\n  it('defines a timelist object type with the correct key', () => {\n    expect(timelistDefinition.key).toEqual(mockTimelistObject.key);\n  });\n\n  it('is creatable', () => {\n    expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable);\n  });\n\n  describe('the timelist view', () => {\n    it('provides a timelist view', () => {\n      const testViewObject = {\n        id: 'test-object',\n        type: TIMELIST_TYPE\n      };\n      openmct.router.path = [testViewObject];\n\n      const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);\n      let timelistView = applicableViews.find(\n        (viewProvider) => viewProvider.key === 'timelist.view'\n      );\n      expect(timelistView).toBeDefined();\n    });\n  });\n\n  describe('the timelist view displays activities', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 0,\n          futureEventsIndex: 1,\n          futureEventsDurationIndex: 0,\n          futureEventsDuration: 0,\n          currentEventsIndex: 1,\n          currentEventsDurationIndex: 0,\n          currentEventsDuration: 0,\n          pastEventsIndex: 1,\n          pastEventsDurationIndex: 0,\n          pastEventsDuration: 0,\n          filter: ''\n        },\n        selectFile: {\n          body: JSON.stringify({\n            'TEST-GROUP': [\n              {\n                name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n                start: twoHoursPast,\n                end: oneHourPast,\n                type: 'TEST-GROUP',\n                color: 'fuchsia',\n                textColor: 'black'\n              },\n              {\n                name: 'Sed ut perspiciatis',\n                start: now,\n                end: twoHoursFuture,\n                type: 'TEST-GROUP',\n                color: 'fuchsia',\n                textColor: 'black'\n              }\n            ]\n          })\n        }\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, []);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('displays the activities', () => {\n      const items = element.querySelectorAll(LIST_ITEM_CLASS);\n      expect(items.length).toEqual(1);\n    });\n\n    it('displays the activity headers', () => {\n      const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS);\n      expect(headers.length).toEqual(5);\n    });\n\n    it('displays activity details', (done) => {\n      const timeFormat = openmct.time.timeSystem().timeFormat;\n      const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n      nextTick(() => {\n        const itemEls = element.querySelectorAll(LIST_ITEM_CLASS);\n        const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);\n        expect(itemValues.length).toEqual(5);\n        expect(itemValues[4].innerHTML.trim()).toEqual('Sed ut perspiciatis');\n        expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));\n        expect(itemValues[1].innerHTML.trim()).toEqual(\n          timeFormatter.format(twoHoursFuture, TIME_FORMAT)\n        );\n\n        done();\n      });\n    });\n  });\n\n  describe('the timelist composition', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 0,\n          futureEventsIndex: 1,\n          futureEventsDurationIndex: 0,\n          futureEventsDuration: 0,\n          currentEventsIndex: 1,\n          currentEventsDurationIndex: 0,\n          currentEventsDuration: 0,\n          pastEventsIndex: 1,\n          pastEventsDurationIndex: 0,\n          pastEventsDuration: 0,\n          filter: ''\n        },\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('loads the plan from composition', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n      });\n    });\n  });\n\n  describe('filters by name', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 2,\n          futureEventsIndex: 1,\n          futureEventsDurationIndex: 0,\n          futureEventsDuration: 0,\n          currentEventsIndex: 1,\n          currentEventsDurationIndex: 0,\n          currentEventsDuration: 0,\n          pastEventsIndex: 1,\n          pastEventsDurationIndex: 0,\n          pastEventsDuration: 0,\n          filter: 'perspiciatis'\n        },\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('activities', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n      });\n    });\n\n    it('activities and sorts them correctly', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const timeFormat = openmct.time.timeSystem().timeFormat;\n        const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n\n        const itemValues = items[1].querySelectorAll(LIST_ITEM_VALUE_CLASS);\n        expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));\n        expect(itemValues[1].innerHTML.trim()).toEqual(\n          timeFormatter.format(threeHoursFuture, TIME_FORMAT)\n        );\n        expect(itemValues[4].innerHTML.trim()).toEqual('Sed ut perspiciatis two');\n      });\n    });\n  });\n\n  describe('filters by metadata', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 2,\n          futureEventsIndex: 1,\n          futureEventsDurationIndex: 0,\n          futureEventsDuration: 0,\n          currentEventsIndex: 1,\n          currentEventsDurationIndex: 0,\n          currentEventsDuration: 0,\n          pastEventsIndex: 1,\n          pastEventsDurationIndex: 0,\n          pastEventsDuration: 0,\n          filter: '',\n          filterMetadata: 'hallway,garden'\n        },\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('activities and sorts them correctly', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const timeFormat = openmct.time.timeSystem().timeFormat;\n        const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n\n        const itemValues = items[1].querySelectorAll(LIST_ITEM_VALUE_CLASS);\n        expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));\n        expect(itemValues[1].innerHTML.trim()).toEqual(\n          timeFormatter.format(threeHoursFuture, TIME_FORMAT)\n        );\n        expect(itemValues[4].innerHTML.trim()).toEqual('Sed ut perspiciatis two');\n      });\n    });\n  });\n\n  describe('filters by name and metadata', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 2,\n          currentEventsIndex: 1,\n          filter: 'two',\n          filterMetadata: 'garden'\n        },\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('activities and sorts them correctly', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const timeFormat = openmct.time.timeSystem().timeFormat;\n        const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;\n\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n\n        const itemValues = items[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);\n        expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));\n        expect(itemValues[1].innerHTML.trim()).toEqual(\n          timeFormatter.format(twoHoursFuture, TIME_FORMAT)\n        );\n      });\n    });\n  });\n\n  describe('time filtering - past', () => {\n    let timelistDomainObject;\n    let timelistView;\n\n    beforeEach(() => {\n      timelistDomainObject = {\n        identifier: {\n          key: 'test-object',\n          namespace: ''\n        },\n        type: TIMELIST_TYPE,\n        id: 'test-object',\n        configuration: {\n          sortOrderIndex: 0,\n          futureEventsIndex: 1,\n          futureEventsDurationIndex: 0,\n          futureEventsDuration: 0,\n          currentEventsIndex: 1,\n          currentEventsDurationIndex: 0,\n          currentEventsDuration: 0,\n          pastEventsIndex: 0,\n          pastEventsDurationIndex: 0,\n          pastEventsDuration: 0,\n          filter: ''\n        },\n        composition: [\n          {\n            identifier: {\n              key: 'test-plan-object',\n              namespace: ''\n            }\n          }\n        ]\n      };\n\n      openmct.router.path = [timelistDomainObject];\n\n      const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);\n      timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');\n      let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);\n      view.show(child, true);\n\n      return nextTick();\n    });\n\n    it('hides past events', () => {\n      mockComposition.emit('add', planObject);\n\n      return nextTick(() => {\n        const items = element.querySelectorAll(LIST_ITEM_CLASS);\n        expect(items.length).toEqual(2);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/timelist/svg-progress.js",
    "content": "const PI = Math.PI; // Use the built-in constant directly\nconst DEGREES_TO_RADIANS = PI / 180; // Calculate the conversion factor\n\nimport { arc } from 'd3-shape';\n\nconst SVG_VB_SIZE = 100;\nconst UPDATE_RATE_MS = 1000; // 1 Hz\n\nfunction progToDegrees(progVal) {\n  return (progVal / 100) * 360;\n}\n\nfunction renderProgress(progressPercent, element) {\n  let startAngleInDegrees = 0;\n  let endAngleInDegrees = progToDegrees(progressPercent);\n\n  // Convert angles to radians for calculations\n  const startAngleInRadians = startAngleInDegrees * DEGREES_TO_RADIANS;\n  const endAngleInRadians = endAngleInDegrees * DEGREES_TO_RADIANS;\n\n  // d3's arc API does the work for us\n  const progressArc = arc();\n  progressArc.innerRadius(0);\n  progressArc.outerRadius(SVG_VB_SIZE / 2);\n  progressArc.startAngle(startAngleInRadians);\n  progressArc.endAngle(endAngleInRadians);\n  element.setAttribute('d', progressArc());\n}\n\nexport function updateProgress(start, end, timestamp, element) {\n  const duration = end - start;\n  const update_per_cycle = 100 / (duration / UPDATE_RATE_MS);\n  let progressPercent = 0;\n  if (timestamp > start) {\n    // Now is after activity start datetime\n    if (timestamp > end) {\n      progressPercent = 100;\n    } else {\n      progressPercent = (1 - (end - timestamp) / duration) * 100;\n    }\n  }\n  if (progressPercent < 100 && progressPercent > 0) {\n    // If the remaining percent is less than update_per_cycle, round up to 100%.\n    // Otherwise, increment by update_per_cycle.\n    progressPercent =\n      100 - progressPercent < update_per_cycle ? 100 : (progressPercent += update_per_cycle);\n  }\n  renderProgress(progressPercent, element);\n}\n"
  },
  {
    "path": "src/plugins/timelist/timelist.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n.c-timelist {\n  & .nowMarker.hasCurrent {\n    height: 2px;\n    position: absolute;\n    z-index: 10;\n    background: cyan;\n    width: 100%;\n  }\n\n  .c-list-item {\n    /* Compact Time Lists; is a <tr> element */\n\n    @mixin sSelected($bgColor, $fgColor) {\n      &[s-selected] {\n        background: $bgColor !important;\n        border: 1px solid $colorSelectedFg !important;\n        color: $fgColor !important;\n      }\n    }\n\n    td {\n      $p: $interiorMarginSm;\n      padding-top: $p;\n      padding-bottom: $p;\n    }\n\n    &.--is-past {\n      @include sSelected(transparent, $colorPastFgEm);\n    }\n\n    &.--is-current {\n      @include sSelected($colorCurrentBg, $colorCurrentFgEm);\n      background-color: $colorCurrentBg;\n      border-top: 1px solid $colorCurrentBorder !important;\n      color: $colorCurrentFgEm;\n    }\n\n    &.--is-future {\n      @include sSelected($colorFutureBg, $colorFutureFgEm);\n      background-color: $colorFutureBg;\n      border-top-color: $colorFutureBorder !important;\n      color: $colorFutureFgEm;\n    }\n\n    &.--is-in-progress {\n      @include sSelected($colorInProgressBg, $colorInProgressFgEm);\n      background-color: $colorInProgressBg;\n    }\n\n    &__value {\n      &.--duration {\n        width: 5%;\n      }\n    }\n  }\n}\n\n/**************************************************** LARGE TIME LIST */\n@mixin showTliGraphic {\n  .c-tli__graphic {\n    display: block;\n    &:before {\n      @content;\n    }\n  }\n}\n\n.c-timelist--large {\n  $textSm: 0.8em;\n  $textLg: 1.3em;\n\n  margin-right: $interiorMargin; // Fend off from scrollbar\n  padding: 1px 1px; // Provide room for selected Activities border\n\n  > * + * {\n    margin-top: $interiorMarginSm;\n  }\n\n  .c-tli {\n    $baseBg: $colorPastBg;\n    $baseFg: $colorPastFg;\n    $baseFgEm: $colorPastFgEm;\n    $opSubtle: $opacitySubtle;\n\n    border-radius: $basicCr;\n    border: 1px solid transparent;\n    display: flex;\n    gap: 1px;\n    overflow: hidden;\n\n    &[s-selected] {\n      box-shadow: rgba($colorSelectedFg, 0.8) 0 0 0 1px;\n    }\n\n    .--subtle {\n      opacity: $opSubtle;\n    }\n\n    &__activity-color {\n      flex: 0 0 auto;\n      width: 7px;\n    }\n\n    &__contents {\n      color: $baseFg;\n      display: flex;\n      flex-direction: column;\n      flex: 1 1 auto;\n      gap: $interiorMargin;\n      overflow: hidden;\n      padding: $interiorMargin;\n\n      &.--is-past {\n        background-color: $colorPastBg;\n        color: $colorPastFg;\n        font-style: italic;\n\n        .--subtle {\n          opacity: $opSubtle * 1.5;\n        }\n\n        > * {\n          opacity: $opSubtle * 0.9;\n        }\n\n        &.--is-in-progress {\n          > * {\n            opacity: $opSubtle;\n          }\n        }\n      }\n\n      &.--is-current {\n        background-color: $colorCurrentBg;\n        color: $colorCurrentFg;\n      }\n\n      &.--is-future {\n        background-color: $colorFutureBg;\n        color: $colorFutureFg;\n      }\n\n      &.--is-in-progress {\n        background-color: $colorInProgressBg;\n\n        @include showTliGraphic {\n          @include gearSpinner($color: $colorInProgressFg);\n        }\n      }\n\n      &.--is-completed {\n        @include showTliGraphic {\n          @include bgCheckMark($color: $colorActivityStatusGreen);\n        }\n      }\n\n      &.--is-aborted {\n        @include showTliGraphic {\n          @include bgCircleSlash($color: $colorActivityStatusOrange);\n        }\n      }\n\n      &.--is-skipped {\n        @include showTliGraphic {\n          @include bgSkip($color: $colorBodyFg);\n        }\n      }\n    }\n\n    /************************ TITLE AND STATUS + GRAPHIC ICON */\n    &__title-and-status {\n      align-items: center;\n      display: flex;\n      flex-wrap: wrap;\n      gap: $interiorMargin;\n      justify-content: space-between;\n      overflow: hidden;\n    }\n\n    &__title {\n      @include ellipsize();\n      font-size: $textLg;\n      flex: 1 1 auto;\n      min-width: 100px;\n    }\n\n    &__status-and-icon-graphic {\n      align-items: center;\n      display: flex;\n      flex: 0 0 auto;\n      gap: $interiorMargin;\n      text-align: right;\n      text-transform: uppercase;\n    }\n\n    .c-tli__graphic {\n      $d: 20px;\n      width: $d;\n      height: $d;\n      display: none;\n\n      &:before {\n        content: '';\n        display: block;\n        height: 100%;\n        width: 100%;\n      }\n    }\n\n    /************************ COUNT +/- (TIME HERO) */\n    &__time-hero {\n      display: flex;\n      align-items: baseline;\n      gap: $interiorMargin;\n    }\n\n    &__time-hero-time {\n      display: flex;\n      align-items: center;\n      font-size: $textLg;\n      white-space: nowrap;\n\n      &:before {\n        display: block;\n        font-family: symbolsfont;\n        font-size: 0.7em;\n        margin-right: 3px;\n      }\n\n      &.--is-countdown {\n        &:before {\n          content: $glyph-icon-minus;\n        }\n      }\n\n      &.--is-countup {\n        &:before {\n          content: $glyph-icon-plus;\n        }\n      }\n    }\n\n    &__time-hero-context {\n      text-transform: uppercase;\n      white-space: nowrap;\n    }\n\n    /************************ BOUNDS AND DURATION */\n    &__bounds-and-duration {\n      align-items: center;\n      display: flex;\n      flex-wrap: wrap;\n      gap: $interiorMarginSm;\n\n      > * {\n        white-space: nowrap;\n      }\n\n\n      &.--has-duration {\n        .c-tli__start-time {\n          display: flex;\n          align-items: start;\n\n          &:after {\n            content: $glyph-icon-play;\n            font-family: symbolsfont;\n            font-size: 0.6em;\n            display: block;\n            margin-left: $interiorMargin;\n            margin-top: 4px;\n            opacity: $opSubtle;\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/timer/TimerViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport Timer from './components/TimerComponent.vue';\n\nexport default function TimerViewProvider(openmct) {\n  return {\n    key: 'timer.view',\n    name: 'Timer',\n    cssClass: 'icon-timer',\n    canView(domainObject) {\n      return domainObject.type === 'timer';\n    },\n\n    view: function (domainObject, objectPath) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                Timer\n              },\n              provide: {\n                openmct,\n                currentView: this\n              },\n              data() {\n                return {\n                  domainObject,\n                  objectPath\n                };\n              },\n              template: '<timer :domain-object=\"domainObject\" :object-path=\"objectPath\" />'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/timer/actions/PauseTimerAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst PAUSE_TIMER_ACTION_KEY = 'timer.pause';\n\nclass PauseTimerAction {\n  constructor(openmct) {\n    this.name = 'Pause';\n    this.key = PAUSE_TIMER_ACTION_KEY;\n    this.description = 'Pause the currently displayed timer';\n    this.group = 'view';\n    this.cssClass = 'icon-pause';\n    this.priority = 3;\n\n    this.openmct = openmct;\n  }\n  invoke(objectPath) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return new Error('Unable to run pause timer action. No domainObject provided.');\n    }\n\n    const newConfiguration = { ...domainObject.configuration };\n    newConfiguration.timerState = 'paused';\n    newConfiguration.pausedTime = new Date(this.openmct.time.now());\n\n    this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);\n  }\n  appliesTo(objectPath, view = {}) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return;\n    }\n\n    // Use object configuration timerState for viewless context menus,\n    // otherwise manually show/hide based on the view's timerState\n    const viewKey = view.key;\n    const { timerState } = domainObject.configuration;\n\n    return viewKey\n      ? domainObject.type === 'timer'\n      : domainObject.type === 'timer' && timerState === 'started';\n  }\n}\n\nexport { PAUSE_TIMER_ACTION_KEY };\n\nexport default PauseTimerAction;\n"
  },
  {
    "path": "src/plugins/timer/actions/RestartTimerAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst RESTART_TIMER_ACTION_KEY = 'timer.restart';\n\nclass RestartTimerAction {\n  constructor(openmct) {\n    this.name = 'Restart at 0';\n    this.key = RESTART_TIMER_ACTION_KEY;\n    this.description = 'Restart the currently displayed timer';\n    this.group = 'view';\n    this.cssClass = 'icon-refresh';\n    this.priority = 2;\n\n    this.openmct = openmct;\n  }\n  invoke(objectPath) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return new Error('Unable to run restart timer action. No domainObject provided.');\n    }\n\n    const newConfiguration = { ...domainObject.configuration };\n    newConfiguration.timerState = 'started';\n    newConfiguration.timestamp = new Date(this.openmct.time.now());\n    newConfiguration.pausedTime = undefined;\n\n    this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);\n  }\n  appliesTo(objectPath, view = {}) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return;\n    }\n\n    // Use object configuration timerState for viewless context menus,\n    // otherwise manually show/hide based on the view's timerState\n    const viewKey = view.key;\n    const { timerState } = domainObject.configuration;\n\n    return viewKey\n      ? domainObject.type === 'timer'\n      : domainObject.type === 'timer' && timerState !== 'stopped';\n  }\n}\n\nexport { RESTART_TIMER_ACTION_KEY };\n\nexport default RestartTimerAction;\n"
  },
  {
    "path": "src/plugins/timer/actions/StartTimerAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst START_TIMER_ACTION_KEY = 'timer.start';\n\nclass StartTimerAction {\n  constructor(openmct) {\n    this.name = 'Start';\n    this.key = START_TIMER_ACTION_KEY;\n    this.description = 'Start the currently displayed timer';\n    this.group = 'view';\n    this.cssClass = 'icon-play';\n    this.priority = 3;\n\n    this.openmct = openmct;\n  }\n\n  invoke(objectPath) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return new Error('Unable to run start timer action. No domainObject provided.');\n    }\n\n    let { pausedTime, timestamp } = domainObject.configuration;\n    const newConfiguration = { ...domainObject.configuration };\n\n    const now = new Date(this.openmct.time.now());\n\n    if (pausedTime) {\n      const timeShift = now - new Date(pausedTime);\n      const shiftedTime = new Date(new Date(timestamp).getTime() + timeShift);\n      newConfiguration.timestamp = shiftedTime;\n    } else if (!timestamp) {\n      newConfiguration.timestamp = now;\n    }\n\n    newConfiguration.timerState = 'started';\n    newConfiguration.pausedTime = undefined;\n    this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return;\n    }\n\n    // Use object configuration timerState for viewless context menus,\n    // otherwise manually show/hide based on the view's timerState\n    const viewKey = view.key;\n    const { timerState } = domainObject.configuration;\n\n    return viewKey\n      ? domainObject.type === 'timer'\n      : domainObject.type === 'timer' && timerState !== 'started';\n  }\n}\n\nexport { START_TIMER_ACTION_KEY };\n\nexport default StartTimerAction;\n"
  },
  {
    "path": "src/plugins/timer/actions/StopTimerAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst STOP_TIMER_ACTION_KEY = 'timer.stop';\n\nclass StopTimerAction {\n  constructor(openmct) {\n    this.name = 'Stop';\n    this.key = STOP_TIMER_ACTION_KEY;\n    this.description = 'Stop the currently displayed timer';\n    this.group = 'view';\n    this.cssClass = 'icon-box-round-corners';\n    this.priority = 1;\n\n    this.openmct = openmct;\n  }\n  invoke(objectPath) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return new Error('Unable to run stop timer action. No domainObject provided.');\n    }\n\n    const newConfiguration = { ...domainObject.configuration };\n    newConfiguration.timerState = 'stopped';\n    newConfiguration.timestamp = undefined;\n    newConfiguration.pausedTime = undefined;\n\n    this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);\n  }\n  appliesTo(objectPath, view = {}) {\n    const domainObject = objectPath[0];\n    if (!domainObject || !domainObject.configuration) {\n      return;\n    }\n\n    // Use object configuration timerState for viewless context menus,\n    // otherwise manually show/hide based on the view's timerState\n    const viewKey = view.key;\n    const { timerState } = domainObject.configuration;\n\n    return viewKey\n      ? domainObject.type === 'timer'\n      : domainObject.type === 'timer' && timerState !== 'stopped';\n  }\n}\n\nexport { STOP_TIMER_ACTION_KEY };\n\nexport default StopTimerAction;\n"
  },
  {
    "path": "src/plugins/timer/components/TimerComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-timer u-style-receiver js-style-receiver\" :class=\"[`is-${timerState}`]\">\n    <div class=\"c-timer__controls\">\n      <button\n        title=\"Reset\"\n        aria-label=\"Reset\"\n        class=\"c-timer__ctrl-reset c-icon-button c-icon-button--major icon-reset\"\n        :class=\"[{ hide: timerState === 'stopped' }]\"\n        @click=\"restartTimer\"\n      ></button>\n      <button\n        :title=\"timerStateButtonText\"\n        :aria-label=\"timerStateButtonText\"\n        class=\"c-timer__ctrl-pause-play c-icon-button c-icon-button--major\"\n        :class=\"[timerStateButtonIcon]\"\n        @click=\"toggleStateButton\"\n      ></button>\n    </div>\n    <div class=\"c-timer__direction\" :class=\"[{ hide: !timerSign }, `icon-${timerSign}`]\"></div>\n    <div class=\"c-timer__value\">{{ timerState === 'stopped' ? '--:--:--' : timeTextValue }}</div>\n  </div>\n</template>\n\n<script>\nimport momentDurationFormatSetup from 'moment-duration-format';\nimport moment from 'moment-timezone';\nimport raf from 'utils/raf';\n\nimport throttle from '../../../utils/throttle.js';\nconst refreshRateSeconds = 2;\n\nmomentDurationFormatSetup(moment);\n\nexport default {\n  inject: ['openmct', 'currentView'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  data() {\n    return {\n      configuration: this.domainObject.configuration,\n      lastTimestamp: null\n    };\n  },\n  computed: {\n    timeDelta() {\n      if (this.configuration.pausedTime) {\n        return Date.parse(this.configuration.pausedTime) - this.startTimeMs;\n      } else {\n        return this.lastTimestamp - this.startTimeMs;\n      }\n    },\n    startTimeMs() {\n      return Date.parse(this.configuration.timestamp);\n    },\n    timeTextValue() {\n      const toWholeSeconds = Math.abs(Math.floor(this.timeDelta / 1000) * 1000);\n\n      return moment.duration(toWholeSeconds, 'ms').format(this.format, { trim: false });\n    },\n    timerState() {\n      let timerState = 'started';\n      if (this.configuration && this.configuration.timerState) {\n        timerState = this.configuration.timerState;\n      }\n\n      return timerState;\n    },\n    timerStateButtonText() {\n      let buttonText = 'Pause';\n      if (['paused', 'stopped'].includes(this.timerState)) {\n        buttonText = 'Start';\n      }\n\n      return buttonText;\n    },\n    timerStateButtonIcon() {\n      let buttonIcon = 'icon-pause';\n      if (['paused', 'stopped'].includes(this.timerState)) {\n        buttonIcon = 'icon-play';\n      }\n\n      return buttonIcon;\n    },\n    timerFormat() {\n      let timerFormat = 'long';\n      if (this.configuration && this.configuration.timerFormat) {\n        timerFormat = this.configuration.timerFormat;\n      }\n\n      return timerFormat;\n    },\n    format() {\n      let format;\n      if (this.timerFormat === 'long') {\n        format = 'd[D] HH:mm:ss';\n      }\n\n      if (this.timerFormat === 'short') {\n        format = 'HH:mm:ss';\n      }\n\n      return format;\n    },\n    timerType() {\n      let timerType = null;\n      if (isNaN(this.timeDelta)) {\n        return timerType;\n      }\n\n      if (this.timeDelta < 0) {\n        timerType = 'countDown';\n      } else if (this.timeDelta >= 1000) {\n        timerType = 'countUp';\n      }\n\n      return timerType;\n    },\n    timerSign() {\n      let timerSign = null;\n      if (this.timerType === 'countUp') {\n        timerSign = 'plus';\n      } else if (this.timerType === 'countDown') {\n        timerSign = 'minus';\n      }\n\n      return timerSign;\n    }\n  },\n  watch: {\n    timerState() {\n      if (!this.viewActionsCollection) {\n        return;\n      }\n\n      this.showOrHideAvailableActions();\n    }\n  },\n  mounted() {\n    this.unobserve = this.openmct.objects.observe(this.domainObject, '*', (domainObject) => {\n      this.configuration = domainObject.configuration;\n    });\n    this.$nextTick(() => {\n      if (!this.configuration?.timerState) {\n        const timerAction = !this.timeDelta ? 'stop' : 'start';\n        this.triggerAction(`timer.${timerAction}`);\n      }\n\n      this.handleTick = raf(this.handleTick);\n      this.refreshTimerObject = throttle(this.refreshTimerObject, refreshRateSeconds * 1000);\n      this.openmct.time.on('tick', this.handleTick);\n\n      this.viewActionsCollection = this.openmct.actions.getActionsCollection(\n        this.objectPath,\n        this.currentView\n      );\n      this.showOrHideAvailableActions();\n    });\n  },\n  beforeUnmount() {\n    if (this.unobserve) {\n      this.unobserve();\n    }\n    this.openmct.time.off('tick', this.handleTick);\n  },\n  methods: {\n    handleTick() {\n      this.lastTimestamp = new Date(this.openmct.time.now());\n      this.refreshTimerObject();\n    },\n    refreshTimerObject() {\n      this.openmct.objects.refresh(this.domainObject);\n    },\n    restartTimer() {\n      this.triggerAction('timer.restart');\n    },\n    toggleStateButton() {\n      if (this.timerState === 'started') {\n        this.triggerAction('timer.pause');\n      } else if (['paused', 'stopped'].includes(this.timerState)) {\n        this.triggerAction('timer.start');\n      }\n    },\n    triggerAction(actionKey) {\n      const action = this.openmct.actions.getAction(actionKey);\n      if (action) {\n        action.invoke(this.objectPath, this.currentView);\n      }\n    },\n    showOrHideAvailableActions() {\n      switch (this.timerState) {\n        case 'started':\n          this.viewActionsCollection.hide(['timer.start']);\n          this.viewActionsCollection.show(['timer.stop', 'timer.pause', 'timer.restart']);\n          break;\n        case 'paused':\n          this.viewActionsCollection.hide(['timer.pause']);\n          this.viewActionsCollection.show(['timer.stop', 'timer.start', 'timer.restart']);\n          break;\n        case 'stopped':\n          this.viewActionsCollection.hide(['timer.stop', 'timer.pause', 'timer.restart']);\n          this.viewActionsCollection.show(['timer.start']);\n          break;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/timer/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport PauseTimerAction from './actions/PauseTimerAction.js';\nimport RestartTimerAction from './actions/RestartTimerAction.js';\nimport StartTimerAction from './actions/StartTimerAction.js';\nimport StopTimerAction from './actions/StopTimerAction.js';\nimport TimerViewProvider from './TimerViewProvider.js';\n\n/** @type {OpenMCTPlugin} */\nexport default function TimerPlugin() {\n  return function install(openmct) {\n    openmct.types.addType('timer', {\n      name: 'Timer',\n      description:\n        'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.',\n      creatable: true,\n      cssClass: 'icon-timer',\n      initialize: function (domainObject) {\n        domainObject.configuration = {\n          timerFormat: 'long',\n          timestamp: undefined,\n          timezone: 'UTC',\n          timerState: undefined,\n          pausedTime: undefined\n        };\n      },\n      form: [\n        {\n          key: 'timestamp',\n          control: 'datetime',\n          name: 'Target',\n          property: ['configuration', 'timestamp']\n        },\n        {\n          key: 'timerFormat',\n          name: 'Display Format',\n          control: 'select',\n          options: [\n            {\n              value: 'long',\n              name: 'DDD hh:mm:ss'\n            },\n            {\n              value: 'short',\n              name: 'hh:mm:ss'\n            }\n          ],\n          property: ['configuration', 'timerFormat']\n        }\n      ]\n    });\n    openmct.objectViews.addProvider(new TimerViewProvider(openmct));\n\n    openmct.actions.register(new PauseTimerAction(openmct));\n    openmct.actions.register(new RestartTimerAction(openmct));\n    openmct.actions.register(new StartTimerAction(openmct));\n    openmct.actions.register(new StopTimerAction(openmct));\n\n    openmct.objects.addGetInterceptor({\n      appliesTo: (identifier, domainObject) => {\n        return domainObject && domainObject.type === 'timer';\n      },\n      invoke: (identifier, domainObject) => {\n        if (domainObject.configuration) {\n          return domainObject;\n        }\n\n        const configuration = {};\n\n        if (domainObject.timerFormat) {\n          configuration.timerFormat = domainObject.timerFormat;\n        }\n\n        if (domainObject.timestamp) {\n          configuration.timestamp = domainObject.timestamp;\n        }\n\n        if (domainObject.timerState) {\n          configuration.timerState = domainObject.timerState;\n        }\n\n        if (domainObject.pausedTime) {\n          configuration.pausedTime = domainObject.pausedTime;\n        }\n\n        openmct.objects.mutate(domainObject, 'configuration', configuration);\n\n        return domainObject;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/timer/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport timerPlugin from './plugin.js';\n\nxdescribe('Timer plugin:', () => {\n  let openmct;\n  let timerDefinition;\n  let element;\n  let child;\n  let appHolder;\n\n  let timerDomainObject;\n\n  function setupTimer() {\n    return new Promise((resolve, reject) => {\n      timerDomainObject = {\n        identifier: {\n          key: 'timer',\n          namespace: 'test-namespace'\n        },\n        type: 'timer'\n      };\n\n      appHolder = document.createElement('div');\n      appHolder.style.width = '640px';\n      appHolder.style.height = '480px';\n      document.body.appendChild(appHolder);\n\n      openmct = createOpenMct();\n\n      element = document.createElement('div');\n      child = document.createElement('div');\n      element.appendChild(child);\n\n      openmct.install(timerPlugin());\n\n      timerDefinition = openmct.types.get('timer').definition;\n      timerDefinition.initialize(timerDomainObject);\n\n      spyOn(openmct.objects, 'supportsMutation').and.returnValue(true);\n\n      openmct.on('start', resolve);\n      openmct.start(appHolder);\n    });\n  }\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe(\"should still work if it's in the old format\", () => {\n    let timerViewProvider;\n    let timerView;\n    let timerViewObject;\n    let mutableTimerObject;\n    let timerObjectPath;\n    const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM\n\n    beforeEach(async () => {\n      await setupTimer();\n\n      timerViewObject = {\n        identifier: {\n          key: 'timer',\n          namespace: 'test-namespace'\n        },\n        type: 'timer',\n        id: 'test-object',\n        name: 'Timer',\n        timerFormat: 'short',\n        timestamp: relativeTimestamp,\n        timerState: 'paused',\n        pausedTime: relativeTimestamp\n      };\n\n      const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]);\n      timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view');\n\n      spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject));\n      spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));\n\n      mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier);\n\n      timerObjectPath = [mutableTimerObject];\n      timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath);\n      timerView.show(child);\n\n      await nextTick();\n    });\n\n    afterEach(() => {\n      timerView.destroy();\n    });\n\n    it('should migrate old object properties to the configuration section', () => {\n      openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject);\n      expect(timerViewObject.configuration.timerFormat).toBe('short');\n      expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp);\n      expect(timerViewObject.configuration.timerState).toBe('paused');\n      expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp);\n    });\n  });\n\n  describe('Timer view:', () => {\n    let timerViewProvider;\n    let timerView;\n    let timerViewObject;\n    let mutableTimerObject;\n    let timerObjectPath;\n\n    beforeEach(async () => {\n      await setupTimer();\n\n      spyOnBuiltins(['requestAnimationFrame']);\n      window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500));\n      const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM\n      const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM\n\n      jasmine.clock().install();\n      const baseTime = new Date(baseTimestamp);\n      jasmine.clock().mockDate(baseTime);\n\n      timerViewObject = {\n        ...timerDomainObject,\n        id: 'test-object',\n        name: 'Timer',\n        configuration: {\n          timerFormat: 'long',\n          timestamp: relativeTimestamp,\n          timezone: 'UTC',\n          timerState: undefined,\n          pausedTime: undefined\n        }\n      };\n\n      spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject));\n      spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));\n\n      const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]);\n      timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view');\n\n      mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier);\n\n      timerObjectPath = [mutableTimerObject];\n      timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath);\n      timerView.show(child);\n\n      await nextTick();\n    });\n\n    afterEach(() => {\n      jasmine.clock().uninstall();\n      timerView.destroy();\n      openmct.objects.destroyMutable(mutableTimerObject);\n      if (appHolder) {\n        appHolder.remove();\n      }\n    });\n\n    it('has name as Timer', () => {\n      expect(timerDefinition.name).toEqual('Timer');\n    });\n\n    it('is creatable', () => {\n      expect(timerDefinition.creatable).toEqual(true);\n    });\n\n    it('provides timer view', () => {\n      expect(timerViewProvider).toBeDefined();\n    });\n\n    it('renders timer element', () => {\n      const timerElement = element.querySelectorAll('.c-timer');\n      expect(timerElement.length).toBe(1);\n    });\n\n    it('renders major elements', () => {\n      const timerElement = element.querySelector('.c-timer');\n      const resetButton = timerElement.querySelector('.c-timer__ctrl-reset');\n      const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n      const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');\n      const timerValue = timerElement.querySelector('.c-timer__value');\n      const hasMajorElements = Boolean(\n        resetButton && pausePlayButton && timerDirectionIcon && timerValue\n      );\n\n      expect(hasMajorElements).toBe(true);\n    });\n\n    it('gets errors from actions if configuration is not passed', async () => {\n      await nextTick();\n      const objectPath = _.cloneDeep(timerObjectPath);\n      delete objectPath[0].configuration;\n\n      let action = openmct.actions.getAction('timer.start');\n      let actionResults = action.invoke(objectPath);\n      let actionFilterWithoutConfig = action.appliesTo(objectPath);\n      await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' });\n      let actionFilterWithConfig = action.appliesTo(timerObjectPath);\n\n      let actionError = new Error('Unable to run start timer action. No domainObject provided.');\n      expect(actionResults).toEqual(actionError);\n      expect(actionFilterWithoutConfig).toBe(undefined);\n      expect(actionFilterWithConfig).toBe(false);\n\n      action = openmct.actions.getAction('timer.stop');\n      actionResults = action.invoke(objectPath);\n      actionFilterWithoutConfig = action.appliesTo(objectPath);\n      await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' });\n      actionFilterWithConfig = action.appliesTo(timerObjectPath);\n\n      actionError = new Error('Unable to run stop timer action. No domainObject provided.');\n      expect(actionResults).toEqual(actionError);\n      expect(actionFilterWithoutConfig).toBe(undefined);\n      expect(actionFilterWithConfig).toBe(false);\n\n      action = openmct.actions.getAction('timer.pause');\n      actionResults = action.invoke(objectPath);\n      actionFilterWithoutConfig = action.appliesTo(objectPath);\n      await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' });\n      actionFilterWithConfig = action.appliesTo(timerObjectPath);\n\n      actionError = new Error('Unable to run pause timer action. No domainObject provided.');\n      expect(actionResults).toEqual(actionError);\n      expect(actionFilterWithoutConfig).toBe(undefined);\n      expect(actionFilterWithConfig).toBe(false);\n\n      action = openmct.actions.getAction('timer.restart');\n      actionResults = action.invoke(objectPath);\n      actionFilterWithoutConfig = action.appliesTo(objectPath);\n      await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' });\n      actionFilterWithConfig = action.appliesTo(timerObjectPath);\n\n      actionError = new Error('Unable to run restart timer action. No domainObject provided.');\n      expect(actionResults).toEqual(actionError);\n      expect(actionFilterWithoutConfig).toBe(undefined);\n      expect(actionFilterWithConfig).toBe(false);\n    });\n\n    it('displays a started timer ticking down to a future date', async () => {\n      const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM\n      openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime);\n\n      jasmine.clock().tick(5000);\n      await nextTick();\n\n      const timerElement = element.querySelector('.c-timer');\n      const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n      const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');\n      const timerValue = timerElement.querySelector('.c-timer__value').innerText;\n\n      expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);\n      expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true);\n      expect(timerValue).toBe('0D 23:59:55');\n    });\n\n    it('displays a started timer ticking up from a past date', async () => {\n      const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM\n      openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime);\n\n      jasmine.clock().tick(5000);\n      await nextTick();\n\n      const timerElement = element.querySelector('.c-timer');\n      const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n      const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');\n      const timerValue = timerElement.querySelector('.c-timer__value').innerText;\n\n      expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);\n      expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true);\n      expect(timerValue).toBe('1D 00:00:05');\n    });\n\n    it('displays a paused timer correctly in the DOM', async () => {\n      jasmine.clock().tick(5000);\n      await nextTick();\n\n      let action = openmct.actions.getAction('timer.pause');\n      if (action) {\n        action.invoke(timerObjectPath, timerView);\n      }\n\n      await nextTick();\n      const timerElement = element.querySelector('.c-timer');\n      const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n      let timerValue = timerElement.querySelector('.c-timer__value').innerText;\n\n      expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true);\n      expect(timerValue).toBe('0D 23:59:55');\n\n      jasmine.clock().tick(5000);\n      await nextTick();\n      expect(timerValue).toBe('0D 23:59:55');\n\n      action = openmct.actions.getAction('timer.start');\n      if (action) {\n        action.invoke(timerObjectPath, timerView);\n      }\n\n      await nextTick();\n      action = openmct.actions.getAction('timer.pause');\n      if (action) {\n        action.invoke(timerObjectPath, timerView);\n      }\n\n      await nextTick();\n      timerValue = timerElement.querySelector('.c-timer__value').innerText;\n      expect(timerValue).toBe('1D 00:00:00');\n    });\n\n    it('displays a stopped timer correctly in the DOM', async () => {\n      const action = openmct.actions.getAction('timer.stop');\n      if (action) {\n        action.invoke(timerObjectPath, timerView);\n      }\n\n      await nextTick();\n      const timerElement = element.querySelector('.c-timer');\n      const timerValue = timerElement.querySelector('.c-timer__value').innerText;\n      const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset');\n      const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n\n      expect(timerResetButton.classList.contains('hide')).toBe(true);\n      expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true);\n      expect(timerValue).toBe('--:--:--');\n    });\n\n    it('displays a restarted timer correctly in the DOM', async () => {\n      const action = openmct.actions.getAction('timer.restart');\n      if (action) {\n        action.invoke(timerObjectPath, timerView);\n      }\n\n      jasmine.clock().tick(5000);\n      await nextTick();\n      const timerElement = element.querySelector('.c-timer');\n      const timerValue = timerElement.querySelector('.c-timer__value').innerText;\n      const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');\n\n      expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);\n      expect(timerValue).toBe('0D 00:00:05');\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/userIndicator/README.md",
    "content": "# User Indicator\n\nThis plugin provides a user indicator for the top status bar that displays user information as\ndefined by a User Provider. A User Provider must be registered as a prerequisite for this plugin.\n\nIf the User Provider supports role-based access control, the user indicator will display the user's\ncurrent role and, if the user has multiple roles, allow the user to switch between them.\n\n## Usage\n\nTo use this plugin, first register a custom User Provider with the `openmct` API, or install the example\nUser Provider plugin:\n\n```javascript\nopenmct.install(openmct.plugins.example.ExampleUser());\n```\n\nThen, install the User Indicator plugin:\n\n```javascript\nopenmct.install(openmct.plugins.UserIndicator());\n```\n\n## Configuration\n\nThe User Indicator plugin does not require any configuration itself.\n\n## Mission Status\n\n\"Mission Status\" is a feature that is used to indicate the current state of a mission with regards to\none or more \"Mission Actions\". A mission action defines a verb that may be, for example, a task \nfor a spacecraft (such as \"Drive\" or \"Imagery\"), a change in the state of a ground system, or any \nother event that is relevant to the mission. Example states for a mission action might include\n\"Go\" or \"No Go\", indicating whether a particular action is currently cleared for execution.\n\nA user with the appropriate permissions may set the mission status by clicking on either the\nUser Indicator itself, or the \"Mission Status\" button next to the User Indicator. This will\nopen a dialog that allows the user to set the status of each mission action.\n\n### Provider Configuration\n\nIn order to use the Mission Status feature, a registered User Provider must define the following\nmethods:\n\n* `canSetMissionStatus`: A method that returns a boolean indicating whether the current user has\n  permission to set the mission status.\n* `getPossibleMissionActions`: A method that returns an array of mission actions that the user\n  may set the status of. Each mission action should have a `key` and a `name` property.\n* `getPossibleMissionActionStatuses`: A method that returns an array of `MissionStatusOption` objects,\n  which define the possible statuses for each mission action.\n* `getStatusForMissionAction`: A method that returns the current status for a given mission action.\n* `setStatusForMissionAction`: A method that sets the status for a given mission action.\n\nThe [Example User Provider](../../../example/exampleUser/ExampleUserProvider.js) provides an example\nimplementation of these methods.\n"
  },
  {
    "path": "src/plugins/userIndicator/components/MissionStatusPopup.vue",
    "content": "<template>\n  <div class=\"c-user-control-panel__component\">\n    <div class=\"c-user-control-panel__header\">\n      <div class=\"c-user-control-panel__title\">Mission Status</div>\n      <button\n        aria-label=\"Close Mission Status Panel\"\n        class=\"c-icon-button c-icon-button--sm t-close-btn icon-x\"\n        @click.stop=\"onDismiss\"\n      ></button>\n    </div>\n    <div class=\"c-ucp-mission-status\">\n      <template v-for=\"action in missionActions\" :key=\"action\">\n        <label class=\"c-ucp-mission-status__label\" :for=\"action\">{{ action }}</label>\n        <div class=\"c-ucp-mission-status__widget\" :class=\"getMissionActionStatusClass(action)\">\n          {{ missionActionStatusOptions[missionActionStatusMap[action]]?.label }}\n        </div>\n        <div class=\"c-ucp-mission-status__select\">\n          <select\n            :id=\"action\"\n            v-model=\"missionActionStatusMap[action]\"\n            :name=\"`${action} status`\"\n            @change=\"onChangeStatus(action)\"\n          >\n            <option\n              v-for=\"option in missionActionStatusOptions\"\n              :key=\"option.key\"\n              :value=\"option.key\"\n            >\n              {{ option.label }}\n            </option>\n          </select>\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { inject, ref } from 'vue';\n\nimport { useEventEmitter } from '../../../ui/composables/event';\n\nexport default {\n  inject: ['openmct'],\n  emits: ['dismiss'],\n  async setup() {\n    const openmct = inject('openmct');\n    let missionActions = ref([]);\n    let missionActionStatusOptions = ref([]);\n    let missionActionStatusMap = ref({});\n\n    try {\n      // Listen for missionActionStatusChange events\n      useEventEmitter(openmct.user.status, 'missionActionStatusChange', ({ action, status }) => {\n        missionActionStatusMap.value[action] = status.key; // Update the reactive property\n      });\n      // Fetch missionStatuses and missionActionStatuses simultaneously\n      const [fetchedMissionActions, fetchedMissionActionStatusOptions] = await Promise.all([\n        openmct.user.status.getPossibleMissionActions(),\n        openmct.user.status.getPossibleMissionActionStatuses()\n      ]);\n\n      // Assign the results to the reactive variables\n      missionActions.value = fetchedMissionActions;\n      missionActionStatusOptions.value = fetchedMissionActionStatusOptions;\n\n      const statusPromises = missionActions.value.map((action) =>\n        openmct.user.status.getStatusForMissionAction(action)\n      );\n\n      // Fetch all mission action statuses simultaneously\n      const statuses = await Promise.all(statusPromises);\n\n      // Reduce to a map of mission action to status\n      missionActionStatusMap.value = missionActions.value.reduce((acc, action, index) => {\n        acc[action] = statuses[index].key;\n        return acc;\n      }, {});\n    } catch (error) {\n      console.error('Error fetching mission statuses:', error);\n    }\n\n    return {\n      missionActions,\n      missionActionStatusOptions,\n      missionActionStatusMap\n    };\n  },\n  methods: {\n    onDismiss() {\n      this.$emit('dismiss');\n    },\n    async onChangeStatus(action) {\n      if (!this.openmct.user.status.canSetMissionStatus()) {\n        this.openmct.notifications.error('Selected user role is ineligible to set mission status');\n\n        return;\n      }\n\n      if (this.missionActionStatusMap !== undefined) {\n        const statusObject = this.findOptionByKey(this.missionActionStatusMap[action]);\n\n        const result = await this.openmct.user.status.setStatusForMissionAction(\n          action,\n          statusObject\n        );\n        if (result === true) {\n          this.openmct.notifications.info('Successfully set mission status');\n        } else {\n          this.openmct.notifications.error('Unable to set mission status');\n        }\n      }\n    },\n    /**\n     * @param {number} optionKey\n     */\n    findOptionByKey(optionKey) {\n      return this.missionActionStatusOptions.find(\n        (possibleMatch) => possibleMatch.key === optionKey\n      );\n    },\n    getMissionActionStatusClass(status) {\n      const statusValue =\n        this.missionActionStatusOptions[this.missionActionStatusMap[status]]?.label;\n      return {\n        '--is-no-go': statusValue === 'NO GO',\n        '--is-go': statusValue === 'GO'\n      };\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/userIndicator/components/UserIndicator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    ref=\"userIndicatorRef\"\n    class=\"c-indicator c-indicator--user icon-person\"\n    :class=\"canSetMissionStatus ? 'clickable' : ''\"\n    v-bind=\"$attrs\"\n    @click.stop=\"togglePopup\"\n  >\n    <span class=\"label c-indicator__label\" aria-label=\"User Role\">\n      {{ role ? `${userName}: ${role}` : userName }}\n      <button v-if=\"availableRoles?.length > 1\" @click.stop=\"promptForRoleSelection\">\n        Change Role\n      </button>\n      <button\n        v-if=\"canSetMissionStatus\"\n        aria-label=\"Toggle Mission Status Panel\"\n        @click.stop=\"togglePopup\"\n      >\n        Mission Status\n      </button>\n    </span>\n  </div>\n  <Teleport to=\"body\">\n    <div\n      v-show=\"isPopupVisible\"\n      ref=\"popupRef\"\n      class=\"c-user-control-panel\"\n      role=\"dialog\"\n      aria-label=\"User Control Panel\"\n      :style=\"popupStyle\"\n    >\n      <Suspense>\n        <template #default>\n          <MissionStatusPopup v-if=\"canSetMissionStatus\" @dismiss=\"togglePopup\" />\n        </template>\n        <template #fallback>\n          <div>Loading...</div>\n        </template>\n      </Suspense>\n    </div>\n  </Teleport>\n</template>\n\n<script>\nimport { ref } from 'vue';\n\nimport ActiveRoleSynchronizer from '../../../api/user/ActiveRoleSynchronizer.js';\nimport { useEventListener } from '../../../ui/composables/event.js';\nimport { useWindowResize } from '../../../ui/composables/resize.js';\nimport MissionStatusPopup from './MissionStatusPopup.vue';\n\nexport default {\n  name: 'UserIndicator',\n  components: {\n    MissionStatusPopup\n  },\n  inject: ['openmct'],\n  inheritAttrs: false,\n  setup() {\n    const { windowSize } = useWindowResize();\n    const isPopupVisible = ref(false);\n    const userIndicatorRef = ref(null);\n    const popupRef = ref(null);\n\n    // eslint-disable-next-line func-style\n    const handleOutsideClick = (event) => {\n      if (isPopupVisible.value && popupRef.value && !popupRef.value.contains(event.target)) {\n        isPopupVisible.value = false;\n      }\n    };\n\n    useEventListener(document, 'click', handleOutsideClick);\n\n    return { windowSize, isPopupVisible, popupRef, userIndicatorRef };\n  },\n  data() {\n    return {\n      userName: undefined,\n      role: undefined,\n      availableRoles: [],\n      loggedIn: false,\n      inputRoleSelection: undefined,\n      roleSelectionDialog: undefined,\n      canSetMissionStatus: false\n    };\n  },\n  computed: {\n    popupStyle() {\n      return {\n        top: `${this.position.top}px`,\n        left: `${this.position.left}px`\n      };\n    },\n    position() {\n      if (!this.isPopupVisible) {\n        return { top: 0, left: 0 };\n      }\n      const indicator = this.userIndicatorRef;\n      const indicatorRect = indicator.getBoundingClientRect();\n      let top = indicatorRect.bottom;\n      let left = indicatorRect.left;\n\n      const popupRect = this.popupRef.getBoundingClientRect();\n      const popupWidth = popupRect.width;\n      const popupHeight = popupRect.height;\n\n      // Check if the popup goes beyond the right edge of the window\n      if (left + popupWidth > this.windowSize.width) {\n        left = this.windowSize.width - popupWidth; // Adjust left to fit within the window\n      }\n\n      // Check if the popup goes beyond the bottom edge of the window\n      if (top + popupHeight > this.windowSize.height) {\n        top = indicatorRect.top - popupHeight; // Place popup above the indicator\n      }\n\n      return { top, left };\n    }\n  },\n  async created() {\n    await this.getUserInfo();\n  },\n  mounted() {\n    // need to wait for openmct to be loaded before using openmct.overlays.selection\n    // as document.body could be null\n    this.openmct.on('start', this.fetchOrPromptForRole);\n    this.roleChannel = new ActiveRoleSynchronizer(this.openmct);\n    this.roleChannel.subscribeToRoleChanges(this.onRoleChange);\n  },\n  beforeUnmount() {\n    this.roleChannel.unsubscribeFromRoleChanges(this.onRoleChange);\n    this.openmct.off('start', this.fetchOrPromptForRole);\n  },\n  methods: {\n    async getUserInfo() {\n      const user = await this.openmct.user.getCurrentUser();\n      this.canSetMissionStatus = await this.openmct.user.status.canSetMissionStatus();\n      this.userName = user.getName();\n      this.role = this.openmct.user.getActiveRole();\n      this.loggedIn = this.openmct.user.isLoggedIn();\n    },\n    async fetchOrPromptForRole() {\n      const UserAPI = this.openmct.user;\n      const activeRole = UserAPI.getActiveRole();\n      this.role = activeRole;\n      this.availableRoles = await this.openmct.user.getPossibleRoles();\n\n      // clear role if it's not in list of available roles, e.g., removed by admin\n      if (!this.availableRoles?.includes(this.role)) {\n        this.role = null;\n        UserAPI.setActiveRole(null);\n      }\n\n      // see if we need to prompt for role selection\n      if (!this.role) {\n        this.promptForRoleSelection();\n      } else {\n        // only notify the user if they have more than one role available\n        if (this.availableRoles.length > 1) {\n          this.openmct.notifications.info(`You're logged in as role ${activeRole}`);\n        }\n      }\n    },\n    promptForRoleSelection() {\n      const selectionOptions = this.availableRoles.map((role) => ({\n        key: role,\n        name: role\n      }));\n      // automatically select only role option\n      if (selectionOptions.length === 0) {\n        return;\n      }\n      if (selectionOptions.length === 1) {\n        this.updateRole(selectionOptions[0].key);\n        return;\n      }\n\n      this.roleSelectionDialog = this.openmct.overlays.selection({\n        selectionOptions,\n        iconClass: 'alert',\n        title: 'Select Role',\n        message: '',\n        currentSelection: this.role,\n        onChange: (event) => {\n          this.inputRoleSelection = event.target.value;\n        },\n        buttons: [\n          {\n            label: 'Select',\n            emphasis: true,\n            callback: () => {\n              this.roleSelectionDialog.dismiss();\n              this.roleSelectionDialog = undefined;\n              const inputValueOrDefault = this.inputRoleSelection || selectionOptions[0].key;\n              this.updateRole(inputValueOrDefault);\n              this.openmct.notifications.info(`Successfully set new role to ${this.role}`);\n            }\n          }\n        ]\n      });\n    },\n    onRoleChange(event) {\n      const role = event.data;\n      this.roleSelectionDialog?.dismiss();\n      this.setRoleSelection(role);\n    },\n    setRoleSelection(role) {\n      this.role = role;\n    },\n\n    updateRole(role) {\n      this.setRoleSelection(role);\n      this.openmct.user.setActiveRole(role);\n      // update other tabs through broadcast channel\n      this.roleChannel.broadcastNewRole(role);\n    },\n    togglePopup() {\n      this.isPopupVisible = !this.isPopupVisible && this.canSetMissionStatus;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/userIndicator/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport UserIndicator from './components/UserIndicator.vue';\n\nexport default function UserIndicatorPlugin() {\n  function addIndicator(openmct) {\n    openmct.indicators.add({\n      key: 'user-indicator',\n      vueComponent: UserIndicator,\n      priority: openmct.priority.HIGH\n    });\n  }\n\n  return function install(openmct) {\n    if (openmct.user.hasProvider()) {\n      addIndicator(openmct);\n    } else {\n      // back up if user provider added after indicator installed\n      openmct.user.on('providerAdded', () => {\n        addIndicator(openmct);\n      });\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/userIndicator/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * 'License'); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider.js';\n\nconst USERNAME = 'Coach McGuirk';\n\ndescribe('The User Indicator plugin', () => {\n  let openmct;\n  let element;\n  let child;\n  let appHolder;\n  let userIndicator;\n  let provider;\n\n  beforeEach((done) => {\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n    document.body.appendChild(appHolder);\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct = createOpenMct();\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('will not show, if there is no user provider', () => {\n    userIndicator = openmct.indicators.indicatorObjects.find(\n      (indicator) => indicator.key === 'user-indicator'\n    );\n\n    expect(userIndicator).toBe(undefined);\n  });\n\n  describe('with a user provider installed', () => {\n    beforeEach(() => {\n      provider = new ExampleUserProvider(openmct);\n      provider.autoLogin(USERNAME);\n\n      openmct.user.setProvider(provider);\n\n      return nextTick();\n    });\n\n    it('exists', () => {\n      userIndicator = openmct.indicators.indicatorObjects.find(\n        (indicator) => indicator.key === 'user-indicator'\n      ).vueComponent;\n\n      const hasClockIndicator = userIndicator !== null && userIndicator !== undefined;\n      expect(hasClockIndicator).toBe(true);\n    });\n\n    it('contains the logged in user name', (done) => {\n      openmct.user\n        .getCurrentUser()\n        .then(async (user) => {\n          await nextTick();\n\n          userIndicator = openmct.indicators.indicatorObjects.find(\n            (indicator) => indicator.key === 'user-indicator'\n          ).vueComponent;\n\n          expect(userIndicator).toBeDefined();\n          expect(userIndicator).not.toBeNull();\n          const userName = document.querySelector('[aria-label=\"User Role\"]').textContent.trim();\n\n          expect(user.name).toEqual(USERNAME);\n          expect(userName).toContain(USERNAME);\n        })\n        .finally(done);\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/userIndicator/user-indicator.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n.c-indicator {\n  &:before {\n    // Indicator icon\n    color: $colorKey;\n  }\n\n  &--user {\n    max-width: max-content;\n    &.clickable {\n      cursor: pointer;\n\n      @include hover() {\n        background: $colorIndicatorBgHov;\n      }\n    }\n  }\n}\n\n$statusCountWidth: 30px;\n\n.c-user-control-panel {\n  @include menuOuter();\n  display: flex;\n  flex-direction: column;\n  padding: $interiorMarginLg;\n  min-width: max-content;\n  max-width: 35%;\n\n  > * + * {\n    margin-top: $interiorMarginLg;\n  }\n\n  *:before {\n    font-size: 0.8em;\n    margin-right: $interiorMarginSm; // WTF - this is BAD!\n  }\n\n  &__section {\n    display: flex;\n    align-items: center;\n    flex-direction: row;\n\n    > * + * {\n      margin-left: $interiorMarginLg;\n    }\n  }\n\n  &__header {\n    align-items: center;\n    display: flex;\n    column-gap: $interiorMargin;\n    text-transform: uppercase;\n\n    > * {\n      flex: 0 0 auto;\n    }\n\n    [class*='title'] {\n      flex: 1 1 auto;\n    }\n  }\n\n  .t-close-btn {\n    &:before {\n      margin-right: 0;\n    }\n  }\n\n  &__component {\n    // General classes for new control panel component\n    display: flex;\n    flex-direction: column;\n    gap: $interiorMargin;\n  }\n\n  &__top {\n    text-transform: uppercase;\n  }\n\n  &__user-role,\n  &__updated {\n    opacity: 50%;\n  }\n\n  &__updated {\n    flex: 1 1 auto;\n    text-align: right;\n  }\n\n  &__poll-question {\n    background: $colorBodyFg;\n    color: $colorBodyBg;\n    border-radius: $controlCr;\n    font-weight: bold;\n    padding: $interiorMarginSm $interiorMargin;\n\n    .c-user-control-panel--admin & {\n      background: rgba($colorBodyFg, 0.1);\n      color: $colorBodyFg;\n    }\n  }\n\n  /*************************************************** ADMIN INTERFACE */\n  &__content {\n    $m: $interiorMargin;\n    display: grid;\n    grid-template-columns: max-content 1fr;\n    grid-column-gap: $m;\n    grid-row-gap: $m;\n\n    [class*='__label'] {\n      padding: 3px 0;\n    }\n\n    [class*='__label'] {\n      padding: 3px 0;\n    }\n\n    [class*='__poll-table'] {\n      grid-column: span 2;\n    }\n\n    [class*='new-question'] {\n      align-items: center;\n      display: flex;\n      flex-direction: row;\n      > * + * {\n        margin-left: $interiorMargin;\n      }\n\n      input {\n        flex: 1 1 auto;\n        height: $btnStdH;\n      }\n\n      button {\n        flex: 0 0 auto;\n      }\n    }\n  }\n}\n\n/**************** STYLES FOR THE MISSION STATUS USER CONTROL PANEL */\n.c-ucp-mission-status {\n  $bg: rgba(black, 0.7);\n  display: grid;\n  grid-template-columns: max-content max-content 1fr;\n  align-items: center;\n  grid-column-gap: $interiorMarginLg;\n  grid-row-gap: $interiorMargin;\n\n  &__widget {\n    border-radius: $basicCr;\n    background: $bg;\n    border: 1px solid transparent;\n    padding: $interiorMarginSm $interiorMarginLg;\n    text-align: center;\n\n    &.--is-go {\n      $c: #2c7527;\n      background: $c;\n      color: white;\n    }\n\n    &.--is-no-go {\n      $c: #fbc147;\n      background: $bg;\n      border: 1px solid $c;\n      color: $c;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/DurationFormat.js",
    "content": "import moment from 'moment';\n\nconst DATE_FORMAT = 'HH:mm:ss';\nconst DATE_FORMATS = [DATE_FORMAT, `${DATE_FORMAT}.SSS`];\n\n/**\n * Formatter for duration. Uses moment to produce a date from a given\n * value, but output is formatted to display only time. Can be used for\n * specifying a time duration. For specifying duration, it's best to\n * specify a date of January 1, 1970, as the ms offset will equal the\n * duration represented by the time.\n *\n * @implements {Format}\n * @constructor\n */\nclass DurationFormat {\n  constructor() {\n    this.key = 'duration';\n  }\n  format(value, formatString) {\n    return moment.utc(value).format(formatString || DATE_FORMAT);\n  }\n\n  parse(text) {\n    return moment.duration(text).asMilliseconds();\n  }\n\n  validate(text) {\n    return moment.utc(text, DATE_FORMATS, true).isValid();\n  }\n}\n\nexport default DurationFormat;\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/LocalClock.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport DefaultClock from '../../utils/clock/DefaultClock.js';\n/**\n * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the\n * application based on UTC time values provided by a ticking local clock,\n * with the periodicity specified.\n * @param {number} period The periodicity with which the clock should tick\n * @constructor\n */\n\nexport default class LocalClock extends DefaultClock {\n  constructor(period = 100) {\n    super();\n\n    this.key = 'local';\n    this.name = 'Local Clock';\n    this.description = 'Provides UTC timestamps every second from the local system clock.';\n\n    this.period = period;\n    this.timeoutHandle = undefined;\n    this.lastTick = Date.now();\n  }\n\n  start() {\n    super.tick(this.lastTick);\n    this.timeoutHandle = setTimeout(this.tick.bind(this), this.period);\n  }\n\n  stop() {\n    if (this.timeoutHandle) {\n      clearTimeout(this.timeoutHandle);\n      this.timeoutHandle = undefined;\n    }\n  }\n\n  tick() {\n    const now = Date.now();\n    super.tick(now);\n    this.timeoutHandle = setTimeout(this.tick.bind(this), this.period);\n  }\n}\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/UTCTimeFormat.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport moment from 'moment';\n\n/**\n * Formatter for UTC timestamps. Interprets numeric values as\n * milliseconds since the start of 1970.\n *\n * @implements {Format}\n * @constructor\n */\nexport default class UTCTimeFormat {\n  constructor() {\n    this.key = 'utc';\n    this.DATE_FORMAT = `YYYY-MM-DD HH:mm:ss.SSS`;\n    this.DATE_FORMATS = {\n      PRECISION_DEFAULT: this.DATE_FORMAT,\n      PRECISION_DEFAULT_WITH_ZULU: `${this.DATE_FORMAT}Z`,\n      PRECISION_DEFAULT_WITH_ZULU_MOMENT: `${this.DATE_FORMAT}[Z]`,\n      PRECISION_SECONDS: `YYYY-MM-DD HH:mm:ss`,\n      PRECISION_MINUTES: `YYYY-MM-DD HH:mm`,\n      PRECISION_DAYS: 'YYYY-MM-DD',\n      PRECISION_SECONDS_TIME_ONLY: 'HH:mm:ss',\n      PRECISION_MINUTES_TIME_ONLY: 'HH:mm'\n    };\n  }\n\n  /**\n   * @param {string} formatString\n   * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value.\n   */\n  isValidFormatString(formatString) {\n    return Object.values(this.DATE_FORMATS).includes(formatString);\n  }\n\n  /**\n   * @param {number} value The value to format.\n   * @param {string} formatString The format string to use for formatting.\n   * @returns {string} the formatted date(s). If multiple values were requested, then an array of\n   * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position\n   * in the array.\n   */\n  format(value, formatString) {\n    if (value !== undefined) {\n      const utc = moment.utc(value);\n\n      if (formatString !== undefined && !this.isValidFormatString(formatString)) {\n        throw 'Invalid format requested from UTC Time Formatter ';\n      }\n\n      const format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT_WITH_ZULU_MOMENT;\n\n      return utc.format(format);\n    }\n  }\n\n  /**\n   * Optional formatting method that allows for splitting date and time into separate inputs\n   * Allows for easier manipulation of date or time\n   * @param {number} value The value to format.\n   * @returns {string} the formatted date.\n   */\n  formatDate(value) {\n    return this.format(value, this.DATE_FORMATS.PRECISION_DAYS);\n  }\n\n  /**\n   * @param {number|string} text The text to parse.\n   * @param {string} formatString The format string to use for parsing.\n   * @returns {number} the value parsed from the text.\n   * If the text is a number, it is returned as is.\n   */\n  parse(text, formatString) {\n    if (typeof text === 'number') {\n      return text;\n    }\n\n    return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf();\n  }\n\n  validate(text) {\n    return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid();\n  }\n}\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/UTCTimeSystem.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * This time system supports UTC dates.\n * @implements TimeSystem\n * @constructor\n */\nclass UTCTimeSystem {\n  /**\n   * Metadata used to identify the time system in\n   * the UI\n   */\n  constructor() {\n    this.key = 'utc';\n    this.name = 'UTC';\n    this.cssClass = 'icon-clock';\n    this.timeFormat = 'utc';\n    this.durationFormat = 'duration';\n    this.isUTCBased = true;\n  }\n}\n\nexport default UTCTimeSystem;\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport DurationFormat from './DurationFormat.js';\nimport LocalClock from './LocalClock.js';\nimport UTCTimeFormat from './UTCTimeFormat.js';\nimport UTCTimeSystem from './UTCTimeSystem.js';\n\n/**\n * Install a time system that supports UTC times. It also installs a local\n * clock source that ticks every 100ms, providing UTC times.\n */\nexport default function () {\n  return function (openmct) {\n    const timeSystem = new UTCTimeSystem();\n    openmct.time.addTimeSystem(timeSystem);\n    openmct.time.addClock(new LocalClock(100));\n    openmct.telemetry.addFormat(new UTCTimeFormat());\n    openmct.telemetry.addFormat(new DurationFormat());\n  };\n}\n"
  },
  {
    "path": "src/plugins/utcTimeSystem/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport LocalClock from './LocalClock.js';\nimport UTCTimeFormat from './UTCTimeFormat.js';\nimport UTCTimeSystem from './UTCTimeSystem.js';\n\ndescribe('The UTC Time System', () => {\n  const UTC_SYSTEM_AND_FORMAT_KEY = 'utc';\n  const DURATION_FORMAT_KEY = 'duration';\n  let openmct;\n  let utcTimeSystem;\n  let mockTimeout;\n\n  beforeEach(() => {\n    openmct = createOpenMct();\n    openmct.install(openmct.plugins.UTCTimeSystem());\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('plugin', function () {\n    beforeEach(function () {\n      mockTimeout = jasmine.createSpy('timeout');\n      utcTimeSystem = new UTCTimeSystem(mockTimeout);\n    });\n\n    it('is installed', () => {\n      let timeSystems = openmct.time.getAllTimeSystems();\n      let utc = timeSystems.find((ts) => ts.key === UTC_SYSTEM_AND_FORMAT_KEY);\n\n      expect(utc).not.toEqual(-1);\n    });\n\n    it('can be set to be the main time system', () => {\n      openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, {\n        start: 0,\n        end: 1\n      });\n\n      expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY);\n    });\n\n    it('uses the utc time format', () => {\n      expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY);\n    });\n\n    it('is UTC based', () => {\n      expect(utcTimeSystem.isUTCBased).toBe(true);\n    });\n\n    it('defines expected metadata', () => {\n      expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY);\n      expect(utcTimeSystem.name).toBeDefined();\n      expect(utcTimeSystem.cssClass).toBeDefined();\n      expect(utcTimeSystem.durationFormat).toBeDefined();\n    });\n  });\n\n  describe('LocalClock class', function () {\n    let clock;\n    const timeoutHandle = {};\n\n    beforeEach(function () {\n      mockTimeout = jasmine.createSpy('timeout');\n      mockTimeout.and.returnValue(timeoutHandle);\n\n      clock = new LocalClock(0);\n      clock.start();\n    });\n\n    it('calls listeners on tick with current time', function () {\n      const mockListener = jasmine.createSpy('listener');\n      clock.on('tick', mockListener);\n      clock.tick();\n      expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number));\n    });\n  });\n\n  describe('UTC Time Format', () => {\n    let utcTimeFormatter;\n\n    beforeEach(() => {\n      utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY);\n    });\n\n    it('is installed by the plugin', () => {\n      expect(utcTimeFormatter).toBeDefined();\n    });\n\n    it('formats from ms since Unix epoch into Open MCT UTC time format', () => {\n      const TIME_IN_MS = 1638574560945;\n      const TIME_AS_STRING = '2021-12-03 23:36:00.945Z';\n\n      const formattedTime = utcTimeFormatter.format(TIME_IN_MS);\n      expect(formattedTime).toEqual(TIME_AS_STRING);\n    });\n\n    it('formats from ms since Unix epoch into terse UTC formats', () => {\n      const utcTimeFormatterInstance = new UTCTimeFormat();\n\n      const TIME_IN_MS = 1638574560945;\n      const EXPECTED_FORMATS = {\n        PRECISION_DEFAULT: '2021-12-03 23:36:00.945',\n        PRECISION_SECONDS: '2021-12-03 23:36:00',\n        PRECISION_MINUTES: '2021-12-03 23:36',\n        PRECISION_DAYS: '2021-12-03'\n      };\n\n      Object.keys(EXPECTED_FORMATS).forEach((formatKey) => {\n        const formattedTime = utcTimeFormatterInstance.format(\n          TIME_IN_MS,\n          utcTimeFormatterInstance.DATE_FORMATS[formatKey]\n        );\n        expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]);\n      });\n    });\n\n    it('parses from Open MCT UTC time format to ms since Unix epoch.', () => {\n      const TIME_IN_MS = 1638574560945;\n      const TIME_AS_STRING = '2021-12-03 23:36:00.945Z';\n\n      const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING);\n      expect(parsedTime).toEqual(TIME_IN_MS);\n    });\n\n    it('validates correctly formatted Open MCT UTC times.', () => {\n      const TIME_AS_STRING = '2021-12-03 23:36:00.945Z';\n\n      const isValid = utcTimeFormatter.validate(TIME_AS_STRING);\n      expect(isValid).toBeTrue();\n    });\n  });\n\n  describe('Duration Format', () => {\n    let durationTimeFormatter;\n\n    beforeEach(() => {\n      durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY);\n    });\n\n    it('is installed by the plugin', () => {\n      expect(durationTimeFormatter).toBeDefined();\n    });\n\n    it('formats from ms into Open MCT duration format', () => {\n      const TIME_IN_MS = 2000;\n      const TIME_AS_STRING = '00:00:02';\n\n      const formattedTime = durationTimeFormatter.format(TIME_IN_MS);\n      expect(formattedTime).toEqual(TIME_AS_STRING);\n    });\n\n    it('parses from Open MCT duration format to ms', () => {\n      const TIME_IN_MS = 2000;\n      const TIME_AS_STRING = '00:00:02';\n\n      const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING);\n      expect(parsedTime).toEqual(TIME_IN_MS);\n    });\n\n    it('validates correctly formatted Open MCT duration strings.', () => {\n      const TIME_AS_STRING = '00:00:02';\n\n      const isValid = durationTimeFormatter.validate(TIME_AS_STRING);\n      expect(isValid).toBeTrue();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/viewDatumAction/ViewDatumAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport MetadataListView from './components/MetadataList.vue';\n\nconst VIEW_DATUM_ACTION_KEY = 'viewDatumAction';\n\nclass ViewDatumAction {\n  constructor(openmct) {\n    this.name = 'View Full Datum';\n    this.key = VIEW_DATUM_ACTION_KEY;\n    this.description = 'View full value of datum received';\n    this.cssClass = 'icon-object';\n\n    this.openmct = openmct;\n  }\n  invoke(objectPath, view) {\n    let viewContext = view.getViewContext && view.getViewContext();\n    const row = viewContext.row;\n    let attributes = row.getDatum && row.getDatum();\n    const { vNode, destroy } = mount(\n      {\n        components: {\n          MetadataListView\n        },\n        provide: {\n          name: this.name,\n          attributes\n        },\n        template: '<MetadataListView />'\n      },\n      {\n        app: this.openmct.app\n      }\n    );\n\n    this.openmct.overlays.overlay({\n      element: vNode.el,\n      size: 'large',\n      dismissible: true,\n      onDestroy: destroy\n    });\n  }\n  appliesTo(objectPath, view = {}) {\n    let viewContext = (view.getViewContext && view.getViewContext()) || {};\n    const row = viewContext.row;\n    if (!row) {\n      return false;\n    }\n\n    let datum = row.getDatum;\n    let enabled = row.viewDatumAction;\n    if (enabled && datum) {\n      return true;\n    }\n\n    return false;\n  }\n}\n\nexport { VIEW_DATUM_ACTION_KEY };\n\nexport default ViewDatumAction;\n"
  },
  {
    "path": "src/plugins/viewDatumAction/components/MetadataList.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-attributes-view\">\n    <div class=\"c-overlay__top-bar\">\n      <div class=\"c-overlay__dialog-title\">{{ name }}</div>\n    </div>\n    <div class=\"c-overlay__contents-main l-preview-window__object-view\">\n      <ul class=\"c-attributes-view__content\">\n        <li v-for=\"attribute in Object.keys(attributes)\" :key=\"attribute\">\n          <span class=\"c-attributes-view__grid-item__label\">{{ attribute }}</span>\n          <span class=\"c-attributes-view__grid-item__value\">{{ attributes[attribute] }}</span>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['name', 'attributes']\n};\n</script>\n"
  },
  {
    "path": "src/plugins/viewDatumAction/components/metadata-list.scss",
    "content": ".c-attributes-view {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n\n  > * {\n    flex: 0 0 auto;\n  }\n\n  &__content {\n    $p: 3px;\n\n    display: grid;\n    grid-template-columns: max-content 1fr;\n    grid-row-gap: $p;\n\n    li {\n      display: contents;\n    }\n\n    [class*='__grid-item'] {\n      border-bottom: 1px solid rgba(#999, 0.2);\n      padding: 0 5px $p 0;\n    }\n\n    [class*='__label'] {\n      opacity: 0.8;\n    }\n  }\n}\n"
  },
  {
    "path": "src/plugins/viewDatumAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ViewDatumAction from './ViewDatumAction.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.actions.register(new ViewDatumAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/viewDatumAction/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\ndescribe('the plugin', () => {\n  let openmct;\n  let viewDatumAction;\n  let mockObjectPath;\n  let mockView;\n  let mockDatum;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n\n    viewDatumAction = openmct.actions._allActions.viewDatumAction;\n\n    mockObjectPath = [\n      {\n        name: 'mock object',\n        type: 'telemetry-table',\n        identifier: {\n          key: 'mock-object',\n          namespace: ''\n        }\n      }\n    ];\n\n    mockDatum = {\n      time: 123456789,\n      sin: 0.4455512,\n      cos: 0.4455512\n    };\n\n    mockView = {\n      getViewContext: () => {\n        return {\n          row: {\n            viewDatumAction: true,\n            getDatum: () => {\n              return mockDatum;\n            }\n          }\n        };\n      }\n    };\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('installs the view datum action', () => {\n    expect(viewDatumAction).toBeDefined();\n  });\n\n  describe('when invoked', () => {\n    beforeEach(() => {\n      openmct.overlays.overlay = function (options) {};\n\n      spyOn(openmct.overlays, 'overlay');\n\n      viewDatumAction.invoke(mockObjectPath, mockView);\n    });\n\n    it('creates an overlay', () => {\n      expect(openmct.overlays.overlay).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "src/plugins/viewLargeAction/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport ViewLargeAction from './viewLargeAction.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.actions.register(new ViewLargeAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/plugins/viewLargeAction/viewLargeAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport PreviewContainer from '@/ui/preview/PreviewContainer.vue';\n\nconst VIEW_LARGE_ACTION_KEY = 'large.view';\n\nclass ViewLargeAction {\n  constructor(openmct) {\n    this.openmct = openmct;\n\n    this.cssClass = 'icon-items-expand';\n    this.description = 'View Large';\n    this.group = 'windowing';\n    this.key = VIEW_LARGE_ACTION_KEY;\n    this.name = 'Large View';\n    this.priority = 1;\n    this.showInStatusBar = true;\n    this.destroy = null;\n    this.preview = null;\n  }\n\n  invoke(objectPath, view) {\n    performance.mark('viewlarge.start');\n    const childElement = view?.parentElement?.firstElementChild;\n    if (!childElement) {\n      const message = 'ViewLargeAction: missing element';\n      this.openmct.notifications.error(message);\n      throw new Error(message);\n    }\n\n    this._expand(objectPath, view);\n  }\n\n  appliesTo(objectPath, view) {\n    const childElement = view?.parentElement?.firstElementChild;\n\n    return (\n      childElement &&\n      !childElement?.classList?.contains('js-main-container') &&\n      !this.openmct.router.isNavigatedObject(objectPath)\n    );\n  }\n\n  _expand(objectPath, view) {\n    const element = this._getPreview(objectPath, view);\n    view.onPreviewModeChange?.({ isPreviewing: true });\n\n    this.overlay = this.openmct.overlays.overlay({\n      element,\n      size: 'large',\n      autoHide: false,\n      onDestroy: () => {\n        this.destroy();\n        this.preview = null;\n        view.onPreviewModeChange?.();\n      }\n    });\n  }\n\n  _getPreview(objectPath, view) {\n    const { vNode, destroy } = mount(\n      {\n        components: {\n          PreviewContainer\n        },\n        provide: {\n          openmct: this.openmct,\n          objectPath\n        },\n        data() {\n          return {\n            view\n          };\n        },\n        template: '<preview-container :existing-view=\"view\"></preview-container>'\n      },\n      {\n        app: this.openmct.app\n      }\n    );\n    this.preview = vNode.componentInstance;\n    this.destroy = () => {\n      destroy();\n      this.preview = null;\n    };\n\n    return this.preview.$el;\n  }\n}\n\nexport { VIEW_LARGE_ACTION_KEY };\n\nexport default ViewLargeAction;\n"
  },
  {
    "path": "src/plugins/webPage/WebPageViewProvider.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\n\nimport WebPageComponent from './components/WebPage.vue';\n\nexport default function WebPage(openmct) {\n  return {\n    key: 'webPage',\n    name: 'Web Page',\n    cssClass: 'icon-page',\n    canView: function (domainObject) {\n      return domainObject.type === 'webPage';\n    },\n    view: function (domainObject) {\n      let _destroy = null;\n\n      return {\n        show: function (element) {\n          const { destroy } = mount(\n            {\n              el: element,\n              components: {\n                WebPageComponent: WebPageComponent\n              },\n              provide: {\n                openmct,\n                domainObject\n              },\n              template: '<web-page-component></web-page-component>'\n            },\n            {\n              app: openmct.app,\n              element\n            }\n          );\n          _destroy = destroy;\n        },\n        destroy: function () {\n          if (_destroy) {\n            _destroy();\n          }\n        }\n      };\n    }\n  };\n}\n"
  },
  {
    "path": "src/plugins/webPage/components/WebPage.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"l-iframe abs\">\n    <iframe :src=\"url\"></iframe>\n  </div>\n</template>\n\n<script>\nimport { sanitizeUrl } from '@braintree/sanitize-url';\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  data: function () {\n    return {\n      currentDomainObject: this.domainObject\n    };\n  },\n  computed: {\n    url() {\n      return sanitizeUrl(this.currentDomainObject.url);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/plugins/webPage/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport WebPageViewProvider from './WebPageViewProvider.js';\n\nexport default function plugin() {\n  return function install(openmct) {\n    openmct.objectViews.addProvider(new WebPageViewProvider(openmct));\n\n    openmct.types.addType('webPage', {\n      name: 'Web Page',\n      description:\n        'Embed a web page or web-based image in a resizeable window component. Note that the URL being embedded must allow iframing.',\n      creatable: true,\n      cssClass: 'icon-page',\n      form: [\n        {\n          key: 'url',\n          name: 'URL',\n          control: 'textfield',\n          required: true,\n          cssClass: 'l-input-lg'\n        }\n      ]\n    });\n  };\n}\n"
  },
  {
    "path": "src/plugins/webPage/pluginSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\n\nimport WebPagePlugin from './plugin.js';\n\nfunction getView(openmct, domainObj, objectPath) {\n  const applicableViews = openmct.objectViews.get(domainObj, objectPath);\n  const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage');\n\n  return webpageView.view(domainObj, [domainObj]);\n}\n\nfunction destroyView(view) {\n  return view.destroy();\n}\n\ndescribe('The web page plugin', function () {\n  let mockDomainObject;\n  let mockDomainObjectPath;\n  let openmct;\n  let element;\n  let child;\n  let view;\n\n  beforeEach((done) => {\n    mockDomainObjectPath = [\n      {\n        name: 'mock webpage',\n        type: 'webpage',\n        identifier: {\n          key: 'mock-webpage',\n          namespace: ''\n        }\n      }\n    ];\n\n    mockDomainObject = {\n      displayFormat: '',\n      name: 'Unnamed WebPage',\n      type: 'webPage',\n      location: 'f69c21ac-24ef-450c-8e2f-3d527087d285',\n      modified: 1627483839783,\n      url: '123',\n      displayText: '123',\n      persisted: 1627483839783,\n      id: '3d9c243d-dffb-446b-8474-d9931a99d679',\n      identifier: {\n        namespace: '',\n        key: '3d9c243d-dffb-446b-8474-d9931a99d679'\n      }\n    };\n\n    openmct = createOpenMct();\n    openmct.install(new WebPagePlugin());\n\n    element = document.createElement('div');\n    element.style.width = '640px';\n    element.style.height = '480px';\n    child = document.createElement('div');\n    child.style.width = '640px';\n    child.style.height = '480px';\n    element.appendChild(child);\n\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    destroyView(view);\n\n    return resetApplicationState(openmct);\n  });\n\n  describe('the view', () => {\n    beforeEach(() => {\n      view = getView(openmct, mockDomainObject, mockDomainObjectPath);\n      view.show(child, true);\n    });\n\n    it('provides a view', () => {\n      expect(view).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "src/selection/Selection.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport _ from 'lodash';\n\n/**\n * @typedef {Object} Selectable\n * @property {HTMLElement} element The HTML element that is selectable\n * @property {Object} context The context of the selectable, which may include a DomainObject\n */\n\n/**\n * @typedef {import('../../src/MCT').MCT} OpenMCT\n */\n\n/**\n * Manages selection state for Open MCT\n * @constructor\n * @extends EventEmitter\n */\nexport default class Selection extends EventEmitter {\n  /**\n   * @param {OpenMCT} openmct The Open MCT instance\n   */\n  constructor(openmct) {\n    super();\n\n    /** @type {OpenMCT} */\n    this.openmct = openmct;\n    /** @type {Selectable[]} */\n    this.selected = [];\n  }\n\n  /**\n   * Gets the selected object.\n   * @returns {Selectable[]} The currently selected objects\n   * @public\n   */\n  get() {\n    return this.selected;\n  }\n\n  /**\n   * Selects the selectable object and emits the 'change' event.\n   *\n   * @param {Selectable|Selectable[]} selectable An object or array of objects with element and context properties\n   * @param {boolean} isMultiSelectEvent flag indication shift key is pressed or not\n   * @private\n   */\n  select(selectable, isMultiSelectEvent) {\n    if (!Array.isArray(selectable)) {\n      selectable = [selectable];\n    }\n\n    let multiSelect =\n      isMultiSelectEvent &&\n      this.parentSupportsMultiSelect(selectable) &&\n      this.isPeer(selectable) &&\n      !this.selectionContainsParent(selectable);\n\n    if (multiSelect) {\n      this.handleMultiSelect(selectable);\n    } else {\n      this.handleSingleSelect(selectable);\n    }\n  }\n  /**\n   * @private\n   */\n  handleMultiSelect(selectable) {\n    if (this.elementSelected(selectable)) {\n      this.remove(selectable);\n    } else {\n      this.addSelectionAttributes(selectable);\n      this.selected.push(selectable);\n    }\n\n    this.emit('change', this.selected);\n  }\n  /**\n   * @private\n   */\n  handleSingleSelect(selectable) {\n    if (!_.isEqual([selectable], this.selected)) {\n      this.setSelectionStyles(selectable);\n      this.selected = [selectable];\n\n      this.emit('change', this.selected);\n    }\n  }\n  /**\n   * @private\n   */\n  elementSelected(selectable) {\n    return this.selected.some((selectionPath) => _.isEqual(selectionPath, selectable));\n  }\n  /**\n   * @private\n   */\n  remove(selectable) {\n    this.selected = this.selected.filter((selectionPath) => !_.isEqual(selectionPath, selectable));\n\n    if (this.selected.length === 0) {\n      this.removeSelectionAttributes(selectable);\n      selectable[1].element.click(); // Select the parent if there is no selection.\n    } else {\n      this.removeSelectionAttributes(selectable, true);\n    }\n  }\n  /**\n   * @private\n   */\n  setSelectionStyles(selectable) {\n    this.selected.forEach((selectionPath) => this.removeSelectionAttributes(selectionPath));\n    this.addSelectionAttributes(selectable);\n  }\n  removeSelectionAttributes(selectionPath, keepParentStyle) {\n    if (selectionPath[0] && selectionPath[0].element) {\n      selectionPath[0].element.removeAttribute('s-selected');\n    }\n\n    if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) {\n      selectionPath[1].element.removeAttribute('s-selected-parent');\n    }\n  }\n  /**\n   * Adds selection attributes to the selected element and its parent.\n   * @private\n   */\n  addSelectionAttributes(selectable) {\n    if (selectable[0] && selectable[0].element) {\n      selectable[0].element.setAttribute('s-selected', '');\n    }\n\n    if (selectable[1] && selectable[1].element) {\n      selectable[1].element.setAttribute('s-selected-parent', '');\n    }\n  }\n  /**\n   * @private\n   */\n  parentSupportsMultiSelect(selectable) {\n    return selectable[1] && selectable[1].context.supportsMultiSelect;\n  }\n  /**\n   * @private\n   */\n  selectionContainsParent(selectable) {\n    return this.selected.some((selectionPath) => _.isEqual(selectionPath[0], selectable[1]));\n  }\n  /**\n   * @private\n   */\n  isPeer(selectable) {\n    return this.selected.some((selectionPath) => _.isEqual(selectionPath[1], selectable[1]));\n  }\n  /**\n   * @private\n   */\n  isSelectable(element) {\n    if (!element) {\n      return false;\n    }\n\n    return Boolean(element.closest('[data-selectable]'));\n  }\n  /**\n   * @private\n   */\n  capture(selectable) {\n    let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable);\n\n    if (!this.capturing || capturingContainsSelectable) {\n      this.capturing = [];\n    }\n\n    this.capturing.push(selectable);\n  }\n  /**\n   * @private\n   */\n  selectCapture(selectable, event) {\n    if (!this.capturing) {\n      return;\n    }\n\n    let reversedCapturing = this.capturing.reverse();\n    delete this.capturing;\n    this.select(reversedCapturing, event.shiftKey);\n  }\n  /**\n   * Attaches the click handlers to the element.\n   *\n   * @param element an html element\n   * @param context object which defines item or other arbitrary properties.\n   * e.g. {\n   *          item: domainObject,\n   *          elementProxy: element,\n   *          controller: fixedController\n   *       }\n   * @param select a flag to select the element if true\n   * @returns a function that removes the click handlers from the element\n   * @public\n   */\n  selectable(element, context, select) {\n    if (!this.isSelectable(element)) {\n      return () => {};\n    }\n\n    let selectable = {\n      context: context,\n      element: element\n    };\n\n    const capture = this.capture.bind(this, selectable);\n    const selectCapture = this.selectCapture.bind(this, selectable);\n    let removeMutable = false;\n\n    element.addEventListener('click', capture, true);\n    element.addEventListener('click', selectCapture);\n\n    if (context.item && context.item.isMutable !== true) {\n      removeMutable = true;\n      context.item = this.openmct.objects.toMutable(context.item);\n    }\n\n    if (select) {\n      if (typeof select === 'object') {\n        element.dispatchEvent(select);\n      } else if (typeof select === 'boolean') {\n        element.click();\n      }\n    }\n\n    return function () {\n      element.removeEventListener('click', capture, true);\n      element.removeEventListener('click', selectCapture);\n\n      if (context.item !== undefined && context.item.isMutable && removeMutable === true) {\n        this.openmct.objects.destroyMutable(context.item);\n      }\n    }.bind(this);\n  }\n}\n"
  },
  {
    "path": "src/styles/_about.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n// Used by About screen, licenses, etc.\n.c-splash-image {\n  background-position: center;\n  background-repeat: no-repeat;\n  background-size: cover;\n  background-image: url('../ui/layout/assets/images/bg-splash.jpg');\n  margin-top: 30px; // Don't overlap with close \"X\" button\n\n  &:before,\n  &:after {\n    background-position: center;\n    background-repeat: no-repeat;\n    position: absolute;\n    background-image: url('../ui/layout/assets/images/logo-openmct-shdw.svg');\n    background-size: contain;\n    content: '';\n  }\n\n  &:before {\n    // NASA logo, dude\n    $w: 5%;\n    $m: 10px;\n    background-image: url('../ui/layout/assets/images/logo-nasa.svg');\n    top: $m;\n    right: auto;\n    bottom: auto;\n    left: $m;\n    height: auto;\n    width: $w * 2;\n    padding-bottom: $w;\n    padding-top: $w;\n  }\n\n  &:after {\n    // App logo\n    $d: 25%;\n    top: $d;\n    right: $d;\n    bottom: $d;\n    left: $d;\n  }\n}\n\n.c-about {\n  &--splash {\n    // Large initial image after click on app logo with text beneath\n    @include abs();\n    display: flex;\n    flex-direction: column;\n  }\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__image,\n  &__text {\n    flex: 1 1 auto;\n  }\n\n  &__image {\n    height: 35%;\n  }\n\n  &__text {\n    height: 65%;\n    overflow: auto;\n    > * + * {\n      border-top: 1px solid $colorInteriorBorder;\n      margin-top: 1em;\n    }\n  }\n\n  &--licenses {\n    padding: 0 10%;\n    .c-license {\n      + .c-license {\n        border-top: 1px solid $colorInteriorBorder;\n        margin-top: 2em;\n      }\n    }\n  }\n\n  a {\n    color: $colorAboutLink;\n  }\n\n  em {\n    color: pushBack($colorBodyFg, 20%);\n  }\n\n  h1,\n  h2,\n  h3 {\n    font-weight: normal;\n    margin-bottom: 0.25em;\n  }\n\n  h1 {\n    font-size: 2.25em;\n  }\n\n  h2 {\n    font-size: 1.5em;\n  }\n}\n"
  },
  {
    "path": "src/styles/_animations.scss",
    "content": "@keyframes rotation {\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes rotation-centered {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes clock-hands {\n  0% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  100% {\n    transform: translate(-50%, -50%) rotate(360deg);\n  }\n}\n\n@keyframes clock-hands-sticky {\n  0% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  7% {\n    transform: translate(-50%, -50%) rotate(0deg);\n  }\n  8% {\n    transform: translate(-50%, -50%) rotate(30deg);\n  }\n  15% {\n    transform: translate(-50%, -50%) rotate(30deg);\n  }\n  16% {\n    transform: translate(-50%, -50%) rotate(60deg);\n  }\n  24% {\n    transform: translate(-50%, -50%) rotate(60deg);\n  }\n  25% {\n    transform: translate(-50%, -50%) rotate(90deg);\n  }\n  32% {\n    transform: translate(-50%, -50%) rotate(90deg);\n  }\n  33% {\n    transform: translate(-50%, -50%) rotate(120deg);\n  }\n  40% {\n    transform: translate(-50%, -50%) rotate(120deg);\n  }\n  41% {\n    transform: translate(-50%, -50%) rotate(150deg);\n  }\n  49% {\n    transform: translate(-50%, -50%) rotate(150deg);\n  }\n  50% {\n    transform: translate(-50%, -50%) rotate(180deg);\n  }\n  57% {\n    transform: translate(-50%, -50%) rotate(180deg);\n  }\n  58% {\n    transform: translate(-50%, -50%) rotate(210deg);\n  }\n  65% {\n    transform: translate(-50%, -50%) rotate(210deg);\n  }\n  66% {\n    transform: translate(-50%, -50%) rotate(240deg);\n  }\n  74% {\n    transform: translate(-50%, -50%) rotate(240deg);\n  }\n  75% {\n    transform: translate(-50%, -50%) rotate(270deg);\n  }\n  82% {\n    transform: translate(-50%, -50%) rotate(270deg);\n  }\n  83% {\n    transform: translate(-50%, -50%) rotate(300deg);\n  }\n  90% {\n    transform: translate(-50%, -50%) rotate(300deg);\n  }\n  91% {\n    transform: translate(-50%, -50%) rotate(330deg);\n  }\n  99% {\n    transform: translate(-50%, -50%) rotate(330deg);\n  }\n  100% {\n    transform: translate(-50%, -50%) rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/styles/_constants-darkmatter.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/************************************************** DARKMATTER THEME*/\n// Fonts\n@import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap'); // Header font, Roboto Condensed. This is an alternative to the DIN Alt font, which is not available on Google Fonts.\n@import url('https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&display=swap'); // Body Font, Exo\n@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch&family=Roboto+Condensed&display=swap'); // Temporary numeric font, Chakra Petch (need to import local font instead).\n$heroFont: 'Teko', sans-serif;\n$headerFont: 'Cabin Condensed', sans-serif;\n$bodyFont: 'Exo', sans-serif;\n$numericFont: 'Chakra Petch', sans-serif;\n\n@mixin heroFont($size: 1em) {\n  font-family: $heroFont;\n  font-size: $size;\n}\n\n@mixin headerFont($size: 1em) {\n  font-family: $headerFont;\n  font-size: $size;\n}\n\n@mixin bodyFont($size: 1em) {\n  font-family: $bodyFont;\n  font-size: $size;\n}\n\n@mixin numericFont($size: 1em) {\n  font-family: $numericFont;\n  font-size: $size;\n}\n\n@mixin discreteItem() {\n  background: $colorDiscreteItemBg;\n  border: none;\n  border-radius: $controlCr;\n\n  .c-input-inline:hover {\n    background: $colorBodyBg;\n  }\n\n  &--current-match {\n    background: $colorDiscreteItemCurrentBg;\n  }\n}\n\n@mixin discreteItemInnerElem() {\n  border: 1px solid rgba(#fff, 0.1);\n  border-radius: $controlCr;\n}\n\n@mixin themedButton($c: $colorBtnBg) {\n  background: radial-gradient(rgba($c, 1), rgba($c, 0.7));\n  box-shadow: rgba(black, 0.5) 0 0.5px 2px;\n}\n\n@mixin telemetryView() {\n  border: 1px solid $colorBodyFg;\n  border-radius: $controlCr;\n}\n\n@mixin browseFrameBorder() {\n  // Used on main object container to add highlighted corners to non-hidden frames.\n  border-image: radial-gradient(circle, #575757, #6c6c6c, #818181, #979797, #aeaeae);\n  border-style: solid;\n  padding: 10px;\n  $browseFrameCornerWidth: 4px;\n  background:\n    linear-gradient(to right, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 0 0,\n    linear-gradient(to right, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 0 100%,\n    linear-gradient(to left, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 100% 0,\n    linear-gradient(to left, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 100% 100%,\n    linear-gradient(to bottom, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 0 0,\n    linear-gradient(to bottom, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 100% 0,\n    linear-gradient(to top, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 0 100%,\n    linear-gradient(to top, $browseFrameCornerColor, transparent $browseFrameCornerWidth) 100% 100%,\n    rgb(0, 0, 0, 0.4);\n\n  background-repeat: no-repeat;\n  background-size: 35px 35px;\n  border-radius: $interiorMarginLg;\n  box-shadow: 0px 0px 20px 2px rgb(140 140 140 / 20%);\n}\n\n// Functions\n@function buttonBg($c: $colorBtnBg) {\n  @return radial-gradient(rgba($colorBodyBg, 1), rgba($colorBodyBg, 0.6));\n}\n\n@function pullForward($val, $amt) {\n  @return lighten($val, $amt);\n}\n\n@function pushBack($val, $amt) {\n  @return darken($val, $amt);\n}\n\n/**************************************************** CONSTANTS */\n$fontBaseSize: 12px;\n$smallCr: 2px;\n$controlCr: 3px;\n$basicCr: 4px;\n$shdwBtns: rgba(black, 0.2) 0 1px 2px;\n$shdwBtnsOverlay: rgba(black, 0.5) 0 1px 5px;\n\n// Base colors\n$colorBodyBg: #17171b;\n$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);\n$colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);\n$colorBodyFg: #aaaaaa;\n$colorBodyFgSubtle: #9c9c9c;\n$colorBodyFgEm: #fff;\n$colorGenBg: #222;\n$colorHeadBg: rgba($colorBodyBg, 0.5);\n$colorHeadFg: $colorBodyFg;\n$colorKey: #1c67e3;\n$colorKeyBg: #015fca;\n$colorKeyFg: #fff; // Darker version of colorKey for use in major buttons\n$colorKeyHov: lighten($colorKey, 10%);\n$colorKeyBgHov: lighten($colorKeyBg, 10%);\n$colorKeyFilter: invert(36%) sepia(10%) saturate(2512%) hue-rotate(170deg) brightness(100%)\n  contrast(200%);\n$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%)\n  contrast(100%);\n$colorKeySelectedBg: $colorKey;\n$uiColor: #0093ff; // Resize bars, splitter bars, etc.\n$colorInteriorBorder: rgba($colorBodyFg, 0.2);\n$colorInteriorBorderNotebook: rgba($colorBodyFg, 0.5);\n$colorA: #ccc;\n$colorAHov: #fff;\n$filterHov: brightness(1.3) contrast(1.5); // Tree, location items\n$filterHovSubtle: brightness(1.2) contrast(1.2);\n$colorSelectedBg: rgba($colorKey, 0.3);\n$colorSelectedFg: pullForward($colorBodyFg, 20%);\n\n// Body constants\n$bodyBg: $colorBodyBg url('../ui/layout/assets/images/darkmatter-bg.png') no-repeat center 85%; // Reference: https://science.nasa.gov/wp-content/uploads/2023/08/s2-1280.jpg?w=4096&format=webp\n$bodyBgSize: cover;\n$bodySize: 100vh;\n\n// Object labels\n$objectLabelTypeIconOpacity: 0.9;\n$objectLabelNameColorFg: $colorBodyFgEm;\n\n// Layout\n$shellMainPad: 4px 0;\n$shellPanePad: $interiorMargin, 7px;\n$drawerBg: lighten($colorBodyBg, 5%);\n$drawerFg: lighten($colorBodyFg, 5%);\n$sideBarBg: $drawerBg;\n$sideBarHeaderBg: rgba($colorBodyFg, 0.1);\n$sideBarHeaderFg: rgba($colorBodyFg, 0.7);\n\n// Status colors, mainly used for messaging and item ancillary symbols\n$colorStatusFg: #888;\n$colorStatusDefault: #ccc;\n$colorStatusInfo: #60ba7b;\n$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%)\n  contrast(92%);\n$colorStatusAlert: #ffb66c;\n$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%)\n  contrast(101%);\n$colorStatusError: #da0004;\n$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%)\n  contrast(115%);\n$colorStatusBtnBg: #666; // Where is this used?\n$colorStatusPartialBg: #3f5e8b;\n$colorStatusCompleteBg: #457638;\n$colorAlert: #ff8a0d;\n$colorAlertFg: #fff;\n$colorError: #ff3c00;\n$colorErrorFg: #fff;\n$colorWarningHi: #990000;\n$colorWarningHiFg: #ff9594;\n$colorWarningLo: #ff9900;\n$colorWarningLoFg: #523400;\n$colorDiagnostic: #a4b442;\n$colorDiagnosticFg: #39461a;\n$colorCommand: #3693bd;\n$colorCommandFg: #fff;\n$colorInfo: #2294a2;\n$colorInfoFg: #fff;\n$colorOk: #33cc33;\n$colorOkFg: #fff;\n$colorFilterBg: #44449c;\n$colorFilterFg: #8984e9;\n$colorFilter: $colorFilterFg; // Standalone against $colorBodyBg\n\n// States\n$colorPausedBg: #ff9900;\n$colorPausedFg: #333;\n\n// Time Colors\n$colorTimeCommonFg: #eee;\n$colorTimeFixed: #59554c;\n$colorTimeFixedBg: $colorTimeFixed;\n$colorTimeFixedFg: #eee;\n$colorTimeFixedFgSubtle: #b2aa98;\n$colorTimeFixedHov: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);\n$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;\n$colorTimeFixedBtnBgMajor: #a09375;\n$colorTimeFixedBtnFgMajor: #fff;\n$colorTimeRealtime: #445890;\n$colorTimeRealtimeBg: $colorTimeRealtime;\n$colorTimeRealtimeFg: #eee;\n$colorTimeRealtimeFgSubtle: #88b0ff;\n$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);\n$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;\n$colorTimeRealtimeBtnBgMajor: #588ffa;\n$colorTimeRealtimeBtnFgMajor: #fff;\n$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor\n$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov\n$timeConductorAxisHoverFilter: brightness(1.2);\n$timeConductorActiveBg: $colorKey;\n$timeConductorActivePanBg: #226074;\n\n// Browse\n$browseFrameColor: pullForward($colorBodyBg, 10%);\n$browseFrameBorder: 1px solid rgb(89, 89, 89, 0.4); // Frames in Disp and Flex Layouts when frame is showing\n$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;\n$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);\n$filterItemHoverFg: brightness(1.2) contrast(1.1);\n$interiorMarginObjectFrameVertical: 10px;\n$interiorMarginObjectFrameHorizontal: 10px;\n\n// Missing Items\n$filterItemMissing: brightness(0.6) grayscale(1);\n$opacityMissing: 0.5;\n$borderMissing: 1px dashed $colorAlert !important;\n$browseFrameCornerColor: $colorKey 4px; //Color used for the corners of frames\n\n// Edit\n$editUIColor: $uiColor; // Base color\n$editUIColorBg: $editUIColor;\n$editUIColorFg: #fff;\n$editUIColorHov: pullForward(\n  saturate($uiColor, 10%),\n  10%\n); // Hover color when $editUIColor is applied as a base color\n$editUIBaseColor: #344b8d; // Base color, toolbar bg\n$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);\n$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.\n$editUIAreaBaseColor: pullForward(saturate($editUIBaseColor, 30%), 20%);\n$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area\n$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area\n$editDimensionsColor: #6a5ea6;\n$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover\n$editFrameBorder: 1px dotted $editFrameColor;\n$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects\n$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames\n$editFrameColorSelected: #ffefc2; // Border of selected frames while editing\n$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout\n$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color\n$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;\n$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color\n$editFrameMovebarColorFg: pullForward(\n  $editFrameMovebarColorBg,\n  20%\n); // Grippy lines, container size text\n$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style\n$editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%);\n$editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style\n$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);\n$editFrameMovebarH: 10px; // Height of move bar in layout frame\n$editMarqueeBorder: 1px dashed $editFrameColorSelected;\n$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element\n\n// Icons\n$colorIconAlias: #4af6f3;\n$colorIconAliasForKeyFilter: #aaa;\n\n// Holders\n$colorTabsHolderBg: rgba(black, 0.2);\n\n// Buttons and Controls\n$colorBtnBg: pullForward($colorBodyBg, 20%);\n$colorBtnBgHov: pullForward($colorBtnBg, 10%);\n$shdwBtnHov: inset rgba(white, 10%) 0 0 0 100px;\n$colorBtnFg: pullForward($colorBodyFg, 100%);\n$colorBtnReverseFg: pullForward($colorBtnFg, 10%);\n$colorBtnReverseBg: pullForward($colorBtnBg, 10%);\n$colorBtnFgHov: $colorBtnFg;\n$colorBtnMajorBg: $colorKey;\n$colorBtnMajorBgHov: $colorKeyHov;\n$colorBtnMajorFg: $colorKeyFg;\n$colorBtnMajorFgHov: pushBack($colorBtnMajorFg, 10%);\n$colorBtnCautionBg: $colorStatusAlert;\n$colorBtnCautionBgHov: #f1504e;\n$colorBtnCautionFg: $colorBtnBg;\n$colorBtnActiveBg: $colorOk;\n$colorBtnActiveFg: $colorOkFg;\n$colorBtnSelectedBg: $colorSelectedBg;\n$colorBtnSelectedFg: $colorSelectedFg;\n$colorClickIconButton: $colorKey;\n$colorClickIconButtonBgHov: rgba($colorKey, 0.3);\n$colorClickIconButtonFgHov: $colorKeyHov;\n$colorDropHint: $colorKey;\n$colorDropHintBg: pushBack($colorDropHint, 10%);\n$colorDropHintBgHov: $colorDropHint;\n$colorDropHintFg: pullForward($colorDropHint, 40%);\n$colorDisclosureCtrl: rgba($colorBodyFg, 0.5);\n$colorDisclosureCtrlHov: rgba($colorBodyFg, 0.7);\n$btnStdH: 24px;\n$colorCursorGuide: rgba(white, 0.6);\n$shdwCursorGuide: rgba(black, 0.4) 0 0 2px;\n$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);\n$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects\n$colorSelectFg: $colorBtnFg;\n$colorSelectArw: lighten($colorBtnBg, 20%);\n$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;\n$controlDisabledOpacity: 0.2;\n\n// Menus\n$colorMenuBg: rgba($colorBodyBg, 0.6);\n$colorMenuFg: $colorBodyFg;\n$colorMenuIc: $colorKey;\n$filterMenu: brightness(1.4);\n$colorMenuHovBg: rgba($colorKey, 0.5);\n$colorMenuHovFg: $colorBodyFgEm;\n$colorMenuHovIc: $colorMenuHovFg;\n$colorMenuElementHilite: pullForward($colorMenuBg, 10%);\n$shdwMenu: rgba(black, 0.8) 0 2px 10px;\n$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);\n$shdwMenuText: none;\n$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);\n\n// Palettes and Swatches\n$paletteItemBorderOuterColorSelected: black;\n$paletteItemBorderInnerColorSelected: white;\n$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);\n$mixedSettingBg: (transparent rgba($editUIBaseColorHov, 0.7)); // Used in .c-click-icon--mixed\n$mixedSettingBgSize: 5px;\n\n// Forms\n$colorCheck: $colorKey;\n$colorFormRequired: $colorKey;\n$colorFormValid: $colorOk;\n$colorFormError: #990000;\n$colorFormInvalid: #ff2200;\n$colorFormFieldErrorBg: $colorFormError;\n$colorFormFieldErrorFg: rgba(#fff, 0.6);\n$colorFormLines: rgba(#000, 0.2);\n$colorFormSectionHeaderBg: rgba(#000, 0.1);\n$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);\n$colorInputBg: rgba(rgb(70, 70, 70), 0.3);\n$colorInputBgHov: rgba(black, 0.1);\n$colorInputFg: $colorBodyFg;\n$colorFormText: pushBack($colorBodyFg, 10%);\n$colorInputIcon: pushBack($colorBodyFg, 25%);\n$colorFieldHint: pullForward($colorBodyFg, 40%);\n$shdwInput: inset rgba(black, 0.4) 0 0 1px;\n$shdwInputHov: inset rgba(black, 0.7) 0 0 2px;\n$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;\n$formTBPad: $interiorMargin;\n$formLRPad: $interiorMargin;\n$formInputH: 22px;\n$formRowCtrlsH: 14px;\n\n// Inspector\n$colorInspectorBg: pullForward($colorBodyBg, 5%);\n$colorInspectorFg: $colorBodyFg;\n$colorInspectorPropName: $colorBodyFg;\n$colorInspectorPropVal: pullForward($colorInspectorFg, 15%);\n$colorInspectorSectionHeaderBg: rgba($colorBodyBg, 0.75);\n$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);\n\n// Tabs\n$colorTabBg: $colorBodyBg;\n$colorTabFg: $colorBodyFgEm;\n$colorTabCurrentBg: rgba($colorKey, 0.71);\n$colorTabCurrentFg: $colorBodyFgEm;\n$colorTabsBaseline: $colorBodyBg;\n\n// Overlay\n$colorOvrBlocker: rgba(black, 0.8);\n$overlayCr: $interiorMargin;\n\n// Indicator colors\n$colorIndicatorAvailable: $colorKey;\n$colorIndicatorDisabled: #555555;\n$colorIndicatorOn: $colorOk;\n$colorIndicatorOff: #777777;\n$colorIndicatorBgHov: rgba($colorHeadFg, 0.1);\n$colorIndicatorMenuBg: $colorHeadBg;\n$colorIndicatorMenuBgShdw: rgba(white, 0.6) 0 0 6px;\n$colorIndicatorMenuFg: $colorHeadFg;\n$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);\n\n// Staleness\n$colorTelemStale: cyan;\n$colorTelemStaleFg: #002a2a;\n$styleTelemStale: italic;\n\n// Limits\n$colorLimitYellowBg: #b18b05;\n$colorLimitYellowFg: #feeeb5;\n$colorLimitYellowIc: #fdc707;\n$colorLimitOrangeBg: #b36b00;\n$colorLimitOrangeFg: #ffe0b2;\n$colorLimitOrangeIc: #ff9900;\n$colorLimitRedBg: #b60109;\n$colorLimitRedFg: #ffa489;\n$colorLimitRedIc: #ff4222;\n$colorLimitPurpleBg: #891bb3;\n$colorLimitPurpleFg: #edbeff;\n$colorLimitPurpleIc: #c327ff;\n$colorLimitCyanBg: #4ba6b3;\n$colorLimitCyanFg: #d3faff;\n$colorLimitCyanIc: #6bedff;\n\n// Events\n$colorEventPurpleFg: #ab8fff;\n$colorEventRedFg: #ff9999;\n$colorEventOrangeFg: #ff8800;\n$colorEventYellowFg: #ffdb63;\n$colorEventPurpleBg: #31204a;\n$colorEventRedBg: #3c1616;\n$colorEventOrangeBg: #3e2a13;\n$colorEventYellowBg: #3e3316;\n$colorEventPurpleLine: #9e36ff;\n$colorEventRedLine: #ff2525;\n$colorEventOrangeLine: #ff8800;\n$colorEventYellowLine: #fdce22;\n\n// Bubble colors\n$colorInfoBubbleBg: #dddddd;\n$colorInfoBubbleFg: #666;\n$colorThumbsBubbleFg: pullForward($colorBodyFg, 10%);\n$colorThumbsBubbleBg: pullForward($colorBodyBg, 10%);\n\n// Items\n$colorSelectableItemBg: transparent;\n$colorSelectableItemBgHov: rgba(#fff, 0.1);\n$colorItemBg: buttonBg($colorBtnBg);\n$colorItemBgHov: $colorSelectableItemBgHov;\n$colorListItemBg: transparent;\n$colorListItemBgHov: rgba($colorKey, 0.1);\n$colorItemFg: $colorBtnFg;\n$colorItemFgDetails: $colorBodyFgSubtle;\n$shdwItemText: none;\n\n// Tabular\n$colorTabBorder: pullForward($colorBodyBg, 10%);\n$colorTabBodyBg: $colorBodyBg;\n$colorTabBodyFg: pullForward($colorBodyFg, 20%);\n$colorTabHeaderBg: #575757;\n$colorTabHeaderFg: $colorBodyFg;\n$colorTabHeaderBorder: $colorBodyBg;\n$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);\n$colorTabGroupHeaderFg: pushBack($colorTabHeaderFg, 10%);\n$colorSummaryBg: #2c2c2c;\n$colorSummaryFg: rgba($colorBodyFg, 0.7);\n$colorSummaryFgEm: $colorBodyFg;\n\n// Plot\n$colorPlotBg: rgba(black, 0.1);\n$colorPlotFg: $colorBodyFg;\n$colorPlotHash: $colorPlotFg;\n$opacityPlotHash: 0.2;\n$stylePlotHash: dashed;\n$colorPlotAreaBorder: $colorInteriorBorder;\n$colorPlotLabelFg: pushBack($colorPlotFg, 20%);\n$legendHoverValueBg: rgba($colorBodyFg, 0.2);\n$legendTableHeadBg: $colorTabHeaderBg;\n$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);\n\n// Gauges\n$colorGaugeBase: $colorKeyBg;\n$colorGaugeBg: rgba($colorGaugeBase, 0.35); // Gauge radial area background, meter background\n$colorGaugeValue: $colorKeyBg; // Gauge value graphic (radial sweep, bar) color\n$colorGaugeTextValue: #fff; // Radial gauge text value\n$colorGaugeMeterTextValue: #fff; // Meter text value, overlaid on value bar\n$colorGaugeRange: $colorBodyFg; // Range text color\n$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.5);\n$colorGaugeLimitLow: $colorGaugeLimitHigh;\n$colorGaugeNeedle: $colorGaugeBase; // Color of needle in a needle gauge.\n$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions\n$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges\n$gaugeMeterValueShadow: rgba(255, 255, 255, 0.5);\n\n// Time Strip and Lists\n$colorPastBg: #343434;\n$colorPastFg: white;\n$colorPastFgEm: $colorPastFg;\n$colorCurrentBg: #666666;\n$colorCurrentFg: white;\n$colorCurrentFgEm: $colorCurrentFg;\n$colorCurrentBorder: $colorBodyBg;\n$colorFutureBg: #494949;\n$colorFutureFg: $colorCurrentFg;\n$colorFutureFgEm: $colorFutureFg;\n$colorFutureBorder: $colorCurrentBorder;\n$colorInProgressBg: $colorTimeRealtimeBg;\n$colorInProgressFg: $colorTimeRealtimeFgSubtle;\n$colorInProgressFgEm: $colorTimeRealtimeFg;\n$colorGanttSelectedBorder: rgba(#fff, 0.3);\n$colorActivityStatusGreen: #4bad4b;\n$colorActivityStatusOrange: #b67e41;\n$opacitySubtle: 0.4;\n$colorEventLine: $colorBodyFg;\n$colorEventLineExtended: rgba($colorEventLine, 0.3);\n$colorTimeStripDraftBg: rgba(#a57748, 0.2);\n$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);\n$colorABAhead: #33c56e;\n$colorABBehind: #ffa500;\n$eventLineW: 1px;\n\n// Tree\n$colorTreeBg: transparent;\n$colorItemTreeHoverBg: $colorSelectableItemBgHov;\n$colorItemTreeHoverFg: #fff;\n$colorItemTreeIcon: $colorKey;\n$colorItemTreeIconHover: $colorItemTreeIcon;\n$colorItemTreeFg: #ccc;\n$colorItemTreeSelectedBg: $colorSelectedBg;\n$colorItemTreeSelectedFg: $colorItemTreeHoverFg;\n$filterItemTreeSelected: $filterHov;\n$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;\n$colorItemTreeEditingBg: pushBack($editUIColor, 20%);\n$colorItemTreeEditingFg: $editUIColor;\n$colorItemTreeEditingIcon: $editUIColor;\n$colorItemTreeVC: $colorDisclosureCtrl;\n$colorItemTreeVCHover: $colorDisclosureCtrlHov;\n$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);\n$shdwItemTreeIcon: none;\n\n// Layout frame controls\n$frameControlsColorFg: white;\n$frameControlsColorBg: $colorKey;\n$frameControlsShdw: $shdwMenu;\n\n// Images\n$colorThumbHoverBg: $colorItemTreeHoverBg;\n\n// Scrollbar\n$scrollbarTrackSize: 7px;\n$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;\n$scrollbarTrackColorBg: rgba(#000, 0.2);\n$scrollbarThumbColor: pushBack($colorBodyBg, 50%);\n$scrollbarThumbColorHov: $colorKey;\n$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);\n$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);\n\n// Splitter\n$splitterHandleD: 2px;\n$splitterD: $splitterHandleD;\n$splitterHandleHitMargin: 4px;\n$colorSplitterBaseBg: $colorBodyBg;\n$colorSplitterBg: pullForward($colorBodyBg, 10%);\n$colorSplitterFg: $colorBodyBg;\n$colorSplitterHover: $uiColor;\n$colorSplitterActive: $colorKey;\n$splitterBtnD: (16px, 35px); // height, width\n$splitterBtnColorBg: $colorBtnBg;\n$splitterBtnColorFg: #999;\n$splitterBtnLabelColorFg: #9d9d9d;\n$splitterCollapsedBtnColorBg: #222;\n$splitterCollapsedBtnColorFg: #555;\n$splitterCollapsedBtnColorBgHov: $colorKey;\n$splitterCollapsedBtnColorFgHov: $colorKeyFg;\n\n// Mobile\n$colorMobilePaneLeft: pushBack($colorBodyBg, 2%);\n$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);\n$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;\n$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);\n\n// About Screen\n$colorAboutLink: #9bb5ff;\n\n// Loading\n$colorLoadingFg: #776ba2;\n$colorLoadingBg: rgba($colorLoadingFg, 0.1);\n\n// Transitions\n$transInTime: 50ms;\n$transOutTime: 250ms;\n$transIn: all $transInTime ease-in-out;\n$transOut: all $transOutTime ease-in-out;\n$transInTransform: transform $transInTime ease-in-out;\n$transOutTransform: transform $transOutTime ease-in-out;\n$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5);\n$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3);\n\n// Discrete items\n$createBtnTextTransform: uppercase;\n$colorDiscreteItemBg: rgba($colorBodyFg, 0.1);\n$colorDiscreteItemBgHov: rgba($colorBodyFg, 0.2);\n$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3);\n$scrollContainer: $colorBodyBg;\n"
  },
  {
    "path": "src/styles/_constants-espresso.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/************************************************** ESPRESSO THEME */\n// Fonts\n$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n$headerFont: $heroFont;\n$bodyFont: $heroFont;\n$numericFont: $heroFont;\n\n@mixin heroFont($size: 1em) {\n  font-family: $heroFont;\n  font-size: $size;\n}\n\n@mixin headerFont($size: 1em) {\n  font-family: $headerFont;\n  font-size: $size;\n}\n\n@mixin bodyFont($size: 1em) {\n  font-family: $bodyFont;\n  font-size: $size;\n}\n\n@mixin discreteItem() {\n  background: $colorDiscreteItemBg;\n  border: none;\n  border-radius: $controlCr;\n\n  .c-input-inline:hover {\n    background: $colorBodyBg;\n  }\n\n  &--current-match {\n    background: $colorDiscreteItemCurrentBg;\n  }\n}\n\n@mixin discreteItemInnerElem() {\n  border: 1px solid rgba(#fff, 0.1);\n  border-radius: $controlCr;\n}\n\n@mixin themedButton($c: $colorBtnBg) {\n  background: linear-gradient(pullForward($c, 5%), $c);\n  box-shadow: rgba(black, 0.5) 0 0.5px 2px;\n}\n\n@mixin telemetryView() {\n}\n@mixin browseFrameBorder() {\n}\n\n// Functions\n@function buttonBg($c: $colorBtnBg) {\n  @return linear-gradient(lighten($c, 5%), $c);\n}\n\n@function pullForward($val, $amt) {\n  @return lighten($val, $amt);\n}\n\n@function pushBack($val, $amt) {\n  @return darken($val, $amt);\n}\n\n/**************************************************** CONSTANTS */\n$fontBaseSize: 12px;\n$smallCr: 2px;\n$controlCr: 3px;\n$basicCr: 4px;\n$shdwBtns: rgba(black, 0.2) 0 1px 2px;\n$shdwBtnsOverlay: rgba(black, 0.5) 0 1px 5px;\n\n// Base colors\n$colorBodyBg: #2c2c2c;\n$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);\n$colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);\n$colorBodyFg: #acacac;\n$colorBodyFgSubtle: #9c9c9c;\n$colorBodyFgEm: #fff;\n$colorGenBg: #222;\n$colorHeadBg: #000;\n$colorHeadFg: $colorBodyFg;\n$colorKey: #03ace4;\n$colorKeyBg: #007fad; // Darker version of colorKey for use in major buttons\n$colorKeyFg: #fff;\n$colorKeyHov: lighten($colorKey, 10%);\n$colorKeyBgHov: lighten($colorKeyBg, 10%);\n$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%)\n  contrast(101%);\n$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%)\n  contrast(100%);\n$colorKeySelectedBg: $colorKeyBg;\n$colorKeySubtle: pushBack($colorKey, 10%);\n$uiColor: #0093ff; // Resize bars, splitter bars, etc.\n$colorInteriorBorder: rgba($colorBodyFg, 0.2);\n$colorInteriorBorderNotebook: rgba($colorBodyFg, 0.2);\n$colorA: #ccc;\n$colorAHov: #fff;\n$filterHov: brightness(1.3) contrast(1.5); // Tree, location items\n$filterHovSubtle: brightness(1.2) contrast(1.2);\n$colorSelectedBg: rgba($colorKey, 0.3);\n$colorSelectedFg: pullForward($colorBodyFg, 20%);\n\n// Body constants\n$bodyBg: $colorBodyBg;\n$bodyBgSize: cover;\n$bodySize: 100%;\n\n// Object labels\n$objectLabelTypeIconOpacity: 0.8; //JOHN\n$objectLabelNameColorFg: lighten($colorBodyFg, 20%);\n\n// Layout\n$shellMainPad: 4px 0;\n$shellPanePad: $interiorMargin, 7px;\n$drawerBg: lighten($colorBodyBg, 5%);\n$drawerFg: lighten($colorBodyFg, 5%);\n$sideBarBg: $drawerBg;\n$sideBarHeaderBg: rgba($colorBodyFg, 0.1);\n$sideBarHeaderFg: rgba($colorBodyFg, 0.7);\n\n// Status colors, mainly used for messaging and item ancillary symbols\n$colorStatusFg: #888;\n$colorStatusDefault: #ccc;\n$colorStatusInfo: #60ba7b;\n$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%)\n  contrast(92%);\n$colorStatusAlert: #ffb66c;\n$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%)\n  contrast(101%);\n$colorStatusError: #da0004;\n$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%)\n  contrast(115%);\n$colorStatusBtnBg: #666; // Where is this used?\n$colorStatusPartialBg: #3f5e8b;\n$colorStatusCompleteBg: #457638;\n$colorAlert: #ff8a0d;\n$colorAlertFg: #fff;\n$colorError: #ff3c00;\n$colorErrorFg: #fff;\n$colorWarningHi: #990000;\n$colorWarningHiFg: #ff9594;\n$colorWarningLo: #ff9900;\n$colorWarningLoFg: #523400;\n$colorDiagnostic: #a4b442;\n$colorDiagnosticFg: #39461a;\n$colorCommand: #3693bd;\n$colorCommandFg: #fff;\n$colorInfo: #198290;\n$colorInfoFg: #fff;\n$colorOk: #33cc33;\n$colorOkFg: #fff;\n$colorFilterBg: #44449c;\n$colorFilterFg: #8984e9;\n$colorFilter: $colorFilterFg; // Standalone against $colorBodyBg\n\n// States\n$colorPausedBg: #ff9900;\n$colorPausedFg: #333;\n\n// Time Colors\n$colorTimeCommonFg: #eee;\n$colorTimeFixed: #59554c;\n$colorTimeFixedBg: $colorTimeFixed;\n$colorTimeFixedFg: #eee;\n$colorTimeFixedFgSubtle: #b2aa98;\n$colorTimeFixedHov: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);\n$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;\n$colorTimeFixedBtnBgMajor: #a09375;\n$colorTimeFixedBtnFgMajor: #fff;\n$colorTimeRealtime: #445890;\n$colorTimeRealtimeBg: $colorTimeRealtime;\n$colorTimeRealtimeFg: #eee;\n$colorTimeRealtimeFgSubtle: #88b0ff;\n$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);\n$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;\n$colorTimeRealtimeBtnBgMajor: #588ffa;\n$colorTimeRealtimeBtnFgMajor: #fff;\n$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor\n$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov\n$timeConductorAxisHoverFilter: brightness(1.2);\n$timeConductorActiveBg: $colorKey;\n$timeConductorActivePanBg: #226074;\n\n// Browse\n$browseFrameColor: pullForward($colorBodyBg, 10%);\n$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing\n$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;\n$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);\n$filterItemHoverFg: brightness(1.2) contrast(1.1);\n$interiorMarginObjectFrameVertical: 0px;\n$interiorMarginObjectFrameHorizontal: 3px;\n\n// Missing Items\n$filterItemMissing: brightness(0.6) grayscale(1);\n$opacityMissing: 0.5;\n$borderMissing: 1px dashed $colorAlert !important;\n\n// Edit\n$editUIColor: $uiColor; // Base color\n$editUIColorBg: $editUIColor;\n$editUIColorFg: #fff;\n$editUIColorHov: #ffffff; // Hover color when $editUIColor is applied as a base color\n$editUIBaseColor: #344b8d; // Base color, toolbar bg\n$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);\n$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.\n$editUIAreaBaseColor: $editUIColor; //pullForward(saturate($editUIBaseColor, 30%), 20%);\n$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area\n$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area\n$editDimensionsColor: #6a5ea6;\n$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover\n$editFrameBorder: 1px dotted $editFrameColor;\n$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects\n$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames\n$editFrameColorSelected: #ffefc2; // Border of selected frames while editing\n$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout\n$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color\n$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;\n$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color\n$editFrameMovebarColorFg: pullForward(\n  $editFrameMovebarColorBg,\n  20%\n); // Grippy lines, container size text\n$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style\n$editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%);\n$editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style\n$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);\n$editFrameMovebarH: 10px; // Height of move bar in layout frame\n$editMarqueeBorder: 1px dashed $editFrameColorSelected;\n$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element\n\n// Icons\n$colorIconAlias: #4af6f3;\n$colorIconAliasForKeyFilter: #aaa;\n\n// Holders\n$colorTabsHolderBg: rgba(black, 0.2);\n\n// Buttons and Controls\n$colorBtnBg: pullForward($colorBodyBg, 10%);\n$colorBtnBgHov: pullForward($colorBtnBg, 10%);\n$shdwBtnHov: inset rgba(white, 10%) 0 0 0 100px;\n$colorBtnFg: pullForward($colorBodyFg, 10%);\n$colorBtnReverseFg: pullForward($colorBtnFg, 10%);\n$colorBtnReverseBg: pullForward($colorBtnBg, 10%);\n$colorBtnFgHov: $colorBtnFg;\n$colorBtnMajorBg: $colorKeyBg;\n$colorBtnMajorBgHov: $colorKeyBgHov;\n$colorBtnMajorFg: $colorKeyFg;\n$colorBtnMajorFgHov: pushBack($colorBtnMajorFg, 10%);\n$colorBtnCautionBg: $colorStatusAlert;\n$colorBtnCautionBgHov: #f1504e;\n$colorBtnCautionFg: $colorBtnBg;\n$colorBtnActiveBg: $colorOk;\n$colorBtnActiveFg: $colorOkFg;\n$colorBtnSelectedBg: $colorSelectedBg;\n$colorBtnSelectedFg: $colorSelectedFg;\n$colorClickIconButton: $colorKey;\n$colorClickIconButtonBgHov: rgba($colorKey, 0.3);\n$colorClickIconButtonFgHov: $colorKeyHov;\n$colorDropHint: $colorKey;\n$colorDropHintBg: pushBack($colorDropHint, 10%);\n$colorDropHintBgHov: $colorDropHint;\n$colorDropHintFg: pullForward($colorDropHint, 40%);\n$colorDisclosureCtrl: rgba($colorBodyFg, 0.5);\n$colorDisclosureCtrlHov: rgba($colorBodyFg, 0.7);\n$btnStdH: 24px;\n$colorCursorGuide: rgba(white, 0.6);\n$shdwCursorGuide: rgba(black, 0.4) 0 0 2px;\n$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);\n$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects\n$colorSelectFg: $colorBtnFg;\n$colorSelectArw: #777777; // This must be a solid color, not a gradient, due to usage of SVG bg in selects\n$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;\n$controlDisabledOpacity: 0.2;\n\n// Menus\n$colorMenuBg: $colorBodyBg;\n$colorMenuFg: $colorBodyFg;\n$colorMenuIc: $colorKey;\n$filterMenu: brightness(1.4);\n$colorMenuHovBg: rgba($colorKey, 0.5);\n$colorMenuHovFg: $colorBodyFgEm;\n$colorMenuHovIc: $colorMenuHovFg;\n$colorMenuElementHilite: pullForward($colorMenuBg, 10%);\n$shdwMenu: rgba(black, 0.8) 0 2px 10px;\n$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);\n$shdwMenuText: none;\n$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);\n\n// Palettes and Swatches\n$paletteItemBorderOuterColorSelected: black;\n$paletteItemBorderInnerColorSelected: white;\n$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);\n$mixedSettingBg: (transparent rgba($editUIBaseColorHov, 0.7)); // Used in .c-click-icon--mixed\n$mixedSettingBgSize: 5px;\n\n// Forms\n$colorCheck: $colorKey;\n$colorFormRequired: $colorKey;\n$colorFormValid: $colorOk;\n$colorFormError: #990000;\n$colorFormInvalid: #ff2200;\n$colorFormFieldErrorBg: $colorFormError;\n$colorFormFieldErrorFg: rgba(#fff, 0.6);\n$colorFormLines: rgba(#000, 0.2);\n$colorFormSectionHeaderBg: rgba(#000, 0.1);\n$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);\n$colorInputBg: rgba(black, 0.2);\n$colorInputBgHov: rgba(black, 0.1);\n$colorInputFg: $colorBodyFg;\n$colorFormText: pushBack($colorBodyFg, 10%);\n$colorInputIcon: pushBack($colorBodyFg, 25%);\n$colorFieldHint: pullForward($colorBodyFg, 40%);\n$shdwInput: inset rgba(black, 0.4) 0 0 1px;\n$shdwInputHov: inset rgba(black, 0.7) 0 0 2px;\n$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;\n$formTBPad: $interiorMargin;\n$formLRPad: $interiorMargin;\n$formInputH: 22px;\n$formRowCtrlsH: 14px;\n\n// Inspector\n$colorInspectorBg: $colorBodyBg;\n$colorInspectorFg: $colorBodyFg;\n$colorInspectorPropName: $colorBodyFgSubtle;\n$colorInspectorPropVal: $colorBodyFgEm;\n$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 10%);\n$colorInspectorSectionHeaderFg: #bfbfbf;\n\n// Tabs\n$colorTabBg: pullForward($colorBodyBg, 5%);\n$colorTabFg: pullForward($colorBodyFg, 0%);\n$colorTabCurrentBg: pullForward($colorTabBg, 10%);\n$colorTabCurrentFg: pullForward($colorTabFg, 20%);\n$colorTabsBaseline: $colorTabCurrentBg;\n\n// Overlay\n$colorOvrBlocker: rgba(black, 0.7);\n$overlayCr: $interiorMargin;\n\n// Indicator colors\n$colorIndicatorAvailable: $colorKey;\n$colorIndicatorDisabled: #555555;\n$colorIndicatorOn: $colorOk;\n$colorIndicatorOff: #777777;\n$colorIndicatorBgHov: rgba($colorHeadFg, 0.1);\n$colorIndicatorMenuBg: $colorHeadBg;\n$colorIndicatorMenuBgShdw: rgba(white, 0.3) 0 0 10px 1px;\n$colorIndicatorMenuFg: $colorHeadFg;\n$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);\n\n// Staleness\n$colorTelemStale: cyan;\n$colorTelemStaleFg: #002a2a;\n$styleTelemStale: italic;\n\n// Limits\n$colorLimitYellowBg: #b18b05;\n$colorLimitYellowFg: #feeeb5;\n$colorLimitYellowIc: #fdc707;\n$colorLimitOrangeBg: #b36b00;\n$colorLimitOrangeFg: #ffe0b2;\n$colorLimitOrangeIc: #ff9900;\n$colorLimitRedBg: #940000;\n$colorLimitRedFg: #ffa489;\n$colorLimitRedIc: #ff4222;\n$colorLimitPurpleBg: #891bb3;\n$colorLimitPurpleFg: #edbeff;\n$colorLimitPurpleIc: #c327ff;\n$colorLimitCyanBg: #4ba6b3;\n$colorLimitCyanFg: #d3faff;\n$colorLimitCyanIc: #6bedff;\n\n// Events\n$colorEventPurpleFg: #ab8fff;\n$colorEventRedFg: #ff9999;\n$colorEventOrangeFg: #ff8800;\n$colorEventYellowFg: #ffdb63;\n$colorEventPurpleBg: #31204a;\n$colorEventRedBg: #3c1616;\n$colorEventOrangeBg: #3e2a13;\n$colorEventYellowBg: #3e3316;\n$colorEventPurpleLine: #9e36ff;\n$colorEventRedLine: #ff2525;\n$colorEventOrangeLine: #ff8800;\n$colorEventYellowLine: #fdce22;\n\n// Bubble colors\n$colorInfoBubbleBg: #dddddd;\n$colorInfoBubbleFg: #666;\n$colorThumbsBubbleFg: pullForward($colorBodyFg, 10%);\n$colorThumbsBubbleBg: pullForward($colorBodyBg, 10%);\n\n// Items\n$colorSelectableItemBg: transparent;\n$colorSelectableItemBgHov: rgba(#fff, 0.1);\n$colorItemBg: buttonBg($colorBtnBg);\n$colorItemBgHov: buttonBg(pullForward($colorBtnBg, 5%));\n$colorListItemBg: transparent;\n$colorListItemBgHov: $colorSelectableItemBgHov; //$colorItemTreeHoverBg;\n$colorItemFg: $colorBtnFg;\n$colorItemFgDetails: $colorBodyFgSubtle;\n$shdwItemText: none;\n\n// Tabular\n$colorTabBorder: pullForward($colorBodyBg, 10%);\n$colorTabBodyBg: $colorBodyBg;\n$colorTabBodyFg: pullForward($colorBodyFg, 20%);\n$colorTabHeaderBg: #575757;\n$colorTabHeaderFg: $colorBodyFg;\n$colorTabHeaderBorder: $colorBodyBg;\n$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);\n$colorTabGroupHeaderFg: pushBack($colorTabHeaderFg, 10%);\n$colorSummaryBg: #2c2c2c;\n$colorSummaryFg: rgba($colorBodyFg, 0.7);\n$colorSummaryFgEm: $colorBodyFg;\n\n// Plot\n$colorPlotBg: rgba(black, 0.1);\n$colorPlotFg: $colorBodyFg;\n$colorPlotHash: $colorPlotFg;\n$opacityPlotHash: 0.2;\n$stylePlotHash: dashed;\n$colorPlotAreaBorder: $colorInteriorBorder;\n$colorPlotLabelFg: pushBack($colorPlotFg, 20%);\n$legendHoverValueBg: rgba($colorBodyFg, 0.2);\n$legendTableHeadBg: $colorTabHeaderBg;\n$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);\n\n// Gauges\n$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background\n$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color\n$colorGaugeTextValue: #fff; // Radial gauge text value\n$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar\n$colorGaugeRange: $colorBodyFg; // Range text color\n$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);\n$colorGaugeLimitLow: $colorGaugeLimitHigh;\n$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.\n$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions\n$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges\n$gaugeMeterValueShadow: rgba(255, 255, 255, 0);\n\n// Time Strip and Lists\n$colorPastBg: #343434;\n$colorPastFg: #ffffff;\n$colorPastFgEm: $colorPastFg;\n$colorCurrentBg: #666666;\n$colorCurrentFg: #ffffff;\n$colorCurrentFgEm: $colorCurrentFg;\n$colorCurrentBorder: $colorBodyBg;\n$colorFutureBg: #494949;\n$colorFutureFg: $colorCurrentFg;\n$colorFutureFgEm: $colorFutureFg;\n$colorFutureBorder: $colorCurrentBorder;\n$colorInProgressBg: $colorTimeRealtimeBg;\n$colorInProgressFg: #ffffff;\n$colorInProgressFgEm: $colorTimeRealtimeFg;\n$colorGanttSelectedBorder: rgba(#fff, 0.3);\n$colorEventLine: $colorBodyFg;\n$colorEventLineExtended: rgba($colorEventLine, 0.3);\n$colorTimeStripDraftBg: rgba(#a57748, 0.2);\n$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);\n$eventLineW: 1px;\n$colorActivityStatusGreen: #4bad4b;\n$colorActivityStatusOrange: #b67e41;\n$opacitySubtle: 0.4;\n\n$colorABAhead: #33c56e;\n$colorABBehind: #ffa500;\n\n// Tree\n$colorTreeBg: transparent;\n$colorItemTreeHoverBg: $colorSelectableItemBgHov;\n$colorItemTreeHoverFg: #fff;\n$colorItemTreeIcon: $colorKey;\n$colorItemTreeIconHover: $colorItemTreeIcon;\n$colorItemTreeFg: #ccc;\n$colorItemTreeSelectedBg: $colorSelectedBg;\n$colorItemTreeSelectedFg: $colorItemTreeHoverFg;\n$filterItemTreeSelected: $filterHov;\n$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;\n$colorItemTreeEditingBg: pushBack($editUIColor, 20%);\n$colorItemTreeEditingFg: $editUIColor;\n$colorItemTreeEditingIcon: $editUIColor;\n$colorItemTreeVC: $colorDisclosureCtrl;\n$colorItemTreeVCHover: $colorDisclosureCtrlHov;\n$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);\n$shdwItemTreeIcon: none;\n\n// Layout frame controls\n$frameControlsColorFg: white;\n$frameControlsColorBg: $colorKey;\n$frameControlsShdw: $shdwMenu;\n\n// Images\n$colorThumbHoverBg: $colorItemTreeHoverBg;\n\n// Scrollbar\n$scrollbarTrackSize: 7px;\n$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;\n$scrollbarTrackColorBg: rgba(#000, 0.2);\n$scrollbarThumbColor: pushBack($colorBodyBg, 50%);\n$scrollbarThumbColorHov: $colorKey;\n$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);\n$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);\n\n// Splitter\n$splitterHandleD: 2px;\n$splitterD: $splitterHandleD;\n$splitterHandleHitMargin: 4px;\n$colorSplitterBaseBg: $colorBodyBg;\n$colorSplitterBg: pullForward($colorBodyBg, 10%);\n$colorSplitterFg: $colorBodyBg;\n$colorSplitterHover: $uiColor;\n$colorSplitterActive: $colorKey;\n$splitterBtnD: (16px, 35px); // height, width\n$splitterBtnColorBg: $colorBtnBg;\n$splitterBtnColorFg: #999;\n$splitterBtnLabelColorFg: $colorBodyFgSubtle;\n$splitterCollapsedBtnColorBg: $colorHeadBg;\n$splitterCollapsedBtnColorFg: #757575;\n$splitterCollapsedBtnColorBgHov: $colorKeyBg;\n$splitterCollapsedBtnColorFgHov: $colorKeyFg;\n\n// Mobile\n$colorMobilePaneLeft: pushBack($colorBodyBg, 2%);\n$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);\n$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;\n$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);\n\n// About Screen\n$colorAboutLink: #9bb5ff;\n\n// Loading\n$colorLoadingFg: #776ba2;\n$colorLoadingBg: rgba($colorLoadingFg, 0.1);\n\n// Transitions\n$transInTime: 50ms;\n$transOutTime: 250ms;\n$transIn: all $transInTime ease-in-out;\n$transOut: all $transOutTime ease-in-out;\n$transInTransform: transform $transInTime ease-in-out;\n$transOutTransform: transform $transOutTime ease-in-out;\n$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5);\n$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3);\n\n// Discrete items\n$createBtnTextTransform: uppercase;\n$colorDiscreteItemBg: rgba($colorBodyFg, 0.1);\n$colorDiscreteItemBgHov: rgba($colorBodyFg, 0.2);\n$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3);\n$scrollContainer: $colorBodyBg;\n"
  },
  {
    "path": "src/styles/_constants-maelstrom.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/************************************************** MAELSTROM THEME */\n// Fonts\n@import url('https://fonts.googleapis.com/css?family=Chakra+Petch:400,600,700|Michroma|Teko:400,700');\n\n$heroFont: 'Teko', sans-serif;\n$headerFont: 'Michroma', sans-serif;\n$bodyFont: 'Chakra Petch', sans-serif;\n$numericFont: $heroFont;\n\n@mixin heroFont($size: 1em) {\n  font-family: $heroFont;\n  font-size: $size;\n}\n\n@mixin headerFont($size: 1em) {\n  font-family: $headerFont;\n  font-size: $size * 0.8; // This font is comparatively large, so reduce it a bit\n  text-transform: uppercase;\n  word-spacing: 0.25em;\n}\n\n@mixin bodyFont($size: 1em) {\n  font-family: $bodyFont;\n  font-size: $size;\n}\n\n@mixin discreteItem() {\n  background: rgba($colorBodyFg, 0.1);\n  border: none;\n  border-radius: $controlCr;\n\n  &--current-match {\n    background: $colorDiscreteItemCurrentBg;\n  }\n}\n\n@mixin discreteItemInnerElem() {\n  border: 1px solid rgba(#fff, 0.1);\n  border-radius: $controlCr;\n}\n\n@mixin themedButton($c: $colorBtnBg) {\n  background: linear-gradient(pullForward($c, 5%), $c);\n  box-shadow: rgba(black, 0.5) 0 0.5px 2px;\n}\n\n@mixin telemetryView() {\n}\n@mixin browseFrameBorder() {\n}\n\n/**************************************************** OVERRIDES */\n.c-frame {\n  &:not(.no-frame) {\n    $bc: #666;\n    $bLR: 3px solid transparent;\n    $br: 20px;\n    background: none !important;\n    border-radius: $br;\n    border-top: 4px solid $bc !important;\n    border-bottom: 2px solid $bc !important;\n    border-left: $bLR !important;\n    border-right: $bLR !important;\n    padding: 5px 10px 10px 10px !important;\n  }\n}\n\n// Functions\n@function buttonBg($c: $colorBtnBg) {\n  @return linear-gradient(lighten($c, 5%), $c);\n}\n\n@function pullForward($val, $amt) {\n  @return lighten($val, $amt);\n}\n\n@function pushBack($val, $amt) {\n  @return darken($val, $amt);\n}\n\n/**************************************************** CONSTANTS */\n$fontBaseSize: 12px;\n$smallCr: 2px;\n$controlCr: 3px;\n$basicCr: 4px;\n$shdwBtns: rgba(black, 0.2) 0 1px 2px;\n$shdwBtnsOverlay: rgba(black, 0.5) 0 1px 5px;\n\n// Base colors\n$colorBodyBg: #393939;\n$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);\n$colorBodyBgSubtleHov: pushBack($colorKey, 50%);\n$colorBodyFg: #ccc;\n$colorBodyFgSubtle: #9c9c9c;\n$colorBodyFgEm: #fff;\n$colorGenBg: #222;\n$colorHeadBg: #262626;\n$colorHeadFg: $colorBodyFg;\n$colorKey: #0099cc;\n$colorKeyBg: #007fad; // Darker version of colorKey for use in major buttons\n$colorKeyFg: #fff;\n$colorKeyHov: #26d8ff;\n$colorKeyBgHov: lighten($colorKeyBg, 10%);\n$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%)\n  contrast(101%);\n$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%)\n  contrast(100%);\n$colorKeySelectedBg: $colorKey;\n$colorKeySubtle: pushBack($colorKey, 10%);\n$uiColor: #0093ff; // Resize bars, splitter bars, etc.\n$colorInteriorBorder: rgba($colorBodyFg, 0.2);\n$colorInteriorBorderNotebook: rgba($colorBodyFg, 0.5);\n$colorA: #ccc;\n$colorAHov: #fff;\n$filterHov: brightness(1.3) contrast(1.5); // Tree, location items\n$filterHovSubtle: brightness(1.2) contrast(1.2);\n$colorSelectedBg: rgba($colorKey, 0.3);\n$colorSelectedFg: pullForward($colorBodyFg, 20%);\n\n// Body constants\n$bodyBg: $colorBodyBg;\n$bodyBgSize: cover;\n$bodySize: 100%;\n\n// Object labels\n$objectLabelTypeIconOpacity: 0.7;\n$objectLabelNameColorFg: lighten($colorBodyFg, 10%);\n\n// Layout\n$shellMainPad: 4px 0;\n$shellPanePad: $interiorMargin, 7px;\n$drawerBg: lighten($colorBodyBg, 5%);\n$drawerFg: lighten($colorBodyFg, 5%);\n$sideBarBg: $drawerBg;\n$sideBarHeaderBg: rgba($colorBodyFg, 0.2);\n$sideBarHeaderFg: rgba($colorBodyFg, 0.7);\n\n// Status colors, mainly used for messaging and item ancillary symbols\n$colorStatusFg: #999;\n$colorStatusDefault: #ccc;\n$colorStatusInfo: #60ba7b;\n$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%)\n  contrast(92%);\n$colorStatusAlert: #ffb66c;\n$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%)\n  contrast(101%);\n$colorStatusError: #da0004;\n$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%)\n  contrast(115%);\n$colorStatusBtnBg: #666; // Where is this used?\n$colorStatusPartialBg: #3f5e8b;\n$colorStatusCompleteBg: #457638;\n$colorAlert: #ff8a0d;\n$colorAlertFg: #fff;\n$colorError: #ff3c00;\n$colorErrorFg: #fff;\n$colorWarningHi: #990000;\n$colorWarningHiFg: #ff9594;\n$colorWarningLo: #ff9900;\n$colorWarningLoFg: #523400;\n$colorDiagnostic: #a4b442;\n$colorDiagnosticFg: #39461a;\n$colorCommand: #3693bd;\n$colorCommandFg: #fff;\n$colorInfo: #2294a2;\n$colorInfoFg: #fff;\n$colorOk: #33cc33;\n$colorOkFg: #fff;\n$colorFilterBg: #44449c;\n$colorFilterFg: #8984e9;\n$colorFilter: $colorFilterFg; // Standalone against $colorBodyBg\n\n// States\n$colorPausedBg: #ff9900;\n$colorPausedFg: #333;\n\n// Time Colors\n$colorTimeCommonFg: #eee;\n$colorTimeFixed: #59554c;\n$colorTimeFixedBg: $colorTimeFixed;\n$colorTimeFixedFg: #eee;\n$colorTimeFixedFgSubtle: #b2aa98;\n$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);\n$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);\n$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;\n$colorTimeFixedBtnBgMajor: #a09375;\n$colorTimeFixedBtnFgMajor: #fff;\n$colorTimeRealtime: #445890;\n$colorTimeRealtimeBg: $colorTimeRealtime;\n$colorTimeRealtimeFg: #eee;\n$colorTimeRealtimeFgSubtle: #88b0ff;\n$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);\n$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);\n$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;\n$colorTimeRealtimeBtnBgMajor: #588ffa;\n$colorTimeRealtimeBtnFgMajor: #fff;\n$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor\n$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov\n$timeConductorAxisHoverFilter: brightness(1.2);\n$timeConductorActiveBg: $colorKey;\n$timeConductorActivePanBg: #226074;\n\n// Browse\n$browseFrameColor: pullForward($colorBodyBg, 10%);\n$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing\n$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;\n$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);\n$filterItemHoverFg: brightness(1.2) contrast(1.1);\n$interiorMarginObjectFrameVertical: 0px;\n$interiorMarginObjectFrameHorizontal: 3px;\n\n// Missing Items\n$filterItemMissing: contrast(0.2);\n$opacityMissing: 0.5;\n$borderMissing: 1px dashed $colorAlert !important;\n\n// Edit\n$editUIColor: $uiColor; // Base color\n$editUIColorBg: $editUIColor;\n$editUIColorFg: #fff;\n$editUIColorHov: pullForward(\n  saturate($uiColor, 10%),\n  10%\n); // Hover color when $editUIColor is applied as a base color\n$editUIBaseColor: #344b8d; // Base color, toolbar bg\n$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);\n$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.\n$editUIAreaBaseColor: pullForward(saturate($editUIBaseColor, 30%), 20%);\n$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area\n$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area\n$editDimensionsColor: #6a5ea6;\n$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover\n$editFrameBorder: 1px dotted $editFrameColor;\n$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects\n$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames\n$editFrameColorSelected: #ffefc2; // Border of selected frames while editing\n$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout\n$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color\n$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;\n$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color\n$editFrameMovebarColorFg: pullForward(\n  $editFrameMovebarColorBg,\n  20%\n); // Grippy lines, container size text\n$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style\n$editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%);\n$editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style\n$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);\n$editFrameMovebarH: 10px; // Height of move bar in layout frame\n$editMarqueeBorder: 1px dashed $editFrameColorSelected;\n$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element\n\n// Icons\n$colorIconAlias: #4af6f3;\n$colorIconAliasForKeyFilter: #aaa;\n\n// Holders\n$colorTabsHolderBg: rgba(black, 0.2);\n\n// Buttons and Controls\n$colorBtnBg: pullForward($colorBodyBg, 10%);\n$colorBtnBgHov: pullForward($colorBtnBg, 10%);\n$shdwBtnHov: inset rgba(white, 10%) 0 0 0 100px;\n$colorBtnFg: pullForward($colorBodyFg, 10%);\n$colorBtnReverseFg: pullForward($colorBtnFg, 10%);\n$colorBtnReverseBg: pullForward($colorBtnBg, 10%);\n$colorBtnFgHov: $colorBtnFg;\n$colorBtnMajorBg: $colorKey;\n$colorBtnMajorBgHov: $colorKeyHov;\n$colorBtnMajorFg: $colorKeyFg;\n$colorBtnMajorFgHov: pushBack($colorBtnMajorFg, 10%);\n$colorBtnCautionBg: $colorStatusAlert;\n$colorBtnCautionBgHov: #f1504e;\n$colorBtnCautionFg: $colorBtnBg;\n$colorBtnActiveBg: $colorOk;\n$colorBtnActiveFg: $colorOkFg;\n$colorBtnSelectedBg: $colorSelectedBg;\n$colorBtnSelectedFg: $colorSelectedFg;\n$colorClickIconButton: $colorKey;\n$colorClickIconButtonBgHov: rgba($colorKey, 0.6);\n$colorClickIconButtonFgHov: $colorKeyHov;\n$colorDropHint: $colorKey;\n$colorDropHintBg: pushBack($colorDropHint, 10%);\n$colorDropHintBgHov: $colorDropHint;\n$colorDropHintFg: pullForward($colorDropHint, 40%);\n$colorDisclosureCtrl: rgba($colorBodyFg, 0.5);\n$colorDisclosureCtrlHov: rgba($colorBodyFg, 0.7);\n$btnStdH: 24px;\n$colorCursorGuide: rgba(white, 0.6);\n$shdwCursorGuide: rgba(black, 0.4) 0 0 2px;\n$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);\n$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects\n$colorSelectFg: $colorBtnFg;\n$colorSelectArw: lighten($colorBtnBg, 20%);\n$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;\n$controlDisabledOpacity: 0.2;\n\n// Menus\n$colorMenuBg: $colorBodyBg;\n$colorMenuFg: $colorBodyFg;\n$colorMenuIc: $colorKey;\n$filterMenu: brightness(1.4);\n$colorMenuHovBg: rgba($colorKey, 0.5);\n$colorMenuHovFg: $colorBodyFgEm;\n$colorMenuHovIc: $colorMenuHovFg;\n$colorMenuElementHilite: pullForward($colorMenuBg, 10%);\n$shdwMenu: rgba(black, 0.8) 0 2px 10px;\n$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);\n$shdwMenuText: none;\n$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);\n\n// Palettes and Swatches\n$paletteItemBorderOuterColorSelected: black;\n$paletteItemBorderInnerColorSelected: white;\n$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);\n$mixedSettingBg: (transparent rgba($editUIBaseColorHov, 0.7)); // Used in .c-click-icon--mixed\n$mixedSettingBgSize: 5px;\n\n// Forms\n$colorCheck: $colorKey;\n$colorFormRequired: $colorKey;\n$colorFormValid: $colorOk;\n$colorFormError: #990000;\n$colorFormInvalid: #ff2200;\n$colorFormFieldErrorBg: $colorFormError;\n$colorFormFieldErrorFg: rgba(#fff, 0.6);\n$colorFormLines: rgba(#000, 0.1);\n$colorFormSectionHeaderBg: rgba(#000, 0.1);\n$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);\n$colorInputBg: rgba(black, 0.2);\n$colorInputBgHov: rgba(black, 0.1);\n$colorInputFg: $colorBodyFg;\n$colorFormText: pushBack($colorBodyFg, 10%);\n$colorInputIcon: pushBack($colorBodyFg, 25%);\n$colorFieldHint: pullForward($colorBodyFg, 40%);\n$shdwInput: inset rgba(black, 0.4) 0 0 1px;\n$shdwInputHov: inset rgba(black, 0.7) 0 0 2px;\n$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;\n$formTBPad: $interiorMargin;\n$formLRPad: $interiorMargin;\n$formInputH: 22px;\n$formRowCtrlsH: 14px;\n\n// Inspector\n$colorInspectorBg: pullForward($colorBodyBg, 5%);\n$colorInspectorFg: $colorBodyFg;\n$colorInspectorPropName: pushBack($colorBodyFg, 20%);\n$colorInspectorPropVal: pullForward($colorInspectorFg, 15%);\n$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);\n$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);\n\n// Tabs\n$colorTabBg: pullForward($colorBodyBg, 5%);\n$colorTabFg: pullForward($colorBtnFg, 10%);\n$colorTabCurrentBg: pullForward($colorTabBg, 10%);\n$colorTabCurrentFg: pullForward($colorTabFg, 10%);\n$colorTabsBaseline: $colorTabCurrentBg;\n\n// Overlay\n$colorOvrBlocker: rgba(black, 0.7);\n$overlayCr: $interiorMarginLg;\n\n// Indicator colors\n$colorIndicatorAvailable: $colorKey;\n$colorIndicatorDisabled: #555555;\n$colorIndicatorOn: $colorOk;\n$colorIndicatorOff: #777777;\n$colorIndicatorBgHov: rgba($colorHeadFg, 0.1);\n$colorIndicatorMenuBg: $colorHeadBg;\n$colorIndicatorMenuBgShdw: rgba(white, 0.6) 0 0 6px;\n$colorIndicatorMenuFg: $colorHeadFg;\n$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);\n\n// Staleness\n$colorTelemStale: cyan;\n$colorTelemStaleFg: #002a2a;\n$styleTelemStale: italic;\n\n// Limits\n$colorLimitYellowBg: #b18b05;\n$colorLimitYellowFg: #feeeb5;\n$colorLimitYellowIc: #fdc707;\n$colorLimitOrangeBg: #b36b00;\n$colorLimitOrangeFg: #ffe0b2;\n$colorLimitOrangeIc: #ff9900;\n$colorLimitRedBg: #940000;\n$colorLimitRedFg: #ffa489;\n$colorLimitRedIc: #ff4222;\n$colorLimitPurpleBg: #891bb3;\n$colorLimitPurpleFg: #edbeff;\n$colorLimitPurpleIc: #c327ff;\n$colorLimitCyanBg: #4ba6b3;\n$colorLimitCyanFg: #d3faff;\n$colorLimitCyanIc: #6bedff;\n\n// Events\n$colorEventPurpleFg: #ab8fff;\n$colorEventRedFg: #ff9999;\n$colorEventOrangeFg: #ff8800;\n$colorEventYellowFg: #ffdb63;\n$colorEventPurpleBg: #31204a;\n$colorEventRedBg: #3c1616;\n$colorEventOrangeBg: #3e2a13;\n$colorEventYellowBg: #3e3316;\n$colorEventPurpleLine: #9e36ff;\n$colorEventRedLine: #ff2525;\n$colorEventOrangeLine: #ff8800;\n$colorEventYellowLine: #fdce22;\n\n// Bubble colors\n$colorInfoBubbleBg: #dddddd;\n$colorInfoBubbleFg: #666;\n$colorThumbsBubbleFg: pullForward($colorBodyFg, 10%);\n$colorThumbsBubbleBg: pullForward($colorBodyBg, 10%);\n\n// Items\n$colorSelectableItemBg: transparent;\n$colorSelectableItemBgHov: rgba(#fff, 0.1);\n$colorItemBg: buttonBg($colorBtnBg);\n$colorItemBgHov: buttonBg(pullForward($colorBtnBg, 5%));\n$colorListItemBg: transparent;\n$colorListItemBgHov: $colorSelectableItemBgHov;\n$colorItemFg: $colorBtnFg;\n$colorItemFgDetails: pushBack($colorItemFg, 20%);\n$shdwItemText: none;\n\n// Tabular\n$colorTabBorder: pullForward($colorBodyBg, 10%);\n$colorTabBodyBg: $colorBodyBg;\n$colorTabBodyFg: pullForward($colorBodyFg, 20%);\n$colorTabHeaderBg: #575757;\n$colorTabHeaderFg: $colorBodyFg;\n$colorTabHeaderBorder: $colorBodyBg;\n$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);\n$colorTabGroupHeaderFg: pushBack($colorTabHeaderFg, 10%);\n$colorSummaryBg: #2c2c2c;\n$colorSummaryFg: rgba($colorBodyFg, 0.7);\n$colorSummaryFgEm: $colorBodyFg;\n\n// Plot\n$colorPlotBg: rgba(black, 0.05);\n$colorPlotFg: $colorBodyFg;\n$colorPlotHash: black;\n$opacityPlotHash: 0.2;\n$stylePlotHash: dashed;\n$colorPlotAreaBorder: $colorInteriorBorder;\n$colorPlotLabelFg: pushBack($colorPlotFg, 20%);\n$legendHoverValueBg: rgba($colorBodyFg, 0.2);\n$legendTableHeadBg: rgba($colorBodyFg, 0.15);\n$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);\n\n// Gauges\n$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background\n$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color\n$colorGaugeTextValue: #fff; // Radial gauge text value\n$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar\n$colorGaugeRange: $colorBodyFg; // Range text color\n$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);\n$colorGaugeLimitLow: $colorGaugeLimitHigh;\n$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.\n$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions\n$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges\n$gaugeMeterValueShadow: rgba(255, 255, 255, 0);\n\n// Time Strip and Lists\n$colorPastBg: #343434;\n$colorPastFg: #ffffff;\n$colorPastFgEm: $colorPastFg;\n$colorCurrentBg: #666666;\n$colorCurrentFg: #ffffff;\n$colorCurrentFgEm: $colorCurrentFg;\n$colorCurrentBorder: $colorBodyBg;\n$colorFutureBg: #494949;\n$colorFutureFg: $colorCurrentFg;\n$colorFutureFgEm: $colorFutureFg;\n$colorFutureBorder: $colorCurrentBorder;\n$colorInProgressBg: $colorTimeRealtimeBg;\n$colorInProgressFg: $colorTimeRealtimeFgSubtle;\n$colorInProgressFgEm: $colorTimeRealtimeFg;\n$colorGanttSelectedBorder: rgba(#fff, 0.3);\n$colorActivityStatusGreen: #4bad4b;\n$colorActivityStatusOrange: #b67e41;\n$opacitySubtle: 0.4;\n$colorEventLine: $colorBodyFg;\n$colorEventLineExtended: rgba($colorEventLine, 0.3);\n$colorTimeStripDraftBg: rgba(#a57748, 0.2);\n$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);\n$colorABAhead: #33c56e;\n$colorABBehind: #ffa500;\n$eventLineW: 1px;\n\n// Tree\n$colorTreeBg: transparent;\n$colorItemTreeHoverBg: $colorSelectableItemBgHov;\n$colorItemTreeHoverFg: #fff;\n$colorItemTreeIcon: $colorKey;\n$colorItemTreeIconHover: $colorItemTreeIcon;\n$colorItemTreeFg: $colorA;\n$colorItemTreeSelectedBg: $colorSelectedBg;\n$colorItemTreeSelectedFg: $colorItemTreeHoverFg;\n$filterItemTreeSelected: $filterHov;\n$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;\n$colorItemTreeEditingBg: pushBack($editUIColor, 20%);\n$colorItemTreeEditingFg: $editUIColor;\n$colorItemTreeEditingIcon: $editUIColor;\n$colorItemTreeVC: $colorDisclosureCtrl;\n$colorItemTreeVCHover: $colorDisclosureCtrlHov;\n$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);\n$shdwItemTreeIcon: none;\n\n// Layout frame controls\n$frameControlsColorFg: white;\n$frameControlsColorBg: $colorKey;\n$frameControlsShdw: $shdwMenu;\n\n// Images\n$colorThumbHoverBg: $colorItemTreeHoverBg;\n\n// Scrollbar\n$scrollbarTrackSize: 7px;\n$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;\n$scrollbarTrackColorBg: rgba(#000, 0.2);\n$scrollbarThumbColor: pushBack($colorBodyBg, 50%);\n$scrollbarThumbColorHov: $colorKey;\n$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);\n$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);\n\n// Splitter\n$splitterHandleD: 2px;\n$splitterD: $splitterHandleD;\n$splitterHandleHitMargin: 4px;\n$colorSplitterBaseBg: $colorBodyBg;\n$colorSplitterBg: pullForward($colorBodyBg, 10%);\n$colorSplitterFg: $colorBodyBg;\n$colorSplitterHover: $uiColor;\n$colorSplitterActive: $colorKey;\n$splitterBtnD: (16px, 35px); // height, width\n$splitterBtnColorBg: $colorBtnBg;\n$splitterBtnColorFg: #999;\n$splitterBtnLabelColorFg: #666;\n$splitterCollapsedBtnColorBg: #222;\n$splitterCollapsedBtnColorFg: #555;\n$splitterCollapsedBtnColorBgHov: $colorKey;\n$splitterCollapsedBtnColorFgHov: $colorKeyFg;\n\n// Mobile\n$colorMobilePaneLeft: pushBack($colorBodyBg, 2%);\n$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);\n$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;\n$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);\n\n// About Screen\n$colorAboutLink: #9bb5ff;\n\n// Loading\n$colorLoadingFg: #776ba2;\n$colorLoadingBg: rgba($colorLoadingFg, 0.1);\n\n// Transitions\n$transInTime: 50ms;\n$transOutTime: 250ms;\n$transIn: all $transInTime ease-in-out;\n$transOut: all $transOutTime ease-in-out;\n$transInTransform: transform $transInTime ease-in-out;\n$transOutTransform: transform $transOutTime ease-in-out;\n$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5);\n$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3);\n\n// Discrete items\n$createBtnTextTransform: uppercase;\n$colorDiscreteItemBg: rgba($colorBodyFg, 0.1);\n$colorDiscreteItemBgHov: rgba($colorBodyFg, 0.2);\n$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3);\n$scrollContainer: $colorBodyBg;\n"
  },
  {
    "path": "src/styles/_constants-mobile.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/* REQUIRES /platform/commonUI/general/res/sass/_constants.scss */\n@use 'sass:math';\n\n/************************** MOBILE REPRESENTATION ITEMS DIMENSIONS */\n$mobileListIconSize: 30px;\n$mobileTitleDescH: 35px;\n$mobileOverlayMargin: 20px;\n$mobileMenuIconD: 25px;\n$phoneItemH: floor(math.div($gridItemMobile, 4));\n$tabletItemH: floor(math.div($gridItemMobile, 3));\n$shellTimeConductorMobileH: 90px;\n\n/************************** MOBILE TREE MENU DIMENSIONS */\n$mobileTreeItemH: 35px;\n$mobileTreeItemIndent: 15px;\n$mobileTreeRightArrowW: 30px;\n\n/************************** DEVICE WIDTHS */\n// IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps\n$phoMaxW: 767px;\n$tabMinW: 768px;\n$tabMaxW: 1024px;\n$desktopMinW: 1025px;\n\n/************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */\n$screenPortrait: '(orientation: portrait)';\n$screenLandscape: '(orientation: landscape)';\n\n//$mobileDevice: \"(max-device-width: #{$tabMaxW})\";\n\n$phoneCheck: '(max-device-width: #{$phoMaxW})';\n$tabletCheck: '(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})';\n$desktopCheck: '(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)';\n\n/************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */\n$phonePortrait: 'only screen and #{$screenPortrait} and #{$phoneCheck}';\n$phoneLandscape: 'only screen and #{$screenLandscape} and #{$phoneCheck}';\n\n$tabletPortrait: 'only screen and #{$screenPortrait} and #{$tabletCheck}';\n$tabletLandscape: 'only screen and #{$screenLandscape} and #{$tabletCheck}';\n\n$desktop: 'only screen and #{$desktopCheck}';\n\n/************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */\n$proporMenuOnly: 90%;\n$proporMenuWithView: 40%;\n\n// Phones in any orientation\n@mixin phone {\n  @media #{$phonePortrait},\n    #{$phoneLandscape} {\n    @content;\n  }\n}\n\n//Phones in portrait orientation\n@mixin phonePortrait {\n  @media #{$phonePortrait} {\n    @content;\n  }\n}\n\n// Phones in landscape orientation\n@mixin phoneLandscape {\n  @media #{$phoneLandscape} {\n    @content;\n  }\n}\n\n// Tablets in any orientation\n@mixin tablet {\n  @media #{$tabletPortrait},\n    #{$tabletLandscape} {\n    @content;\n  }\n}\n\n// Tablets in portrait orientation\n@mixin tabletPortrait {\n  @media #{$tabletPortrait} {\n    @content;\n  }\n}\n\n// Tablets in landscape orientation\n@mixin tabletLandscape {\n  @media #{$tabletLandscape} {\n    @content;\n  }\n}\n\n// Phones and tablets in any orientation\n@mixin phoneandtablet {\n  @media #{$phonePortrait},\n    #{$phoneLandscape},\n    #{$tabletPortrait},\n    #{$tabletLandscape} {\n    @content;\n  }\n}\n\n// Desktop monitors in any orientation\n@mixin desktopandtablet {\n  // Keeping only for legacy - should not be used moving forward\n  // Use body.desktop, body.tablet instead.\n  @media #{$tabletPortrait},\n    #{$tabletLandscape},\n    #{$desktop} {\n    @content;\n  }\n}\n\n// Desktop monitors in any orientation\n@mixin desktop {\n  // Keeping only for legacy - should not be used moving forward\n  // Use body.desktop instead.\n  @media #{$desktop} {\n    @content;\n  }\n}\n\n// Transition used for the slide menu\n@mixin slMenuTransitions {\n  @include transition-duration(0.35s);\n  transition-timing-function: ease;\n  backface-visibility: hidden;\n}\n"
  },
  {
    "path": "src/styles/_constants-snow.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/****************************************************** SNOW THEME */\n// Fonts\n$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n$headerFont: $heroFont;\n$bodyFont: $heroFont;\n$numericFont: $heroFont;\n\n@mixin heroFont($size: 1em) {\n  font-family: $heroFont;\n  font-size: $size;\n}\n\n@mixin headerFont($size: 1em) {\n  font-family: $headerFont;\n  font-size: $size;\n}\n\n@mixin bodyFont($size: 1em) {\n  font-family: $bodyFont;\n  font-size: $size;\n}\n\n@mixin discreteItem() {\n  background: $colorDiscreteItemBg;\n  border: 1px solid $colorInteriorBorder;\n  border-radius: $controlCr;\n\n  &--current-match {\n    background: $colorDiscreteItemCurrentBg;\n  }\n\n  .c-input-inline:hover {\n    background: $colorBodyBg;\n  }\n}\n\n@mixin discreteItemInnerElem() {\n  border: 1px solid $colorBodyBg;\n  border-radius: $controlCr;\n}\n\n@mixin themedButton($c: $colorBtnBg) {\n  background: $c;\n}\n\n@mixin telemetryView() {\n}\n@mixin browseFrameBorder() {\n}\n\n// Functions\n@function buttonBg($c: $colorBtnBg) {\n  @return $c;\n}\n\n@function pullForward($val, $amt) {\n  @return darken($val, $amt);\n}\n\n@function pushBack($val, $amt) {\n  @return lighten($val, $amt);\n}\n\n/**************************************************** CONSTANTS */\n$fontBaseSize: 12px;\n$smallCr: 2px;\n$controlCr: 3px;\n$basicCr: 4px;\n$shdwBtns: none;\n$shdwBtnsOverlay: none;\n\n// Base colors\n$colorBodyBg: #fcfcfc;\n$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);\n$colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);\n$colorBodyFg: #666;\n$colorBodyFgSubtle: #888;\n$colorBodyFgEm: #333;\n$colorGenBg: #fff;\n$colorHeadBg: #eee;\n$colorHeadFg: $colorBodyFg;\n$colorKey: #0099cc;\n$colorKeyBg: #007fad; // Darker version of colorKey for use in major buttons\n$colorKeyFg: #fff;\n$colorKeyHov: lighten($colorKey, 10%); //#00c0f6;\n$colorKeyBgHov: lighten($colorKeyBg, 10%);\n$colorKeyFilter: invert(37%) sepia(100%) saturate(686%) hue-rotate(157deg) brightness(102%)\n  contrast(102%);\n$colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) brightness(97%)\n  contrast(102%);\n$colorKeySelectedBg: $colorKey;\n$colorKeySubtle: pushBack($colorKey, 20%);\n$uiColor: #289fec; // Resize bars, splitter bars, etc.\n$colorInteriorBorder: rgba($colorBodyFg, 0.2);\n$colorInteriorBorderNotebook: rgba($colorBodyFg, 0.5);\n$colorA: $colorBodyFg;\n$colorAHov: $colorKey;\n$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items\n$filterHovSubtle: hue-rotate(-8deg) brightness(0.5) contrast(1.2);\n$colorSelectedBg: pushBack($colorKey, 40%);\n$colorSelectedFg: pullForward($colorBodyFg, 10%);\n\n// Body constants\n$bodyBg: $colorBodyBg;\n$bodyBgSize: cover;\n$bodySize: 100%;\n\n// Object labels\n$objectLabelTypeIconOpacity: 0.5;\n$objectLabelNameColorFg: darken($colorBodyFg, 10%);\n\n// Layout\n$shellMainPad: 4px 0;\n$shellPanePad: $interiorMargin, 7px;\n$drawerBg: darken($colorBodyBg, 5%);\n$drawerFg: darken($colorBodyFg, 5%);\n$sideBarBg: $drawerBg;\n$sideBarHeaderBg: rgba(black, 0.1);\n$sideBarHeaderFg: rgba($colorBodyFg, 0.7);\n\n// Status colors, mainly used for messaging and item ancillary symbols\n$colorStatusFg: #999;\n$colorStatusDefault: #ccc;\n$colorStatusInfo: #60ba7b;\n$colorStatusInfoFilter: invert(64%) sepia(42%) saturate(416%) hue-rotate(85deg) brightness(93%)\n  contrast(93%);\n$colorStatusAlert: #ff8a0d;\n$colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316deg) brightness(114%)\n  contrast(107%);\n$colorStatusError: #da0004;\n$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%)\n  contrast(114%);\n$colorStatusBtnBg: #666; // Where is this used?\n$colorStatusPartialBg: #c9d6ff;\n$colorStatusCompleteBg: #a4e4b4;\n$colorAlert: #ff8a0d;\n$colorAlertFg: #fff;\n$colorError: #ff3c00;\n$colorErrorFg: #fff;\n$colorWarningHi: #990000;\n$colorWarningHiFg: #ff9594;\n$colorWarningLo: #ff9900;\n$colorWarningLoFg: #523400;\n$colorDiagnostic: #a4b442;\n$colorDiagnosticFg: #39461a;\n$colorCommand: #3693bd;\n$colorCommandFg: #fff;\n$colorInfo: #2294a2;\n$colorInfoFg: #fff;\n$colorOk: #33cc33;\n$colorOkFg: #fff;\n$colorFilterBg: #a29fe2;\n$colorFilterFg: #fff;\n$colorFilter: $colorFilterBg; // Standalone against $colorBodyBg\n\n// States\n$colorPausedBg: #ff9900;\n$colorPausedFg: #fff;\n\n// Time Colors\n$colorTimeCommonFg: #eee;\n$colorTimeFixed: #59554c;\n$colorTimeFixedBg: $colorTimeFixed;\n$colorTimeFixedFg: #eee;\n$colorTimeFixedFgSubtle: #b2aa98;\n$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);\n$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);\n$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);\n$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;\n$colorTimeFixedBtnBgMajor: #a09375;\n$colorTimeFixedBtnFgMajor: #fff;\n$colorTimeRealtime: #445890;\n$colorTimeRealtimeBg: $colorTimeRealtime;\n$colorTimeRealtimeFg: #fff;\n$colorTimeRealtimeFgSubtle: #eee;\n$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);\n$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);\n$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);\n$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;\n$colorTimeRealtimeBtnBgMajor: #588ffa;\n$colorTimeRealtimeBtnFgMajor: #fff;\n$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor\n$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov\n$timeConductorAxisHoverFilter: brightness(0.8);\n$timeConductorActiveBg: $colorKey;\n$timeConductorActivePanBg: #a0cde1;\n\n// Browse\n$browseFrameColor: pullForward($colorBodyBg, 10%);\n$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing\n$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;\n$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);\n$filterItemHoverFg: brightness(0.9);\n$interiorMarginObjectFrameVertical: 0px;\n$interiorMarginObjectFrameHorizontal: 3px;\n\n// Missing Items\n$filterItemMissing: contrast(0.2);\n$opacityMissing: 0.4;\n$borderMissing: 1px dashed $colorAlert !important;\n\n// Edit\n$editUIColor: $uiColor; // Base color\n$editUIColorBg: $editUIColor;\n$editUIColorFg: #fff;\n$editUIColorHov: pullForward(\n  saturate($uiColor, 10%),\n  20%\n); // Hover color when $editUIColor is applied as a base color\n$editUIBaseColor: #cae1ff; // Base color, toolbar bg\n$editUIBaseColorHov: pushBack($editUIBaseColor, 20%);\n$editUIBaseColorFg: #4c4c4c; // Toolbar button icon colors, etc.\n$editUIAreaBaseColor: pullForward(saturate($editUIBaseColor, 30%), 20%);\n$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area\n$editUIGridColorFg: rgba($editUIBaseColor, 0.3); // Grid lines in layout editing area\n$editDimensionsColor: #d7aeff;\n$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover\n$editFrameBorder: 1px dotted $editFrameColor;\n$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects\n$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames\n$editFrameColorSelected: #ff7c00; // Border of selected frames\n$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout\n$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color\n$editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px;\n$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color\n$editFrameMovebarColorFg: pullForward(\n  $editFrameMovebarColorBg,\n  20%\n); // Grippy lines, container size text\n$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style\n$editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%);\n$editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style\n$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);\n$editFrameMovebarH: 10px; // Height of move bar in layout frame\n$editMarqueeBorder: 1px dashed $editFrameColorSelected;\n$editFrameSelectedBorder: 1px dashed $editMarqueeBorder; // Selected frame element\n\n// Icons\n$colorIconAlias: #4af6f3;\n$colorIconAliasForKeyFilter: #aaa;\n\n// Holders\n$colorTabsHolderBg: rgba($colorBodyFg, 0.2);\n\n// Buttons and Controls\n$colorBtnBg: #aaa;\n$colorBtnBgHov: pullForward($colorBtnBg, 10%);\n$shdwBtnHov: inset rgba(white, 10%) 0 0 0 20px;\n$colorBtnFg: #fff;\n$colorBtnReverseFg: $colorBodyBg;\n$colorBtnReverseBg: $colorBodyFg;\n$colorBtnFgHov: $colorBtnFg;\n$colorBtnMajorBg: $colorKeyBg;\n$colorBtnMajorBgHov: $colorKeyBgHov;\n$colorBtnMajorFg: $colorKeyFg;\n$colorBtnMajorFgHov: pushBack($colorBtnMajorFg, 10%);\n$colorBtnCautionBg: #f16f6f;\n$colorBtnCautionBgHov: #f1504e;\n$colorBtnCautionFg: $colorBtnFg;\n$colorBtnActiveBg: $colorOk;\n$colorBtnActiveFg: $colorOkFg;\n$colorBtnSelectedBg: $colorBtnMajorBg;\n$colorBtnSelectedFg: $colorBtnMajorFg;\n$colorClickIconButton: $colorKey;\n$colorClickIconButtonBgHov: rgba($colorKey, 0.2);\n$colorClickIconButtonFgHov: $colorKeyHov;\n$colorDropHint: $colorKey;\n$colorDropHintBg: pushBack($colorDropHint, 10%);\n$colorDropHintBgHov: pushBack($colorDropHint, 40%);\n$colorDropHintFg: pushBack($colorDropHint, 0);\n$colorDisclosureCtrl: rgba($colorBodyFg, 0.5);\n$colorDisclosureCtrlHov: rgba($colorBodyFg, 0.7);\n$btnStdH: 24px;\n$colorCursorGuide: rgba(black, 0.6);\n$shdwCursorGuide: rgba(white, 0.4) 0 0 2px;\n$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);\n$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects\n$colorSelectFg: $colorBtnFg;\n$colorSelectArw: lighten($colorBtnBg, 20%);\n$shdwSelect: none;\n$controlDisabledOpacity: 0.3;\n\n// Menus\n$colorMenuBg: $colorBodyBg;\n$colorMenuFg: $colorBodyFg;\n$colorMenuIc: $colorKey;\n$filterMenu: brightness(0.95);\n$colorMenuHovBg: $colorMenuIc;\n$colorMenuHovFg: $colorMenuBg;\n$colorMenuHovIc: $colorMenuBg;\n$colorMenuElementHilite: darken($colorMenuBg, 10%);\n$shdwMenu: rgba(black, 0.8) 0 2px 10px;\n$shdwMenuInner: none;\n$shdwMenuText: none;\n$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);\n\n// Palettes and Swatches\n$paletteItemBorderOuterColorSelected: black;\n$paletteItemBorderInnerColorSelected: white;\n$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);\n$mixedSettingBg: (transparent rgba($editUIBaseColorHov, 0.9)); // Used in .c-click-icon--mixed\n$mixedSettingBgSize: 10px;\n\n// Forms\n$colorCheck: $colorKey;\n$colorFormRequired: $colorKey;\n$colorFormValid: $colorOk;\n$colorFormError: #990000;\n$colorFormInvalid: #ff2200;\n$colorFormFieldErrorBg: $colorFormError;\n$colorFormFieldErrorFg: rgba(#fff, 0.6);\n$colorFormLines: rgba(#000, 0.2);\n$colorFormSectionHeaderBg: rgba(#000, 0.05);\n$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.5);\n$colorInputBg: $colorGenBg;\n$colorInputBgHov: rgba($colorGenBg, 0.7);\n$colorInputFg: $colorBodyFg;\n$colorFormText: pushBack($colorBodyFg, 10%);\n$colorInputIcon: pushBack($colorBodyFg, 25%);\n$colorFieldHint: pullForward($colorBodyFg, 40%);\n$shdwInput: inset rgba(black, 0.4) 0 0 2px 1px;\n$shdwInputHov: inset rgba(black, 0.8) 0 0 2px;\n$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;\n$formTBPad: $interiorMargin;\n$formLRPad: $interiorMargin;\n$formInputH: 22px;\n$formRowCtrlsH: 14px;\n\n// Inspector\n$colorInspectorBg: pullForward($colorBodyBg, 5%);\n$colorInspectorFg: $colorBodyFg;\n$colorInspectorPropName: pushBack($colorBodyFg, 20%);\n$colorInspectorPropVal: pullForward($colorInspectorFg, 15%);\n$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);\n$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);\n\n// Tabs\n$colorTabBg: pullForward($colorBodyBg, 15%);\n$colorTabFg: pullForward($colorTabBg, 60%);\n$colorTabCurrentBg: $colorBodyFg; //pullForward($colorTabBg, 10%);\n$colorTabCurrentFg: $colorBodyBg; //pullForward($colorTabFg, 10%);\n$colorTabsBaseline: $colorTabCurrentBg;\n\n// Overlay\n$colorOvrBlocker: rgba(black, 0.7);\n$overlayCr: $interiorMarginLg;\n\n// Indicator colors\n$colorIndicatorAvailable: $colorKey;\n$colorIndicatorDisabled: #444;\n$colorIndicatorOn: $colorOk;\n$colorIndicatorOff: #666;\n$colorIndicatorBgHov: rgba($colorHeadFg, 0.1);\n$colorIndicatorMenuBg: white;\n$colorIndicatorMenuBgShdw: rgba(black, 0.6) 0 0 6px;\n$colorIndicatorMenuFg: $colorHeadFg;\n$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);\n\n// Staleness\n$colorTelemStale: #00c9c9;\n$colorTelemStaleFg: #002a2a;\n$styleTelemStale: italic;\n\n// Limits\n$colorLimitYellowBg: #ffe64d;\n$colorLimitYellowFg: #7f4f20;\n$colorLimitYellowIc: #e7a115;\n$colorLimitOrangeBg: #b36b00;\n$colorLimitOrangeFg: #ffe0b2;\n$colorLimitOrangeIc: #ff9900;\n$colorLimitRedBg: #ff0000;\n$colorLimitRedFg: #fff;\n$colorLimitRedIc: #ffa99a;\n$colorLimitPurpleBg: #891bb3;\n$colorLimitPurpleFg: #edbeff;\n$colorLimitPurpleIc: #c327ff;\n$colorLimitCyanBg: #4ba6b3;\n$colorLimitCyanFg: #d3faff;\n$colorLimitCyanIc: #1795c0;\n\n// Events\n$colorEventPurpleFg: #6f07ed;\n$colorEventRedFg: #aa0000;\n$colorEventOrangeFg: #b84900;\n$colorEventYellowFg: #a98c04;\n$colorEventPurpleBg: #ebe7fb;\n$colorEventRedBg: #fcefef;\n$colorEventOrangeBg: #ffece3;\n$colorEventYellowBg: #fdf8eb;\n$colorEventPurpleLine: $colorEventPurpleFg;\n$colorEventRedLine: $colorEventRedFg;\n$colorEventOrangeLine: $colorEventOrangeFg;\n$colorEventYellowLine: $colorEventYellowFg;\n\n// Bubble colors\n$colorInfoBubbleBg: $colorMenuBg;\n$colorInfoBubbleFg: #666;\n$colorThumbsBubbleFg: pullForward($colorBodyFg, 10%);\n$colorThumbsBubbleBg: pullForward($colorBodyBg, 10%);\n\n// Items\n$colorSelectableItemBg: transparent;\n$colorSelectableItemBgHov: rgba(#000, 0.07);\n$colorItemBg: pushBack($colorBtnBg, 20%);\n$colorItemBgHov: pushBack($colorItemBg, 5%);\n$colorListItemBg: transparent;\n$colorListItemBgHov: $colorSelectableItemBgHov;\n$colorItemFg: $colorBodyFg;\n$colorItemFgDetails: pushBack($colorItemFg, 15%);\n$shdwItemText: none;\n\n// Tabular\n$colorTabBorder: pullForward($colorBodyBg, 10%);\n$colorTabBodyBg: $colorBodyBg;\n$colorTabBodyFg: pullForward($colorBodyFg, 20%);\n$colorTabHeaderBg: #e2e2e2;\n$colorTabHeaderFg: $colorBodyFg;\n$colorTabHeaderBorder: $colorBodyBg;\n$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);\n$colorTabGroupHeaderFg: pullForward($colorTabGroupHeaderBg, 40%);\n$colorSummaryBg: #999;\n$colorSummaryFg: rgba($colorBodyBg, 0.7);\n$colorSummaryFgEm: white;\n\n// Plot\n$colorPlotBg: rgba(black, 0.05);\n$colorPlotFg: $colorBodyFg;\n$colorPlotHash: $colorPlotFg;\n$opacityPlotHash: 0.3;\n$stylePlotHash: dashed;\n$colorPlotAreaBorder: $colorInteriorBorder;\n$colorPlotLabelFg: pushBack($colorPlotFg, 20%);\n$legendHoverValueBg: rgba($colorBodyFg, 0.2);\n$legendTableHeadBg: rgba($colorBodyFg, 0.15);\n$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);\n\n// Gauges\n$colorGaugeBg: pullForward($colorBodyBg, 20%); // Gauge radial area background, meter background\n$colorGaugeValue: rgba(#000, 0.3); // Gauge value graphic (radial sweep, bar) color\n$colorGaugeTextValue: pullForward($colorBodyFg, 20%); // Radial gauge text value\n$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar\n$colorGaugeRange: $colorBodyFg; // Range text color\n$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);\n$colorGaugeLimitLow: $colorGaugeLimitHigh;\n$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.\n$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions\n$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges\n$gaugeMeterValueShadow: rgba(255, 255, 255, 0);\n\n// Time Strip and Lists\n$colorPastBg: #d8d8d8;\n$colorPastFg: #333333;\n$colorPastFgEm: $colorPastFg;\n$colorCurrentBg: #cfcfcf;\n$colorCurrentFg: #333333;\n$colorCurrentFgEm: $colorCurrentFg;\n$colorCurrentBorder: $colorBodyBg;\n$colorFutureBg: #eaeaea;\n$colorFutureFg: #666666;\n$colorFutureFgEm: $colorCurrentFgEm;\n$colorFutureBorder: $colorCurrentBorder;\n$colorInProgressBg: #9cd1e3;\n$colorInProgressFg: $colorCurrentFg;\n$colorInProgressFgEm: $colorCurrentFgEm;\n$colorGanttSelectedBorder: #fff;\n$colorActivityStatusGreen: #4bad4b;\n$colorActivityStatusOrange: #b67e41;\n$opacitySubtle: 0.6;\n$colorEventLine: $colorBodyFg;\n$colorEventLineExtended: rgba($colorEventLine, 0.3);\n$colorTimeStripDraftBg: rgba(#a57748, 0.2);\n$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);\n$colorABAhead: #029d46;\n$colorABBehind: #d58a00;\n$eventLineW: 1px;\n\n// Tree\n$colorTreeBg: transparent;\n$colorItemTreeHoverBg: $colorSelectableItemBgHov;\n$colorItemTreeHoverFg: pullForward($colorBodyFg, 10%);\n$colorItemTreeIcon: $colorKey;\n$colorItemTreeIconHover: $colorItemTreeIcon;\n$colorItemTreeFg: $colorBodyFg;\n$colorItemTreeSelectedBg: $colorSelectedBg;\n$colorItemTreeSelectedFg: $colorItemTreeHoverFg;\n$filterItemTreeSelected: contrast(1.4);\n$colorItemTreeSelectedIcon: $colorItemTreeIcon;\n$colorItemTreeEditingBg: pushBack($editUIColor, 20%);\n$colorItemTreeEditingFg: $editUIColor;\n$colorItemTreeEditingIcon: $editUIColor;\n$colorItemTreeVC: $colorDisclosureCtrl;\n$colorItemTreeVCHover: $colorDisclosureCtrlHov;\n$colorItemTreeNewNode: rgba($colorBodyFg, 0.5);\n$shdwItemTreeIcon: none;\n\n// Layout frame controls\n$frameControlsColorFg: $colorClickIconButton;\n$frameControlsColorBg: $colorMenuBg;\n$frameControlsShdw: $shdwMenu;\n\n// Images\n$colorThumbHoverBg: $colorItemTreeHoverBg;\n\n// Scrollbar\n$scrollbarTrackSize: 7px;\n$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;\n$scrollbarTrackColorBg: rgba(#000, 0.2);\n$scrollbarThumbColor: pullForward($colorBodyBg, 50%);\n$scrollbarThumbColorHov: $colorKey;\n$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);\n$scrollbarThumbColorMenuHov: darken($scrollbarThumbColorMenu, 2%);\n\n// Splitter\n$splitterHandleD: 2px;\n$splitterD: $splitterHandleD;\n$splitterHandleHitMargin: 4px;\n$colorSplitterBaseBg: $colorBodyBg;\n$colorSplitterBg: pullForward($colorSplitterBaseBg, 20%);\n$colorSplitterFg: $colorBodyBg;\n$colorSplitterHover: $colorKey;\n$colorSplitterActive: $colorKey;\n$splitterBtnD: (16px, 35px); // height, width\n$splitterBtnColorBg: $colorBtnBg;\n$splitterBtnColorFg: #ddd;\n$splitterBtnLabelColorFg: #999;\n$splitterCollapsedBtnColorBg: #ccc;\n$splitterCollapsedBtnColorFg: #666;\n$splitterCollapsedBtnColorBgHov: $colorKey;\n$splitterCollapsedBtnColorFgHov: $colorKeyFg;\n\n// Mobile\n$colorMobilePaneLeft: pullForward($colorBodyBg, 2%);\n$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);\n$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;\n$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);\n\n// About Screen\n$colorAboutLink: $colorKeySubtle;\n\n// Loading\n$colorLoadingFg: #776ba2;\n$colorLoadingBg: rgba($colorLoadingFg, 0.1);\n\n// Transitions\n$transInTime: 50ms;\n$transOutTime: 250ms;\n$transIn: all $transInTime ease-in-out;\n$transOut: all $transOutTime ease-in-out;\n$transInTransform: transform $transInTime ease-in-out;\n$transOutTransform: transform $transOutTime ease-in-out;\n$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5);\n$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3);\n\n// Discrete items\n$createBtnTextTransform: uppercase;\n$colorDiscreteItemBg: rgba($colorBodyFg, 0.1);\n$colorDiscreteItemBgHov: rgba($colorBodyFg, 0.2);\n$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3);\n$scrollContainer: rgba(102, 102, 102, 0.1);\n"
  },
  {
    "path": "src/styles/_constants.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/************************** PATHS */\n// Paths need to be relative to /platform/commonUI/theme/<theme-name>/css/ directory\n$dirImgs: 'images/';\n\n/************************** TIMINGS */\n$controlFadeMs: 100ms;\n$browseToEditAnimMs: 400ms;\n$editBorderPulseMs: 500ms;\n$moveBarOutDelay: 500ms;\n\n/************************** SPATIAL */\n$interiorMarginSm: 3px;\n$interiorMargin: 5px;\n$interiorMarginLg: 10px;\n$inputTextPTopBtm: 2px;\n$inputTextPLeftRight: 5px;\n$inputTextP: $inputTextPTopBtm $inputTextPLeftRight;\n$menuLineH: 1.5rem;\n$treeItemIndent: 16px;\n$treeTypeIconW: 18px;\n$overlayOuterMarginFullscreen: (1%, 1%);\n$overlayOuterMarginLarge: (10px, 10px);\n$overlayOuterMarginSmall: (30%, 20%);\n$overlayOuterMarginDialog: (5%, 20%);\n$overlayInnerMargin: 25px;\n$mainViewPad: 0px;\n$treeNavArrowD: 20px;\n$shellMainBrowseBarH: 22px;\n$shellTimeConductorH: 25px;\n$shellToolBarH: 29px;\n$fadeTruncateW: 7px;\n/*************** Items */\n$itemPadLR: 5px;\n$gridItemDesk: 175px;\n$gridItemMobile: 32px;\n/*************** Tabular */\n$tabularHeaderH: 22px;\n$tabularTdPadLR: $itemPadLR;\n$tabularTdPadTB: 2px;\n/*************** Plots */\n$plotYBarW: 60px;\n$plotYLabelMinH: 20px;\n$plotYLabelW: 10px;\n$plotXBarH: 32px;\n$plotLegendH: 20px;\n$plotLegendWidthCollapsed: 20%;\n$plotLegendWidthExpanded: 50%;\n$plotSwatchD: 12px;\n$plotDisplayArea: (0, 0, $plotXBarH, $plotYBarW); // 1: Top, 2: right, 3: bottom, 4: left\n$plotMinH: 95px;\n$controlBarH: 25px;\n/*************** Imagery */\n$imageMainControlBarH: 25px;\n$imageThumbsD: 100px;\n$imageThumbsWrapperH: 155px;\n$imageThumbPad: 1px;\n/*************** Bubbles */\n$bubbleArwSize: 10px;\n$bubblePad: $interiorMargin;\n$bubbleMinW: 100px;\n$bubbleMaxW: 300px;\n/*************** Menus */\n$paletteMenuMinW: 128px; // Min-width of palettes when in a dropdown menu\n/*************** Forms */\n$formLabelMinW: 120px;\n$formLabelW: 30%;\n/*************** Wait Spinner */\n$waitSpinnerD: 32px;\n$waitSpinnerBorderW: 5px;\n$waitSpinnerTreeD: 20px;\n$waitSpinnerTreeBorderW: 3px;\n/*************** Messages */\n$messageIconD: 80px;\n$messageListIconD: 32px;\n/*************** Tables */\n$tableResizeColHitareaD: 6px;\n/*************** Misc */\n$drawingObjBorderW: 3px;\n$tagBorderRadius: 3px;\n/************************** MOBILE */\n$mobileMenuIconD: 24px; // Used\n$mobileTreeItemH: 35px; // Used\n\n/************************** UI ELEMENTS */\n/*************** Progress Bar */\n$colorProgressBarHolder: rgba(black, 0.2);\n$colorProgressBar: #0085ad;\n$progressAnimW: 500px;\n$progressBarMinH: 4px;\n/************************** FONT STYLING */\n$listFontSizes: 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 72, 96, 128;\n\n/************************** GLYPH CHAR UNICODES */\n$glyph-icon-alert-rect: '\\e900';\n$glyph-icon-alert-triangle: '\\e901';\n$glyph-icon-arrow-up: '\\e902';\n$glyph-icon-arrow-double-up: '\\e903';\n$glyph-icon-arrow-tall-up: '\\e904';\n$glyph-icon-arrow-right: '\\e905';\n$glyph-icon-arrow-right-equilateral: '\\e906';\n$glyph-icon-arrow-down: '\\e907';\n$glyph-icon-arrow-double-down: '\\e908';\n$glyph-icon-arrow-tall-down: '\\e909';\n$glyph-icon-arrow-left: '\\e90a';\n$glyph-icon-asterisk: '\\e90b';\n$glyph-icon-bell: '\\e90c';\n$glyph-icon-box-round-corners: '\\e90d';\n$glyph-icon-box-with-arrow: '\\e90e';\n$glyph-icon-check: '\\e90f';\n$glyph-icon-connectivity: '\\e910';\n$glyph-icon-database-in-brackets: '\\e911';\n$glyph-icon-eye-open: '\\e912';\n$glyph-icon-gear: '\\e913';\n$glyph-icon-hourglass: '\\e914';\n$glyph-icon-info: '\\e915';\n$glyph-icon-link: '\\e916';\n$glyph-icon-lock: '\\e917';\n$glyph-icon-minus: '\\e918';\n$glyph-icon-people: '\\e919';\n$glyph-icon-person: '\\e91a';\n$glyph-icon-plus: '\\e91b';\n$glyph-icon-plus-in-rect: '\\e91c';\n$glyph-icon-trash: '\\e91d';\n$glyph-icon-x: '\\e91e';\n$glyph-icon-brackets: '\\e91f';\n$glyph-icon-crosshair: '\\e920';\n$glyph-icon-grippy: '\\e921';\n$glyph-icon-grid: '\\e922';\n$glyph-icon-grippy-ew: '\\e923';\n$glyph-icon-columns: '\\e924';\n$glyph-icon-rows: '\\e925';\n$glyph-icon-filter: '\\e926';\n$glyph-icon-filter-outline: '\\e927';\n$glyph-icon-suitcase: '\\e928';\n$glyph-icon-cursor-lock: '\\e929';\n$glyph-icon-flag: '\\e92a';\n$glyph-icon-eye-disabled: '\\e92b';\n$glyph-icon-notebook-page: '\\e92c';\n$glyph-icon-unlocked: '\\e92d';\n$glyph-icon-circle: '\\e92e';\n$glyph-icon-draft: '\\e92f';\n$glyph-icon-circle-slash: '\\e930';\n$glyph-icon-question-mark: '\\e931';\n$glyph-icon-status-poll-check: '\\e932';\n$glyph-icon-status-poll-caution: '\\e933';\n$glyph-icon-status-poll-circle-slash: '\\e934';\n$glyph-icon-status-poll-question-mark: '\\e935';\n$glyph-icon-status-poll-edit: '\\e936';\n$glyph-icon-stale: '\\e937';\n$glyph-icon-arrows-right-left: '\\ea00';\n$glyph-icon-arrows-up-down: '\\ea01';\n$glyph-icon-bullet: '\\ea02';\n$glyph-icon-calendar: '\\ea03';\n$glyph-icon-chain-links: '\\ea04';\n$glyph-icon-download: '\\ea05';\n$glyph-icon-duplicate: '\\ea06';\n$glyph-icon-folder-new: '\\ea07';\n$glyph-icon-fullscreen-collapse: '\\ea08';\n$glyph-icon-fullscreen-expand: '\\ea09';\n$glyph-icon-layers: '\\ea0a';\n$glyph-icon-line-horz: '\\ea0b';\n$glyph-icon-magnify: '\\ea0c';\n$glyph-icon-magnify-in: '\\ea0d';\n$glyph-icon-magnify-out: '\\ea0e';\n$glyph-icon-menu-hamburger: '\\ea0f';\n$glyph-icon-move: '\\ea10';\n$glyph-icon-new-window: '\\ea11';\n$glyph-icon-paint-bucket: '\\ea12';\n$glyph-icon-pencil: '\\ea13';\n$glyph-icon-pencil-in-brackets: '\\ea14';\n$glyph-icon-play: '\\ea15';\n$glyph-icon-pause: '\\ea16';\n$glyph-icon-plot-resource: '\\ea17';\n$glyph-icon-pointer-left: '\\ea18';\n$glyph-icon-pointer-right: '\\ea19';\n$glyph-icon-refresh: '\\ea1a';\n$glyph-icon-save: '\\ea1b';\n$glyph-icon-save-as: '\\ea1c';\n$glyph-icon-sine: '\\ea1d';\n$glyph-icon-font: '\\ea1e';\n$glyph-icon-thumbs-strip: '\\ea1f';\n$glyph-icon-two-parts-both: '\\ea20';\n$glyph-icon-two-parts-one-only: '\\ea21';\n$glyph-icon-resync: '\\ea22';\n$glyph-icon-reset: '\\ea23';\n$glyph-icon-x-in-circle: '\\ea24';\n$glyph-icon-brightness: '\\ea25';\n$glyph-icon-contrast: '\\ea26';\n$glyph-icon-expand: '\\ea27';\n$glyph-icon-list-view: '\\ea28';\n$glyph-icon-grid-snap-to: '\\ea29';\n$glyph-icon-grid-snap-no: '\\ea2a';\n$glyph-icon-frame-show: '\\ea2b';\n$glyph-icon-frame-hide: '\\ea2c';\n$glyph-icon-import: '\\ea2d';\n$glyph-icon-export: '\\ea2e';\n$glyph-icon-font-size: '\\ea2f';\n$glyph-icon-clear-data: '\\ea30';\n$glyph-icon-history: '\\ea31';\n$glyph-icon-arrow-nav-to-parent: '\\ea32';\n$glyph-icon-crosshair-in-circle: '\\ea33';\n$glyph-icon-target: '\\ea34';\n$glyph-icon-items-collapse: '\\ea35';\n$glyph-icon-items-expand: '\\ea36';\n$glyph-icon-3-dots: '\\ea37';\n$glyph-icon-grid-on: '\\ea38';\n$glyph-icon-grid-off: '\\ea39';\n$glyph-icon-camera: '\\ea3a';\n$glyph-icon-folders-collapse: '\\ea3b';\n$glyph-icon-multiline: '\\ea3c';\n$glyph-icon-singleline: '\\ea3d';\n$glyph-icon-activity: '\\eb00';\n$glyph-icon-activity-mode: '\\eb01';\n$glyph-icon-autoflow-tabular: '\\eb02';\n$glyph-icon-clock: '\\eb03';\n$glyph-icon-database: '\\eb04';\n$glyph-icon-database-query: '\\eb05';\n$glyph-icon-dataset: '\\eb06';\n$glyph-icon-datatable: '\\eb07';\n$glyph-icon-dictionary: '\\eb08';\n$glyph-icon-folder: '\\eb09';\n$glyph-icon-image: '\\eb0a';\n$glyph-icon-layout: '\\eb0b';\n$glyph-icon-object: '\\eb0c';\n$glyph-icon-object-unknown: '\\eb0d';\n$glyph-icon-packet: '\\eb0e';\n$glyph-icon-page: '\\eb0f';\n$glyph-icon-plot-overlay: '\\eb10';\n$glyph-icon-plot-stacked: '\\eb11';\n$glyph-icon-session: '\\eb12';\n$glyph-icon-tabular: '\\eb13';\n$glyph-icon-tabular-lad: '\\eb14';\n$glyph-icon-tabular-lad-set: '\\eb15';\n$glyph-icon-tabular-realtime: '\\eb16';\n$glyph-icon-tabular-scrolling: '\\eb17';\n$glyph-icon-telemetry: '\\eb18';\n$glyph-icon-timeline: '\\eb19';\n$glyph-icon-timer: '\\eb1a';\n$glyph-icon-topic: '\\eb1b';\n$glyph-icon-box-with-dashed-lines: '\\eb1c';\n$glyph-icon-summary-widget: '\\eb1d';\n$glyph-icon-notebook: '\\eb1e';\n$glyph-icon-tabs-view: '\\eb1f';\n$glyph-icon-flexible-layout: '\\eb20';\n$glyph-icon-generator-telemetry: '\\eb21';\n$glyph-icon-generator-events: '\\eb22';\n$glyph-icon-gauge: '\\eb23';\n$glyph-icon-spectra: '\\eb24';\n$glyph-icon-spectra-telemetry: '\\eb25';\n$glyph-icon-command: '\\eb26';\n$glyph-icon-conditional: '\\eb27';\n$glyph-icon-condition-widget: '\\eb28';\n$glyph-icon-alphanumeric: '\\eb29';\n$glyph-icon-image-telemetry: '\\eb2a';\n$glyph-icon-telemetry-aggregate: '\\eb2b';\n$glyph-icon-bar-chart: '\\eb2c';\n$glyph-icon-map: '\\eb2d';\n$glyph-icon-plan: '\\eb2e';\n$glyph-icon-timelist: '\\eb2f';\n$glyph-icon-plot-scatter: '\\eb30';\n$glyph-icon-notebook-shift-log: '\\eb31';\n$glyph-icon-derived-telemetry: '\\eb32';\n\n/************************** GLYPHS AS DATA URI */\n// Only objects have been converted, for use in Create menu and folder views\n$bg-icon-alert-rect: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192z' fill='%23000000'/%3e%3c/svg%3e\");\n$bg-icon-alert-triangle: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M499.1 424.4L287.8 54.6c-17.5-30.6-46-30.6-63.5 0L12.9 424.4C-4.6 455 9.9 480 45.1 480h421.7c35.3 0 49.8-25 32.3-55.6zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V128H299v128z' fill='%23000000'/%3e%3c/svg%3e\");\n$bg-icon-bell: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg fill='%23000000'%3e%3cpath d='M256 512c53 0 96-43 96-96H160c0 53 43 96 96 96zM448 224v-32C448 86 362 0 256 0S64 86 64 192v32c0 35.3-28.7 64-64 64v64h512v-64c-35.3 0-64-28.7-64-64z'/%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-info: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm0 64c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zm96 352H160v-64h32V224h128v128h32v64z' fill='%23000000'/%3e%3c/svg%3e\");\n$bg-icon-plus: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M480,192H320V32A32.1,32.1,0,0,0,288,0H224a32.1,32.1,0,0,0-32,32V192H32A32.1,32.1,0,0,0,0,224v64a32.1,32.1,0,0,0,32,32H192V480a32.1,32.1,0,0,0,32,32h64a32.1,32.1,0,0,0,32-32V320H480a32.1,32.1,0,0,0,32-32V224A32.1,32.1,0,0,0,480,192Z' transform='translate(0)'/%3e%3c/svg%3e\");\n$bg-icon-grippy-ew: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M416 0v512h-64V0zM288 0v512h-64V0zM160 0v512H96V0z'/%3e%3c/svg%3e\");\n$bg-icon-chain-links: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M479.2 32.8C457.3 10.9 428.7 0 400 0c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-37 37-42.7 93.5-17 136.5l-6.4 6.4C215.7 229.3 195.9 224 176 224c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-43.7 43.7-43.7 114.7 0 158.4C54.7 501.1 83.3 512 112 512c28.7 0 57.3-10.9 79.2-32.8l64-64c37-37 42.7-93.5 17-136.5l6.4-6.4c17.6 10.5 37.5 15.8 57.3 15.8 28.7 0 57.3-10.9 79.2-32.8l64-64c43.8-43.8 43.8-114.8.1-158.5zM209.9 369.9l-64 64c-9 9.1-21.1 14.1-33.9 14.1-12.8 0-24.9-5-33.9-14.1-18.7-18.7-18.7-49.2 0-67.9l64-64c9.1-9.1 21.1-14.1 33.9-14.1 2.8 0 5.6.3 8.4.7l-27.8 27.8c-5.2 5.2-8.1 12.1-8.1 19.4s2.9 14.3 8.1 19.4c5.2 5.2 12.1 8.1 19.4 8.1s14.3-2.9 19.4-8.1l27.8-27.8c2.7 15.2-1.8 31.1-13.3 42.5zm224-224l-64 64c-9 9.1-21.1 14.1-33.9 14.1-2.8 0-5.6-.3-8.4-.7l27.8-27.8c5.2-5.2 8.1-12.1 8.1-19.4s-2.9-14.3-8.1-19.4c-5.2-5.2-12.1-8.1-19.4-8.1s-14.3 2.9-19.4 8.1l-27.8 27.8c-2.6-14.9 1.8-30.8 13.3-42.3l64-64C375.1 69 387.2 64 400 64s24.9 5 33.9 14.1c18.8 18.7 18.8 49.1 0 67.8z'/%3e%3c/svg%3e\");\n$bg-icon-clock: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm135 345c-6.4 11.1-18.3 18-31.2 18-6.3 0-12.5-1.7-18-4.8l-110.9-64-.1-.1c-.4-.2-.8-.5-1.2-.7l-.4-.3-.9-.6-.6-.5-.6-.5-.9-.7-.3-.3c-.4-.3-.7-.6-1.1-.9-2.5-2.3-4.7-5-6.5-7.9-.1-.2-.3-.5-.4-.7s-.3-.5-.4-.7c-1.6-3-2.9-6.2-3.6-9.6v-.1c-.1-.5-.2-.9-.3-1.4 0-.1 0-.3-.1-.4-.1-.3-.1-.7-.1-1.1s-.1-.5-.1-.8 0-.5-.1-.8-.1-.8-.1-1.1v-.5-1.4V81c0-19.9 16.1-36 36-36s36 16.1 36 36v161.2l92.9 53.6c17.1 10 22.9 32 13 49.2z'/%3e%3c/svg%3e\");\n$bg-icon-database: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.615 256 0 213.019 0 160v256c0 53.019 114.615 96 256 96s256-42.981 256-96V160c0 53.019-114.615 96-256 96z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e\");\n$bg-icon-database-query: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M341.76 409.643C316.369 423.871 287.118 432 256 432c-97.047 0-176-78.953-176-176S158.953 80 256 80s176 78.953 176 176c0 31.118-8.129 60.369-22.357 85.76l95.846 95.846C509.747 430.661 512 423.429 512 416V96c0-53.019-114.615-96-256-96S0 42.981 0 96v320c0 53.019 114.615 96 256 96 63.055 0 120.774-8.554 165.388-22.73l-79.628-79.627z'/%3e%3cpath fill='%23000000' d='M176 256c0 44.112 35.888 80 80 80s80-35.888 80-80-35.888-80-80-80-80 35.888-80 80z'/%3e%3c/svg%3e\");\n$bg-icon-dataset: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64zM160 448H96V288h64v160zm128 0h-64V288h64v160zm128 0h-64V288h64v160z'/%3e%3c/svg%3e\");\n$bg-icon-datatable: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.6 256 0 213 0 160v256c0 53 114.6 96 256 96s256-43 256-96V160c0 53-114.6 96-256 96zm192 31.5v128c-18.3 7.8-39.9 14.4-64 19.7v-128c24.1-5.3 45.7-11.9 64-19.7zm-320 19.7v128c-24.1-5.2-45.7-11.9-64-19.7v-128c18.3 7.8 39.9 14.4 64 19.7zM192 445V317c20.5 2 41.9 3 64 3s43.5-1.1 64-3v128c-20.5 2-41.9 3-64 3s-43.5-1.1-64-3z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e\");\n$bg-icon-dictionary: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96v160l-64-32-64 32V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96v-96c0 52.8-43.2 96-96 96H96v-96h320z'/%3e%3c/svg%3e\");\n$bg-icon-folder: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z'/%3e%3c/svg%3e\");\n$bg-icon-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zm0 448H64V64h384v384z'/%3e%3cpath fill='%23000000' d='M160 128l-64 64v224h320V256l-64-64-64 64z'/%3e%3c/svg%3e\");\n$bg-icon-layout: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M224 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h128V0zM416 0H288v288.832h224V96c0-52.8-43.2-96-96-96zM288 512h128c52.8 0 96-43.2 96-96v-64.832H288V512z'/%3e%3c/svg%3e\");\n$bg-icon-object: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 512l256-160V160L255.99 0 0 160v192l256 160zm0-416l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e\");\n$bg-icon-object-unknown: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M255-1L-1 159v192l256 160 256-160V159L255-1zm37.7 430.6c-10.6 10.4-23 15.4-38 15.4-15.6 0-28.1-4.9-38.1-14.8-10-10-14.8-22.4-14.8-38.1 0-15.2 5.1-27.6 15.5-38.1s22.6-15.6 37.4-15.6c14.8 0 27.1 5.2 37.8 16 10.7 10.8 15.9 23.2 15.9 38-.1 14.5-5.4 27-15.7 37.2zm26.4-156.3c-11.8 5.9-18.7 11-21.7 16.2-1.8 3.1-3 7.4-3.7 13.4v20.5H213v-22.1c0-20.1 2.2-34.9 6.5-44 4-8.6 11.3-15.1 22.4-20l17.4-7.7c16-7.1 24.1-17.6 24.1-31.4 0-8-3-15.2-8.6-20.9-5.6-5.6-12.8-8.6-20.8-8.6-12 0-27.2 5-31.4 28.7l-1.1 6.1H148l.7-8.1c2-22.3 8.5-41.2 19.4-56.1 9.8-13.5 22.8-24.3 38.5-32.3 15.7-8 32.3-12 49.1-12 30.3 0 55.1 9.7 75.7 29.8 20.6 20 30.6 44 30.6 73.6 0 35.4-14.4 60.7-42.9 74.9z'/%3e%3c/svg%3e\");\n$bg-icon-packet: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 0L0 160v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160L256 0zm0 96l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e\");\n$bg-icon-page: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M352 256c-52.8 0-96-43.2-96-96V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V256H352z'/%3e%3cpath fill='%23000000' d='M384 192h128L320 0v128c0 35.2 28.8 64 64 64z'/%3e%3c/svg%3e\");\n$bg-icon-plot-overlay: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M415 0H97C43.65 0 0 43.65 0 97v203.41c7.09 9.32 12.83 14.17 16 15.42 7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73C403.71 188.64 440.64 124 496 124a69.55 69.55 0 0 1 16 1.87V97c0-53.35-43.65-97-97-97z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73C108.29 323.36 71.36 388 16 388a69.56 69.56 0 0 1-16-1.87V415c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97V211.59c-7.09-9.32-12.83-14.17-16-15.42z'/%3e%3c/svg%3e\");\n$bg-icon-plot-stacked: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M44.8 156c12.49 0 24.48-13.26 42.76-35.09 22.71-27.14 51-60.91 98-60.91 22.32 0 43.31 7.73 62.4 23 14.34 11.45 25.58 25.21 36.46 38.53C303.63 145 314 156 326.4 156H512V97c0-53.35-43.65-97-97-97H97C43.65 0 0 43.65 0 97v59h44.8z'/%3e%3cpath fill='%23000000' d='M264.75 205.2c-14.12-11.32-25.26-25-36-38.14C211 145.32 199.37 132 185.6 132c-12.53 0-24.54 13.27-42.83 35.12-22.7 27.12-51 60.88-98 60.88H0v56h185.6c22 0 42.77 7.67 61.65 22.8 14.12 11.32 25.26 25 36 38.14C301 366.68 312.63 380 326.4 380c12.53 0 24.54-13.27 42.83-35.12 22.7-27.12 51-60.88 98-60.88H512v-56H326.4c-22.03 0-42.77-7.67-61.65-22.8z'/%3e%3cpath fill='%23000000' d='M467.2 356c-12.49 0-24.48 13.26-42.76 35.09-22.71 27.14-51 60.91-98 60.91-22.32 0-43.31-7.73-62.4-23-14.34-11.45-25.58-25.21-36.46-38.53C208.37 367 198 356 185.6 356H0v59c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97v-59h-44.8z'/%3e%3c/svg%3e\");\n$bg-icon-session: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M317.8 262.2c3.3 2.1 6.6 4.3 9.6 6.8l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l67.6-54c.1-2.4.1-4.7.1-7.1 0-26.1-3.9-51.2-11.1-74.9L423.5 243c-29.1 23.3-70.1 29.6-105.7 19.2zM124.3 317.1l60.2-48.2c29-23.2 70-29.6 105.6-19.2-3.3-2.1-6.6-4.3-9.6-6.8l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L103.5 243c-20 16-45.7 24-71.5 24-10.8 0-21.5-1.4-31.9-4.2v.8c2.5 1.7 5 3.4 7.3 5.3l60.2 48.2c14.9 11.9 41.9 11.9 56.7 0z'/%3e%3cpath fill='%23000000' d='M60.3 189.1l60.2-48.2c40.1-32.1 102.8-32.1 142.9 0l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l90.5-72.4C425.2 46.5 346 0 256 0 136.7 0 36.4 81.6 8 192.1c15.4 8.8 38.9 7.8 52.3-3zM344.5 371l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L167.5 371c-20 16-45.7 24-71.5 24-23.9 0-47.7-6.9-67.1-20.7C71.7 456.1 157.3 512 256 512s184.3-55.9 227.1-137.7c-40.2 28.7-99.9 27.6-138.6-3.3z'/%3e%3c/svg%3e\");\n$bg-icon-tabular: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zM320 224H192v-96h128v96zm-128 32h128v96H192v-96zm-32 96H32v-96h128v96zm0-224v96H32v-96h128zM64 480c-8.5 0-16.5-3.3-22.6-9.4S32 456.5 32 448v-64h128v96H64zm128 0v-96h128v96H192zm288-32c0 8.5-3.3 16.5-9.4 22.6S456.5 480 448 480h-96v-96h128v64zm0-96H352v-96h128v96zm0-128H352v-96h128v96z'/%3e%3c/svg%3e\");\n$bg-icon-tabular-lad: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM32 128h128v96H32v-96zm0 128h128v96H32v-96zm32 224c-17.6-.1-31.9-14.4-32-32v-64h128v96H64zm128 0v-96h128v96H192zm288-32c-.1 17.6-14.4 31.9-32 32h-96v-96h128v64zm0-192v96H192v-96h32v-32h-32v-96h288v96h-32v32h32z'/%3e%3cpath fill='%23000000' d='M391.2 273.7L336 246.1V160c0-8.8-7.2-16-16-16s-16 7.2-16 16v105.9l72.8 36.4c7.9 4 17.5.8 21.5-7.2 4-7.8.8-17.5-7.1-21.4z'/%3e%3c/svg%3e\");\n$bg-icon-tabular-lad-set: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M64 384V96c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64H128c-35.3-.1-63.9-28.7-64-64z'/%3e%3cpath fill='%23000000' d='M448 0H160c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM128 96h96v64h-96V96zm0 96h96v96h-96v-96zm32 192c-17.6-.1-31.9-14.4-32-32v-32h96v64h-64zm96 0v-64h96v64h-96zm224-32c-.1 17.6-14.4 31.9-32 32h-64v-64h96v32zm0-64H256V96h224v192z'/%3e%3cpath fill='%23000000' d='M416 240c8.8 0 16-7.2 16-16 0-6.9-4.4-13-10.9-15.2L384 196.5V144c0-8.8-7.2-16-16-16s-16 7.2-16 16v75.5l58.9 19.6c1.7.6 3.4.9 5.1.9z'/%3e%3c/svg%3e\");\n$bg-icon-tabular-scrolling: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64Zm-64 128v96h-96v-96Zm-96 128h96v96h-96Zm-32 96h-96v-96h96Zm0-224v96h-96v-96Zm-224 0h96v96H32Zm0 128h96v96H32Zm32 224a32.2 32.2 0 0 1-32-32v-64h96v96Zm96 0v-96h96v96Zm192 0h-64v-96h96v96Zm118.57-9.43A31.74 31.74 0 0 1 448 480h-32v-32h64a31.74 31.74 0 0 1-9.43 22.57ZM480 384h-64V128h64Z'/%3e%3c/svg%3e\");\n$bg-icon-telemetry: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M16 315.83c7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73 18.7-47.75 49.57-103.57 94.47-116.23A255.87 255.87 0 0 0 256 0C114.62 0 0 114.62 0 256a257.18 257.18 0 0 0 5 50.52c4.77 5.39 8.61 8.37 11 9.31z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73-18.7 47.75-49.57 103.57-94.47 116.23A255.87 255.87 0 0 0 256 512c141.38 0 256-114.62 256-256a257.18 257.18 0 0 0-5-50.52c-4.77-5.39-8.61-8.37-11-9.31z'/%3e%3c/svg%3e\");\n$bg-icon-timeline: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 160V96h128v64Zm64 64h192v64H128Zm320 192H224v-64h224Zm0-128h-64v-64h64Zm0-128H256V96h192Z'/%3e%3c/svg%3e\");\n$bg-icon-timer: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 73.3V32.01a32 32 0 0 0-32-32h-64a32 32 0 0 0-32 32V73.3C67.48 100.84 0 186.54 0 288.01c0 123.71 100.29 224 224 224s224-100.29 224-224c0-101.48-67.5-187.2-160-214.71zm-54 224.71l-131.88 105.5A167.4 167.4 0 0 1 56 288.01c0-92.64 75.36-168 168-168 3.36 0 6.69.11 10 .31v177.69z'/%3e%3c/svg%3e\");\n$bg-icon-box-with-dashed-lines: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 192h64v128H0zM64 64.11l.11-.11H160V0H64A64.19 64.19 0 0 0 0 64v96h64V64.11zM64 447.89V352H0v96a64.19 64.19 0 0 0 64 64h96v-64H64.11zM192 0h128v64H192zM448 447.89l-.11.11H352v64h96a64.19 64.19 0 0 0 64-64v-96h-64v95.89zM448 0h-96v64h95.89l.11.11V160h64V64a64.19 64.19 0 0 0-64-64zM448 192h64v128h-64zM192 448h128v64H192zM128 128h256v256H128z'/%3e%3c/svg%3e\");\n$bg-icon-summary-widget: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM256 384L64 256l192-128 192 128z'/%3e%3c/svg%3e\");\n$bg-icon-notebook: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' xml:space='preserve'%3e%3cpath d='M448 55.4c0-39.9-27.7-63.7-61.5-52.7L0 128h448V55.4zM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64zm-32 256H224V256h192v160z'/%3e%3c/svg%3e\");\n$bg-icon-tabs-view: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 448a64.2 64.2 0 0 0 64 64h384a64.2 64.2 0 0 0 64-64V144H256L230.9 31.2C227.1 14.1 209.6 0 192 0H64A64.2 64.2 0 0 0 0 64zm416-64H96V256h320z'/%3e%3cpath d='M240 0c17.6 0 35.1 14.1 38.9 31.2l18 80.8H512V64a64.2 64.2 0 0 0-64-64z'/%3e%3c/svg%3e\");\n$bg-icon-flexible-layout: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M0 416c0 52.8 43.2 96 96 96h32V224H0zM0 96v64h128V0H96C43.2 0 0 43.2 0 96zM384 512h32c52.8 0 96-43.2 96-96v-64H384zM192 0h128v512H192zM416 0h-32v288h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e\");\n$bg-icon-generator-telemetry: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M76 236.9c5.4-2.1 20.4-17.8 34.9-54.7C126.8 141.5 154.5 93 196 93c19.7 0 38 10.9 54.6 32.5 14 18.2 24.4 40.8 30.5 56.7 14.5 36.9 29.5 52.6 34.9 54.7 5.4-2.1 20.4-17.8 34.9-54.7S388 104.5 421.7 95A192 192 0 0 0 256 0C150 0 64 86 64 192a197.2 197.2 0 0 0 3.7 37.9c3.6 4 6.5 6.3 8.3 7zM442.3 238.5A192.9 192.9 0 0 0 448 192a197.2 197.2 0 0 0-3.7-37.9c-3.6-4-6.5-6.3-8.3-7-5.4 2.1-20.4 17.8-34.9 54.7-10.9 27.9-27.3 59.5-50 76.6z'/%3e%3cpath d='M256 320l67.5-29.5a60.3 60.3 0 0 1-7.5.5c-19.7 0-38-10.9-54.6-32.5-14-18.2-24.4-40.8-30.5-56.7-14.5-36.9-29.5-52.6-34.9-54.7-5.4 2.1-20.4 17.8-34.9 54.7-8.2 21.1-19.6 44.2-34.4 61.6z'/%3e%3cpath d='M512 240L256 352 0 240v160l256 112 256-112V240z'/%3e%3c/svg%3e\");\n$bg-icon-generator-events: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M160 96h192v32H160zM160 224h192v32H160zM160 160h160v32H160z'/%3e%3cpath d='M128 64.1h256V264l64-28V64a64.2 64.2 0 0 0-64-64H128a64.2 64.2 0 0 0-64 64v172l64 28zM329.1 288H182.9l73.1 32 73.1-32z'/%3e%3cpath d='M256 352L0 240v160l256 112 256-112V240L256 352z'/%3e%3c/svg%3e\");\n$bg-icon-gauge: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M256 0C114.6 0 0 114.6 0 256c0 113.2 73.5 209.2 175.3 243L304 256v251.5C422.4 485 512 381 512 256 512 114.6 397.4 0 256 0zm121.4 263.9a159.8 159.8 0 0 0-242.8 0l-73-62.5c4.3-5 8.7-9.8 13.4-14.4a255.9 255.9 0 0 1 362 0c4.7 4.6 9.1 9.4 13.4 14.4z'/%3e%3c/svg%3e\");\n$bg-icon-spectra: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M384 352H128l51.2-89.6L0 288v127c0 53.3 43.7 97 97 97h318c53.4 0 97-43.7 97-97v-31l-162.9-93.1zM415 0H97C43.7 0 0 43.6 0 97v159l200-30.1 56-97.9 54.9 96H512V97a97.2 97.2 0 00-97-97zM512 320v-32l-192-32 192 64z'/%3e%3c/svg%3e\");\n$bg-icon-spectra-telemetry: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 128l54.9 96H510C494.3 97.7 386.5 0 256 0 114.6 0 0 114.6 0 256l200-30.1zM384 352H128l51.2-89.6L2 287.7C17.6 414.1 125.4 512 256 512c100.8 0 188-58.3 229.8-143l-136.7-78.1zM320 256l192 64v-32l-192-32z'/%3e%3c/svg%3e\");\n$bg-icon-command: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M185.1 229.7a96.5 96.5 0 0015.8 11.7A68.5 68.5 0 01192 208c0-19.8 8.9-38.8 25.1-53.7 18.5-17 43.7-26.3 70.9-26.3 20.1 0 39.1 5.1 55.1 14.6a81.3 81.3 0 00-16.2-20.3C308.4 105.3 283.2 96 256 96s-52.4 9.3-70.9 26.3C168.9 137.2 160 156.2 160 176s8.9 38.8 25.1 53.7z'/%3e%3cpath d='M442.7 134.8C422.4 57.5 346.5 0 256 0S89.6 57.5 69.3 134.8C26.3 174.8 0 228.7 0 288c0 123.7 114.6 224 256 224s256-100.3 256-224c0-59.3-26.3-113.2-69.3-153.2zM256 64c70.6 0 128 50.2 128 112s-57.4 112-128 112-128-50.2-128-112S185.4 64 256 64zm0 352c-87.7 0-159.2-63.9-160-142.7 34.4 47.4 93.2 78.7 160 78.7s125.6-31.3 160-78.7c-.8 78.8-72.3 142.7-160 142.7z'/%3e%3c/svg%3e\");\n$bg-icon-conditional: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C114.62 0 0 114.62 0 256s114.62 256 256 256 256-114.62 256-256S397.38 0 256 0zm0 384L64 256l192-128 192 128z'/%3e%3c/svg%3e\");\n$bg-icon-condition-widget: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM256 384L64 256l192-128 192 128z'/%3e%3c/svg%3e\");\n$bg-icon-bar-chart: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM133.82 448H64V224h69.82Zm104.73 0h-69.82V64h69.82Zm104.72 0h-69.82V288h69.82ZM448 448h-69.82V128H448Z'/%3e%3c/svg%3e\");\n$bg-icon-map: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e\");\n$bg-icon-plan: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-timelist: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-plot-scatter: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 176a48 48 0 1 1 48 48 48 48 0 0 1-48-48Zm80 240a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128-96a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm0-160a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128 256a48 48 0 1 1 48-48 48 48 0 0 1-48 48Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-notebook-shift-log: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 55.36c0-39.95-27.69-63.66-61.54-52.68L0 128h448V55.36ZM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64ZM128 416H64v-64h64v64Zm0-96H64v-64h64v64Zm320 96H192v-64h256v64Zm0-96H192v-64h256v64Z'/%3e%3c/svg%3e\");\n$bg-icon-telemetry-aggregate: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 3'%3e%3cpath d='M39 197.72c7-20.72 18.74-50.4 34.6-74.18C92.91 94.65 114.79 80 138.67 80s45.75 14.65 65 43.54c15.86 23.78 27.57 53.46 34.6 74.18 15.44 45.48 31.56 67.49 39 73.27 7.47-5.78 23.6-27.79 39-73.27 7-20.72 18.74-50.4 34.61-74.18q13.9-20.85 29.56-31.75A207.78 207.78 0 0 0 208 0C93.12 0 0 93.12 0 208a208.14 208.14 0 0 0 7.39 55.09c8.39-10.87 20.2-31.67 31.61-65.37Z'/%3e%3cpath d='M377 218.28c-7 20.72-18.74 50.4-34.6 74.18-19.28 28.89-41.16 43.54-65 43.54s-45.75-14.65-65-43.54c-15.86-23.78-27.57-53.46-34.6-74.18-15.44-45.48-31.57-67.49-39-73.27-7.47 5.78-23.6 27.79-39 73.27-7.19 20.72-18.9 50.4-34.8 74.18q-13.9 20.85-29.56 31.75A207.78 207.78 0 0 0 208 416c114.88 0 208-93.12 208-208a208.14 208.14 0 0 0-7.39-55.09c-8.39 10.87-20.2 31.67-31.61 65.37Z'/%3e%3cpath d='M460.78 167.31A258.4 258.4 0 0 1 464 208a255.84 255.84 0 0 1-256 256 258.4 258.4 0 0 1-40.69-3.22A207.23 207.23 0 0 0 304 512c114.88 0 208-93.12 208-208a207.23 207.23 0 0 0-51.22-136.69Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-trash: url(\"data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='512px' height='512px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3e%3cpath d='M416,64h-96.18V32c0-17.6-14.4-32-32-32h-64c-17.6,0-32,14.4-32,32v32H96c-52.8,0-96,36-96,80s0,80,0,80h32v192 c0,52.8,43.2,96,96,96h256c52.8,0,96-43.2,96-96V224h32c0,0,0-36,0-80S468.8,64,416,64z M160,416H96V224h64V416z M288,416h-64V224 h64V416z M416,416h-64V224h64V416z'/%3e%3c/svg%3e\");\n$bg-icon-eye-open: url(\"data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:%2300A14B;%7d %3c/style%3e%3ctitle%3eicon-eye-open-v2%3c/title%3e%3cg%3e%3cpath class='st0' d='M256,58.2c-122.9,0-226.1,84-255.4,197.8C29.9,369.7,133.1,453.8,256,453.8s226.1-84,255.4-197.8 C482.1,142.3,378.9,58.2,256,58.2z M414.6,294.2c-11.3,17.2-25.3,32.4-41.5,45.2c-16.4,12.9-34.5,22.8-54,29.7 c-20.2,7.1-41.4,10.7-63,10.7s-42.9-3.6-63-10.7c-19.5-6.9-37.7-16.9-54-29.7c-16.2-12.8-30.2-27.9-41.5-45.2 c-7.9-12-14.4-24.8-19.3-38.2c5-13.4,11.5-26.2,19.3-38.2c11.3-17.2,25.3-32.4,41.5-45.2c16.4-12.9,34.5-22.8,54-29.7 c20.2-7.1,41.4-10.7,63-10.7s42.9,3.6,63,10.7c19.5,6.9,37.7,16.9,54,29.7c16.2,12.8,30.2,27.9,41.5,45.2 c7.9,12,14.4,24.8,19.3,38.2C429,269.4,422.5,282.2,414.6,294.2z'/%3e%3ccircle class='st0' cx='256' cy='256' r='96'/%3e%3c/g%3e%3c/svg%3e\");\n$bg-icon-camera: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3ctitle%3eicon-camera-v2%3c/title%3e%3cpath d='M448,128H384L320,0H192L128,128H64A64.2,64.2,0,0,0,0,192V448a64.2,64.2,0,0,0,64,64H448a64.2,64.2,0,0,0,64-64V192A64.2,64.2,0,0,0,448,128ZM256,432A128,128,0,1,1,384,304,128,128,0,0,1,256,432Z'/%3e%3c/svg%3e\");\n$bg-icon-derived-telemetry: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M66.1 166c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s9.1-3 20.2-16.3c11.4-13.7 23.1-34.2 33.1-57.7 8.9-21.1 23.8-51.4 44-75.7 23.3-28.1 48.7-42.3 75.6-42.3s52.2 14.2 75.6 42.3c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s1.6-.3 3.2-1.1v-58.9c-.2-141.3-114.9-256-256.2-256H.2v124.6c23.3 3 45.4 17 66 41.7Z'/%3e%3cpath d='M509 387.7c-26.8 0-52.2-14.2-75.6-42.3-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-11.1-13.3-18.5-16.3-20.2-16.3s-9.1 3-20.2 16.3c-11.4 13.7-23.1 34.2-33.1 57.7-8.9 21.1-23.8 51.4-44 75.7-23.3 28.1-48.7 42.3-75.6 42.3s-52.2-14.2-75.6-42.3c-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-4.1-4.9-7.6-8.4-10.6-10.8v54.5c.3 141.4 114.9 256 256.3 256h256V387.6H509Z'/%3e%3c/svg%3e\");\n"
  },
  {
    "path": "src/styles/_controls.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@use 'sass:math';\n\n/******************************************************** CONTROL-SPECIFIC MIXINS */\n@mixin menuOuter() {\n    border-radius: $basicCr;\n    box-shadow: $shdwMenu;\n    @if $shdwMenuInner != none {\n        box-shadow: $shdwMenuInner, $shdwMenu;\n    }\n    background: $colorMenuBg;\n    backdrop-filter: blur($formRowCtrlsH);\n    color: $colorMenuFg;\n    text-shadow: $shdwMenuText;\n    padding: $interiorMarginSm;\n    display: flex;\n    flex-direction: column;\n    position: absolute;\n    z-index: 100;\n\n    > * {\n        flex: 0 0 auto;\n    }\n}\n\n@mixin menuPositioning() {\n    display: flex;\n    flex-direction: column;\n    position: absolute;\n    z-index: 100;\n\n    > * {\n        flex: 0 0 auto;\n    }\n}\n\n@mixin menuInner() {\n    li {\n        @include cControl();\n        justify-content: start;\n        cursor: pointer;\n        display: flex;\n        padding: nth($menuItemPad, 1) nth($menuItemPad, 2);\n        white-space: nowrap;\n\n        @include hover {\n            background: $colorMenuHovBg;\n            color: $colorMenuHovFg;\n            &:before {\n                color: $colorMenuHovIc !important;\n            }\n        }\n\n        &:not(.c-menu--no-icon &) {\n            &:before {\n                color: $colorMenuIc;\n                font-size: 1em;\n                margin-right: $interiorMargin;\n                min-width: 1em;\n            }\n\n            &:not([class*='icon']):before {\n                content: ''; // Enable :before so that menu items without an icon still indent properly\n            }\n        }\n    }\n}\n\n@mixin resizeHandleStyle($size: 1px, $margin: 3px) {\n  // Used by Flexible Layouts and Time Strip views\n  &:before {\n    // The visible resize line\n    background-color: $editUIColor;\n    content: '';\n    display: block;\n    @include abs();\n    min-height: $size;\n    min-width: $size;\n  }\n\n  &:hover {\n    &:before {\n      // The visible resize line\n      background-color: $editUIColorHov;\n    }\n  }\n\n  &.vertical {\n    // Resizes in Y dimension\n    padding: $margin $size;\n    &:before {\n      top: 50%;\n      bottom: auto;\n      transform: translateY(-50%);\n    }\n    &:hover {\n      cursor: row-resize;\n    }\n  }\n\n  &.horizontal {\n    // Resizes in X dimension\n    padding: $size $margin;\n    &:before {\n      left: 50%;\n      right: auto;\n      transform: translateX(-50%);\n    }\n    &:hover {\n      cursor: col-resize;\n    }\n  }\n}\n\n/******************************************************** BUTTONS */\n// Optionally can include icon in :before via markup\nbutton {\n    @include htmlInputReset();\n}\n\n.c-button,\n.c-button--menu {\n    @include cButton();\n}\n\n.c-button {\n    &--menu {\n        &:after {\n            content: $glyph-icon-arrow-down;\n            font-family: symbolsfont;\n            opacity: 0.5;\n        }\n    }\n\n    &--swatched {\n        // Used with c-button--menu: a visual button with a larger swatch element adjacent to an icon\n        .c-swatch {\n            $d: 12px;\n            margin-left: $interiorMarginSm;\n            height: $d;\n            width: $d;\n        }\n    }\n\n    &[class*='__collapse-button'] {\n        box-shadow: none;\n        background: $splitterBtnColorBg;\n        color: $splitterBtnColorFg;\n        border-radius: $smallCr;\n        line-height: 90%;\n        padding: 3px 10px;\n\n        @include desktop() {\n            font-size: 6px;\n        }\n\n        &:before {\n            content: $glyph-icon-arrow-down;\n            font-size: 1.1em;\n        }\n    }\n\n    &.is-active {\n        background: $colorBtnActiveBg;\n        color: $colorBtnActiveFg;\n    }\n\n    &.is-selected {\n        background: $colorBtnSelectedBg;\n        color: $colorBtnSelectedFg;\n    }\n}\n\n/********* Icon Buttons and Links */\n.c-click-icon {\n    @include cClickIcon();\n\n    &--section-collapse {\n        color: inherit;\n        display: block;\n        transition: transform $transOutTime;\n\n        &:before {\n            content: $glyph-icon-arrow-down;\n            font-family: symbolsfont;\n        }\n\n        &.is-collapsed {\n            transform: rotate(180deg);\n        }\n    }\n}\n\n.c-click-link,\n.c-icon-link {\n    // A clickable element, typically inline, with an icon and label\n    @include cControl();\n    cursor: pointer;\n}\n\n.c-icon-button,\n.c-click-swatch {\n    @include cClickIconButton();\n\n    &--menu {\n        @include hasMenu();\n    }\n}\n\n.c-icon-button--disabled {\n    @include cClickIconButtonLayout();\n}\n\n.c-icon-link {\n    &:before {\n        // Icon\n        //color: $colorBtnMajorBg;\n    }\n}\n\n.c-icon-button {\n    [class*='label'] {\n        padding: 1px 0;\n    }\n\n    &--mixed {\n        @include mixedBg();\n    }\n\n    &--swatched {\n        // Color control, show swatch element\n        display: flex;\n        flex-flow: column nowrap;\n        align-items: center;\n        justify-content: center;\n\n        > [class*='swatch'] {\n            box-shadow: inset rgba($editUIBaseColorFg, 0.2) 0 0 0 1px;\n            flex: 0 0 auto;\n            height: 5px;\n            width: 100%;\n            margin-top: 1px;\n        }\n\n        &:before {\n            // Reduce size of icon to make a bit of room\n            flex: 1 1 auto;\n            font-size: 1.1em;\n        }\n    }\n\n    &--sm {\n        padding: $interiorMarginSm $interiorMargin;\n    }\n}\n\n.c-list-button {\n    @include cControl();\n    color: $colorBodyFg;\n    cursor: pointer;\n    justify-content: start;\n    padding: $interiorMargin;\n\n    > * + * {\n        margin-left: $interiorMargin;\n    }\n\n    @include hover() {\n        background: $colorItemTreeHoverBg;\n    }\n\n    .c-button {\n        flex: 0 0 auto;\n    }\n}\n\n.c-not-button {\n    // Use within a holder that's clickable; use to indicate interactability\n    @include cButtonLayout();\n    cursor: pointer;\n}\n\n/******************************************************** DISCLOSURE CONTROLS */\n/********* Disclosure Button */\n// Provides a downward arrow icon that when clicked displays additional options and/or info.\n// Always placed AFTER an element\n.c-disclosure-button {\n    @include cClickIcon();\n    margin-left: $interiorMarginSm;\n\n    &:before {\n        content: $glyph-icon-arrow-down;\n        font-family: symbolsfont;\n        font-size: 0.7em;\n    }\n}\n\n/********* Disclosure Triangle */\n// Provides an arrow icon that when clicked expands an element to reveal its contents.\n// Used in tree items, plot legends. Always placed BEFORE an element.\n.c-disclosure-triangle {\n    $d: 12px;\n    color: $colorDisclosureCtrl;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex: 0 0 auto;\n    width: $d;\n    position: relative;\n    visibility: hidden;\n\n    &.is-enabled {\n        cursor: pointer;\n        visibility: visible;\n\n        &:hover {\n            color: $colorDisclosureCtrlHov;\n        }\n\n        &:before {\n            $s: 0.65;\n            content: $glyph-icon-arrow-right-equilateral;\n            display: block;\n            font-family: symbolsfont;\n            font-size: 1rem * $s;\n        }\n    }\n\n    &--expanded {\n        &:before {\n            transform: rotate(90deg);\n        }\n    }\n}\n\n/******************************************************** DRAG AFFORDANCES */\n.c-grippy {\n    $d: 10px;\n    @include grippy($c: $colorItemTreeVC, $dir: 'y');\n    width: $d;\n    height: $d;\n\n    &--vertical-drag {\n        cursor: ns-resize;\n    }\n}\n\n/******************************************************** SECTION */\nsection {\n    flex: 0 1 auto;\n    overflow: hidden;\n\n    + section {\n        //margin-top: $interiorMargin;\n    }\n\n    .c-section__header {\n        @include propertiesHeader();\n        display: flex;\n        flex: 0 0 auto;\n        align-items: center;\n        gap: $interiorMargin;\n    }\n\n    > [class*='__label'] {\n        flex: 1 1 auto;\n        text-transform: uppercase;\n    }\n}\n\n/******************************************************** FORM ELEMENTS */\ninput,\ntextarea {\n    font-family: inherit;\n    font-weight: inherit;\n    letter-spacing: inherit;\n}\n\ninput[type='text'],\ninput[type='search'],\ninput[type='number'],\ninput[type='password'],\ninput[type='date'],\ntextarea {\n    @include reactive-input();\n\n    &.numeric {\n        text-align: right;\n    }\n}\n\ninput[type='text'],\ninput[type='search'],\ninput[type='password'],\ninput[type='date'],\ntextarea {\n    padding: $inputTextP;\n}\n\n.c-input {\n    &--flex {\n        width: 100%;\n        min-width: 20px;\n    }\n\n    &--datetime {\n        // Sized for values such as 2018-09-28 22:32:33.468Z\n        width: 160px;\n    }\n\n    &--hrs-min-sec {\n        // Sized for values such as 00:25:00\n        width: 60px;\n    }\n\n    &-inline,\n    &--inline {\n        // A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus\n        @include inlineInput;\n\n        &:hover,\n        &:focus {\n            background: $colorInputBg;\n            padding-left: $inputTextPLeftRight;\n            padding-right: $inputTextPLeftRight;\n        }\n    }\n\n    &--labeled {\n        // TODO: replace .c-labeled-input with this\n        // An input used in the Toolbar\n        // Assumes label is before the input\n        @include cControl();\n\n        input {\n            margin-left: $interiorMarginSm;\n        }\n    }\n\n    &--sm {\n        // Small inputs, like small numerics\n        width: 40px;\n    }\n\n    &--md {\n        // Smallish inputs, like numerics or short text\n        width: 80px;\n    }\n\n    &--autocomplete {\n        &__wrapper {\n            display: flex;\n            flex-direction: row;\n            align-items: center;\n            overflow: hidden;\n            width: 100%;\n        }\n\n        &__input {\n            min-width: 100px;\n            width: 100%;\n\n            // Fend off from afford-arrow\n            padding-right: 2.5em !important;\n        }\n\n        &__options {\n            @include menuOuter();\n            @include menuInner();\n            display: flex;\n\n            ul {\n                flex: 1 1 auto;\n                overflow: auto;\n            }\n\n            li {\n                &:before {\n                    color: var(--optionIconColor) !important;\n                    font-size: 0.8em !important;\n                }\n            }\n        }\n\n        &__afford-arrow {\n            $p: 2px;\n            font-size: 0.8em;\n            padding-bottom: $p;\n            padding-top: $p;\n            position: absolute;\n            right: 2px;\n            z-index: 2;\n        }\n    }\n}\n\ninput[type='number'].c-input-number--no-spinners {\n    &::-webkit-inner-spin-button,\n    &::-webkit-outer-spin-button {\n        -webkit-appearance: none;\n        margin: 0;\n    }\n\n    -moz-appearance: textfield;\n}\n\n.c-labeled-input {\n    // An input used in the Toolbar\n    // Assumes label is before the input\n    @include cControl();\n\n    input {\n        margin-left: $interiorMarginSm;\n    }\n}\n\n.c-scrollcontainer {\n    @include nice-input();\n    margin-top: $interiorMargin;\n    background: $scrollContainer;\n    border-radius: $controlCr;\n    overflow: auto;\n    padding: $interiorMarginSm;\n}\n\n// SELECTS\nselect {\n    @include appearanceNone();\n    background-color: $colorSelectBg;\n    background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e\");\n    color: $colorSelectFg;\n    box-shadow: $shdwSelect;\n    background-repeat: no-repeat, no-repeat;\n    background-position: right 0.4em top 80%,\n    0 0;\n    border: none;\n    border-radius: $controlCr;\n    padding: 2px 20px 2px $interiorMargin;\n\n    *,\n    option {\n        background: $colorBtnBg;\n        color: $colorBtnFg;\n    }\n}\n\n// CHECKBOX LISTS\n// __input followed by __label\n.c-checkbox-list {\n    // Rows\n    &__row + &__row {\n        margin-top: $interiorMarginSm;\n    }\n\n    // input and label in each __row\n    &__row {\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    li {\n        white-space: nowrap;\n    }\n}\n\n/******************************************************** TABS */\n.c-tabs {\n    // Single horizontal strip of tabs, with a bottom divider line\n    @include userSelectNone();\n    display: flex;\n    flex: 0 0 auto;\n    flex-wrap: wrap;\n    position: relative; // Required in case this is applied to a <ul>\n\n    &:before {\n        // Separator line at bottom of tabs\n        content: '';\n        display: block;\n        height: 1px;\n        width: 100%;\n        background: $colorTabsBaseline;\n        position: absolute;\n        bottom: 0px;\n        z-index: 1;\n    }\n}\n\n.c-tab {\n    // Used in Tab View, generic tabs\n    $notchSize: 7px;\n    $clipPath: polygon(\n                    0% 0%,\n                    calc(100% - #{$notchSize}) 0%,\n                    100% #{$notchSize},\n                    100% calc(100% - #{$notchSize}),\n                    100% 100%,\n                    0% 100%\n    );\n    background: $colorTabBg;\n    color: $colorTabFg;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    flex: 1 1 auto;\n    margin: 1px 1px 0 0;\n    padding: $interiorMargin $interiorMarginLg;\n    white-space: nowrap;\n    clip-path: $clipPath;\n    -webkit-clip-path: $clipPath; // Safari\n\n    > * + * {\n        margin-left: $interiorMargin;\n    }\n\n    @include hover() {\n        box-shadow: $shdwBtnHov;\n    }\n\n    &.is-current {\n        background: $colorTabCurrentBg;\n        color: $colorTabCurrentFg;\n    }\n}\n\n/******************************************************** HYPERLINKS AND HYPERLINK BUTTONS */\n.c-hyperlink {\n    display: inline-block;\n    color: $colorKey;\n\n    &--button {\n        @include cButton();\n    }\n}\n\n.c-so-view--hyperlink.c-so-view--no-frame {\n    .c-hyperlink--button {\n        @include abs();\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 0;\n    }\n\n    .c-so-view__frame-controls {\n        display: none;\n    }\n}\n\n/******************************************************** MENUS */\n.c-menu {\n    @include menuOuter();\n    @include menuPositioning();\n    @include menuInner();\n\n    &__section-hint {\n        $m: $interiorMargin;\n        margin: 0 0 $m 0;\n        padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);\n\n        opacity: 0.6;\n        font-size: 0.9em;\n        font-style: italic;\n    }\n\n    &__section-separator {\n        $m: $interiorMargin;\n        border-top: 1px solid $colorInteriorBorder;\n        margin: $m 0;\n        padding: 0 nth($menuItemPad, 2) 0 nth($menuItemPad, 2);\n    }\n}\n\n.c-super-menu {\n    // Two column layout, menu items on left with detail of hover element on right\n    $m: $interiorMarginLg;\n    @include menuOuter();\n    @include menuPositioning();\n    display: flex;\n    height: auto;\n    padding: $interiorMarginLg;\n    flex-direction: row;\n\n    > [class*='__'] {\n        //flex: 1 1 50%;\n        //&:first-child {\n        //  margin-right: $m;\n        //}\n\n        &:last-child {\n            //border-left: 1px solid $colorInteriorBorder;\n            //padding-left: $m;\n        }\n    }\n\n    &__menu {\n        @include menuInner();\n        flex: 1 1 50%;\n        margin-right: $m;\n        overflow: auto;\n\n        ul {\n            margin-right: $interiorMarginSm; // Fend off scrollbar\n        }\n\n        li {\n            border-radius: $controlCr;\n        }\n    }\n\n    &__item-description {\n        border-left: 1px solid $colorInteriorBorder;\n        flex: 1 1 50%;\n        padding-left: $m;\n        display: flex;\n        flex-direction: column;\n        justify-content: stretch;\n\n        > * + * {\n            margin-top: $interiorMarginLg;\n        }\n\n        .l-item-description {\n            &__icon {\n                min-height: 20%;\n                margin: 10% 25%;\n            }\n\n            &__name {\n                color: $colorMenuFg;\n                flex: 0 0 auto;\n                font-size: 1.25em;\n            }\n\n            &__description {\n                font-size: $fontBaseSize;\n            }\n        }\n    }\n}\n\n.c-super-menu--sm {\n    // Small version of the super menu, used by the compact Time Conductor\n    height: 120px;\n    width: 500px;\n\n    .c-super-menu__menu {\n        flex: 1 1 30%;\n    }\n\n    .c-super-menu__item-description {\n        flex: 1 1 70%;\n\n        [class*='__icon'] {\n            display: none !important;\n        }\n\n        [class*='__name'] {\n            margin-top: 0 !important;\n        }\n\n        [class*='__item-description'] {\n            min-width: 200px;\n        }\n    }\n}\n\n/******************************************************** CONTROL BARS */\n.c-control-bar {\n    display: flex;\n    align-items: center;\n\n    > * + * {\n        margin-left: $interiorMarginSm;\n    }\n\n    &__label {\n        display: inline-block;\n        white-space: nowrap;\n    }\n}\n\n/******************************************************** PALETTES */\n.c-palette {\n    display: flex;\n    flex-flow: column nowrap;\n    line-height: normal;\n\n    &__items {\n        display: grid;\n        grid-gap: 1px;\n        grid-template-columns: repeat(auto-fill, 12px);\n        flex: 1 1 auto;\n\n        .c-menu & {\n            min-width: $paletteMenuMinW;\n        }\n    }\n\n    &__item {\n        $d: 12px;\n        border: 1px solid transparent;\n        cursor: pointer;\n        display: flex;\n        align-items: center;\n        align-content: center;\n        width: $d;\n        height: $d;\n        text-align: center;\n\n        &.is-selected {\n            border-width: 1px;\n        }\n    }\n\n    &__item-none {\n        @include userSelectNone();\n        flex: 0 0 auto;\n        display: flex;\n        align-items: center;\n        margin-bottom: $interiorMarginSm;\n\n        .c-palette__item {\n            @include noColor();\n            border-color: $paletteItemBorderInnerColor;\n            margin-right: $interiorMarginSm;\n        }\n    }\n\n    &--color {\n        .c-palette__item {\n            &:hover {\n                border-color: rgba($paletteItemBorderOuterColorSelected, 0.7);\n                box-shadow: inset rgba($paletteItemBorderInnerColorSelected, 0.7) 0 0 0 1px;\n            }\n\n            &.is-selected {\n                border-color: $paletteItemBorderOuterColorSelected !important;\n                box-shadow: inset rgba($paletteItemBorderInnerColorSelected, 1) 0 0 0 1px;\n            }\n        }\n    }\n\n    &--icon {\n        .c-palette__items {\n            grid-gap: 3px;\n        }\n\n        .c-palette__item {\n            border-radius: $smallCr;\n            font-size: 0.6rem;\n\n            &:before {\n                display: block;\n                width: 100%;\n            }\n\n            &:hover {\n                box-shadow: rgba($paletteItemBorderInnerColorSelected, 0.3) 0 0 0 1px;\n            }\n\n            &.is-selected {\n                box-shadow: rgba($paletteItemBorderInnerColorSelected, 0.6) 0 0 0 1px;\n            }\n        }\n    }\n}\n\n/******************************************************** SWATCHES */\n.c-color-swatch {\n    border: 1px solid rgba(#fff, 0.2);\n    box-shadow: rgba(#000, 0.2) 0 0 0 1px;\n}\n\n/******************************************************** TOOLBAR */\n.c-ctrl-wrapper {\n    @include cCtrlWrapper();\n}\n\n.c-toolbar,\n.c-toolbar .c-ctrl-wrapper {\n    display: flex;\n    align-items: stretch;\n}\n\n@mixin cToolbarSeparator() {\n    $m: 1px;\n    $b: 1px;\n    display: block;\n    width: $m + $b; // Allow for border\n    border-right: $b solid $colorInteriorBorder;\n    margin-right: $m;\n}\n\n.c-separator {\n    @include cToolbarSeparator();\n    height: 100%;\n}\n\n.c-row-separator {\n    border-top: 1px solid $colorInteriorBorder;\n    height: 1px;\n}\n\n.c-toolbar {\n    display: flex;\n    align-items: center;\n\n    > * {\n        // First level items\n        display: flex;\n\n        > * + * {\n            margin-left: 2px;\n        }\n    }\n\n    &__separator {\n        @include cToolbarSeparator();\n    }\n\n    .c-icon-button,\n    .c-labeled-input {\n        color: $editUIBaseColorFg;\n    }\n\n    .c-icon-button {\n        @include cControl();\n        $pLR: $interiorMargin - 1;\n        $pTB: 2px;\n        padding: $pTB $pLR;\n\n        @include hover() {\n            background: $editUIBaseColorHov !important;\n            color: $editUIBaseColorFg !important;\n        }\n\n        &--menu {\n            $p: 4px;\n            padding-top: $p;\n            padding-bottom: $p;\n        }\n\n        &--swatched {\n            padding-bottom: floor(math.div($pTB, 2));\n            width: 2em; // Standardize the width\n        }\n\n        &[class*='--caution'] {\n            &:before {\n                color: $colorBtnCautionBg;\n            }\n\n            @include hover() {\n                background: rgba($colorBtnCautionBgHov, 0.2);\n                :before {\n                    color: $colorBtnCautionBgHov;\n                }\n            }\n        }\n    }\n\n    .c-labeled-input {\n        font-size: 0.9em;\n\n        input[type='number'] {\n            width: 40px; // Number input sucks and must have size set using this method\n        }\n\n        + .c-labeled-input {\n            margin-left: $interiorMargin;\n        }\n    }\n}\n\n/********* Button Sets */\n.c-button-set {\n    display: inline-flex;\n    flex: 0 0 auto;\n\n    > * {\n        // Assume buttons are immediate descendants\n        flex: 0 0 auto;\n    }\n\n    &[class*='--strip'] {\n        // Buttons are smashed together with minimal margin\n        // c-buttons don't have border-radius between buttons, creates a tool-button-strip look\n        // c-icon-buttons get grouped more closely together\n        [class^='c-button'] {\n            // Only apply the following to buttons that have background, eg. c-button\n            border-radius: 0;\n        }\n    }\n\n    &[class*='--strip-h'] {\n        // Horizontal strip\n\n        + .c-button-set {\n            margin-left: $interiorMargin;\n        }\n\n        [class^='c-button'] {\n            + * {\n                margin-left: 1px;\n            }\n\n            &:first-child {\n                border-top-left-radius: $controlCr;\n                border-bottom-left-radius: $controlCr;\n            }\n\n            &:last-child {\n                border-top-right-radius: $controlCr;\n                border-bottom-right-radius: $controlCr;\n            }\n        }\n    }\n}\n\n/******************************************************** STYLE EDITING */\n.c-style {\n    display: flex;\n    flex: 1 1 auto;\n    align-items: center;\n    justify-content: space-between;\n\n    &__controls {\n        // Holds thumb, icon buttons\n        display: flex;\n        flex: 1 0 auto;\n\n        > * + * {\n            margin-left: $interiorMargin;\n        }\n    }\n\n    &__button-save,\n    &__button-delete {\n        // Holds save and delete buttons accordingly\n        flex: 0 0 auto;\n    }\n\n    &--saved {\n        border-radius: $controlCr;\n        padding: $interiorMargin !important;\n        cursor: pointer;\n\n        @include hover {\n            background: rgba($editUIBaseColorHov, 0.3);\n        }\n\n        .c-style__controls {\n            [class*='button'] {\n                pointer-events: none;\n\n                &:before {\n                    opacity: $controlDisabledOpacity;\n                }\n            }\n        }\n    }\n}\n\n.c-style-thumb {\n    background-size: cover;\n    border: 1px solid transparent;\n    border-radius: $basicCr;\n    box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;\n    flex: 0 0 auto;\n    padding: $interiorMargin;\n    text-align: center;\n    width: 50px;\n\n    &--mixed {\n        @include mixedBg();\n    }\n}\n\n/******************************************************** SLIDERS */\n.c-slider {\n    @include cControl();\n\n    > * + * {\n        margin-left: $interiorMargin;\n    }\n}\n\n/******************************************************** SLIDERS AND RANGE */\n@mixin sliderKnobRound($h: 12px) {\n    @include themedButton();\n    cursor: pointer;\n    width: $h;\n    height: $h;\n    border-radius: 50% !important;\n}\n\n@mixin sliderTrack($bg: $scrollbarTrackColorBg, $knobH: 12px, $trackH: 3px) {\n    border-radius: 2px;\n    $breakPointPx: floor(math.div($knobH - $trackH, 2));\n    $bp1: $breakPointPx;\n    $bp2: $breakPointPx + $trackH;\n    box-sizing: border-box;\n    // For cross-browser compatibility, the track needs to be the same height as the knob.\n    height: $knobH;\n    // Gradient visually adds a horizontal line smaller than the knob\n    background: linear-gradient(0deg, rgba($bg, 0) $bp1, $bg $bp1, $bg $bp2, rgba($bg, 0) $bp2);\n}\n\ninput[type='range'] {\n    // HTML5 range inputs\n    $knobH: 11px;\n    $trackH: 3px;\n    -webkit-appearance: none; /* Hides the slider so that custom slider can be made */\n    background: transparent; /* Otherwise white in Chrome */\n\n    &:focus {\n        outline: none; /* Removes the blue border. */\n    }\n\n    // Thumb\n    &::-webkit-slider-thumb {\n        -webkit-appearance: none;\n        @include sliderKnobRound($knobH);\n    }\n\n    &::-moz-range-thumb {\n        border: none;\n        @include sliderKnobRound($knobH);\n    }\n\n    &::-ms-thumb {\n        border: none;\n        @include sliderKnobRound($knobH);\n    }\n\n    // Track\n    &::-webkit-slider-runnable-track {\n        width: 100%;\n        @include sliderTrack($knobH: $knobH, $trackH: $trackH);\n    }\n\n    &::-moz-range-track {\n        width: 100%;\n        @include sliderTrack($knobH: $knobH, $trackH: $trackH);\n    }\n}\n\n/******************************************************** LOCAL CONTROLS */\n.h-local-controls {\n    // Holder for local controls\n    &--horz {\n        // Horizontal layout be\n        display: inline-flex;\n        align-items: center;\n    }\n\n    [class*='--menus-aligned'] {\n        // Contains top level elements that hold dropdown menus\n        // Top level elements use display: contents to allow their menus to compactly align\n        // 03-18-22: used in ImageControls.vue\n        display: flex;\n        flex-direction: row;\n    }\n}\n\n.c-local-controls {\n    // Controls that are in close proximity to an element they effect\n    &--show-on-hover {\n        // Hidden by default; requires a hover 1 - 3 levels above to display\n        @include transition(opacity, $transOutTime);\n        opacity: 0;\n        pointer-events: auto;\n    }\n}\n\n.has-local-controls:hover {\n    > .c-local-controls--show-on-hover,\n    > * > .c-local-controls--show-on-hover,\n    > * > * > .c-local-controls--show-on-hover,\n    > * > * > * > .c-local-controls--show-on-hover {\n        @include transition(opacity);\n        opacity: 1;\n        pointer-events: inherit;\n\n        &[disabled] {\n            opacity: $controlDisabledOpacity;\n        }\n    }\n}\n\n/***************************************************** DRAG AND DROP */\n.c-drop-hint {\n    // Used in Tabs View, Flexible Grid Layouts\n    @include abs();\n    @include transition($prop: background-color, $dur: $transOutTime);\n    background-color: $colorDropHintBg;\n    color: $colorDropHintFg;\n    border-radius: $basicCr;\n    border: 1px dashed $colorDropHintFg;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    z-index: 50;\n\n    &:not(.c-drop-hint--always-show) {\n        opacity: 0; // Must use this (rather than display: none) to enable transition effects\n        pointer-events: none;\n    }\n\n    &:before {\n        $h: 80%;\n        $mh: 25px;\n        background: $bg-icon-plus;\n        background-size: contain;\n        background-position: center center;\n        background-repeat: no-repeat;\n        content: '';\n        display: block;\n        filter: $colorKeyFilterHov;\n        height: $h;\n        width: $h;\n        max-height: $mh;\n        max-width: $mh;\n    }\n\n    .is-dragging &,\n    &.is-dragging {\n        pointer-events: inherit;\n        opacity: 0.8;\n    }\n\n    .is-mouse-over &,\n    &.is-mouse-over {\n        background-color: $colorDropHintBgHov;\n        opacity: 0.9;\n    }\n}\n\n/***************************************************** DISCRETE ITEMS */\n// OUTPUT\n// Element that showcases an output value. Used in Condition Sets and Derived Telemetry\n\n.c-output-featured {\n    display: flex;\n    gap: $interiorMargin;\n    padding: $interiorMargin 0;\n\n    > * {\n        padding: $interiorMargin;\n    }\n\n    &__label {\n        flex: 0 0 auto;\n        text-transform: uppercase;\n    }\n\n    &__value {\n        $p: $interiorMargin * 2;\n        padding-left: $p;\n        padding-right: $p;\n        background: rgba(black, 0.2);\n        border-radius: $basicCr;\n    }\n}\n\n/***************************************************** LEGACY */\n.l-btn-set {\n    display: flex;\n    align-items: center;\n}\n"
  },
  {
    "path": "src/styles/_forms.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n@mixin validationState($sym, $c) {\n  @include glyphAfter($sym);\n  &:after {\n    color: $c;\n    margin-left: $interiorMargin;\n  }\n}\n\n.c-form {\n  color: $colorFormText;\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 auto;\n  overflow: hidden;\n\n  > * + * {\n    margin-top: $interiorMarginLg !important;\n  }\n\n  &__top-bar,\n  &__bottom-bar {\n    flex: 0 0 auto;\n  }\n\n  &__contents {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    overflow: auto;\n    padding-right: $interiorMargin;\n  }\n\n  &__section {\n    display: contents;\n  }\n\n  &__row {\n    display: flex;\n    padding: $formTBPad 0;\n    &:not(.first) {\n      border-top: 1px solid $colorFormLines;\n    }\n    flex: 0 0 auto;\n\n    &.grows {\n      flex: 1 1 auto;\n    }\n  }\n\n  &__section-header {\n    border-radius: $basicCr;\n    background: $colorFormSectionHeaderBg;\n    color: $colorFormSectionHeaderFg;\n    flex: 0 0 auto;\n    font-size: inherit;\n    font-weight: normal;\n    margin: $interiorMargin 0;\n    padding: $formTBPad $formLRPad;\n    text-transform: uppercase;\n  }\n\n  &--sub-grid {\n    // 3 columns: <req> <label> <input/control>\n    display: grid;\n    grid-column-gap: $interiorMargin;\n    grid-template-columns: 20px max-content 1fr;\n    grid-row-gap: $interiorMargin;\n    margin-top: $interiorMarginLg;\n    width: max-content;\n\n    .c-form__row {\n      display: contents;\n    }\n  }\n}\n\n.c-form-row {\n  align-items: start;\n\n  &__label,\n  &__state-indicator {\n    flex: 0 0 auto;\n    padding: 2px 0;\n  }\n\n  &__label {\n    width: 30%;\n    min-width: 100px;\n    order: 1;\n  }\n\n  &__state-indicator {\n    order: 2;\n    width: 30px;\n    text-align: center;\n\n    &.invalid,\n    &.invalid.req {\n      @include validationState($glyph-icon-x, $colorFormInvalid);\n    }\n\n    &.valid,\n    &.valid.req {\n      @include validationState($glyph-icon-check, $colorFormValid);\n    }\n\n    &.req {\n      @include validationState($glyph-icon-asterisk, $colorFormRequired);\n    }\n  }\n\n  &__controls {\n    flex: 1 1 auto;\n    order: 3;\n  }\n}\n\n.c-form-control {\n  &--locator > .c-tree {\n    height: 100%;\n  }\n\n  &--clock-display-format-fields {\n    display: flex;\n\n    > * {\n      flex: 0 0 auto;\n      + * {\n        margin-left: $interiorMargin;\n      }\n    }\n  }\n\n  &--datetime {\n    $size: max-content;\n    display: grid;\n    grid-template-columns: repeat(5, $size);\n    grid-template-rows: $size;\n    grid-row-gap: 3px;\n    grid-column-gap: $interiorMargin;\n    align-items: stretch;\n\n    .hint {\n      align-self: center;\n      opacity: 0.7;\n    }\n  }\n}\n\n.l-input-lg {\n  input[type='text'],\n  input[type='search'],\n  input[type='number'],\n  textarea[type='text'] {\n    width: 100%;\n  }\n}\n\n.l-input-sm {\n  input[type='text'],\n  input[type='search'],\n  input[type='number'] {\n    width: 50px;\n  }\n}\n\n/***************************************************** LEGACY */\n.section-header {\n  border-radius: $basicCr;\n  color: lighten($colorBodyFg, 20%);\n  font-size: inherit;\n  margin: $interiorMargin 0;\n  padding: $formTBPad $formLRPad;\n  text-transform: uppercase;\n  .view-control {\n    display: inline-block;\n    margin-right: $interiorMargin;\n    width: 1em;\n    height: 1em;\n  }\n}\n\n.form {\n  color: $colorFormText;\n  height: 100%;\n  width: 100%;\n\n  .l-form-section {\n    margin-bottom: $interiorMarginLg * 2;\n    position: relative;\n    &.grows {\n      .l-section-body,\n      .form-row {\n        flex: 1 1 auto;\n        .wrapper {\n          height: 100%;\n        }\n      }\n    }\n  }\n\n  .form-row {\n    $m: $interiorMargin;\n    box-sizing: border-box;\n    border-top: 1px solid $colorFormLines;\n    padding: $formTBPad 0;\n    position: relative;\n\n    &.first {\n      border-top: none;\n    }\n\n    > .label,\n    > .controls {\n      box-sizing: border-box;\n      font-size: 0.8rem;\n    }\n\n    > .label {\n      // Only style this way for immediate children of .form-row; prevents problems when .label is used in .controls section of a form\n      min-width: 120px;\n      order: 1;\n      position: relative;\n      width: $formLabelW;\n    }\n\n    .value {\n      color: $colorInputFg;\n    }\n\n    .controls {\n      order: 9;\n      position: relative;\n      flex: 1 1 auto;\n\n      .l-composite-control {\n        &.l-checkbox {\n          display: inline-block;\n          line-height: $formRowCtrlsH;\n          margin-right: 5px;\n        }\n      }\n\n      .l-input-lg {\n        // LEGACY FORM SUPPORT\n        input[type='text'],\n        input[type='search'],\n        input[type='number'] {\n          width: 100%;\n        }\n      }\n\n      select {\n        margin-right: $interiorMargin;\n      }\n    }\n\n    .hint,\n    .field-hints {\n      color: $colorFieldHint;\n    }\n  }\n}\n\n.selector-list {\n  // Displays tree view in dialogs\n  @include nice-input();\n  padding: $interiorMargin;\n  position: relative;\n  min-height: 0; // Chrome 73 overflow bug fix\n  height: 100%;\n  > .wrapper {\n    $p: $interiorMargin;\n    box-sizing: border-box;\n    overflow: auto;\n  }\n}\n\n.l-controls-first .form .form-row,\n.form .form-row.l-controls-first {\n  > .label,\n  > .controls {\n    line-height: inherit;\n    min-height: inherit;\n  }\n  > .label {\n    flex: 1 1 auto;\n    min-width: 0;\n    width: auto;\n    order: 2;\n  }\n  > .control,\n  > .controls {\n    flex: 0 0 auto;\n    margin-right: $interiorMargin;\n    order: 1;\n  }\n}\n\n.l-controls-under.l-flex-row {\n  // Change to use column layout\n  flex-direction: column;\n  .flex-elem {\n    margin-bottom: $interiorMarginLg;\n  }\n}\n\n.l-composite-control {\n  vertical-align: middle;\n  &:not(.l-inline) {\n    margin-bottom: $interiorMargin;\n  }\n  &.l-inline {\n    display: inline-block;\n  }\n  &.l-checkbox {\n    .composite-control-label {\n      line-height: 18px;\n    }\n  }\n}\n\n/********* COMPACT FORM */\n// ul > li > label, control\n// Make a new UL for each form section\n// Allow control-first, controls-below\n// 3/8/19: Used by Summary Widgets edit UI\n\n.l-compact-form .tree ul li,\n.l-compact-form ul li {\n  padding: 2px 0;\n}\n\n.l-compact-form {\n  $h: $btnStdH;\n  $labelW: 40%;\n  $minW: $labelW;\n  ul {\n    li {\n      display: flex;\n      align-items: stretch;\n      padding: $interiorMargin 0;\n\n      label,\n      .control {\n        display: flex;\n      }\n      label {\n        line-height: $h;\n        width: $labelW;\n      }\n      .controls {\n        display: flex;\n        flex-wrap: wrap;\n        align-items: flex-start;\n        flex-grow: 1;\n        margin-left: $interiorMargin;\n        min-height: $h;\n        line-height: $h;\n        input[type='text'],\n        input[type='search'],\n        input[type='number'],\n        button,\n        select {\n          min-height: $h;\n        }\n\n        > * + * {\n          margin-left: $interiorMarginSm;\n        }\n      }\n\n      &.connects-to-previous {\n        padding-top: 0;\n      }\n\n      &.section-header {\n        margin-top: $interiorMarginLg;\n        border-top: 1px solid $colorFormLines;\n      }\n\n      &.controls-first {\n        .control {\n          flex-grow: 0;\n          margin-right: $interiorMargin;\n          min-width: 0;\n          order: 1;\n          width: auto;\n        }\n        label {\n          flex-grow: 1;\n          order: 2;\n          width: auto;\n        }\n      }\n      &.controls-under {\n        display: block;\n        .control,\n        label {\n          display: block;\n          width: auto;\n        }\n\n        ul li {\n          border-top: none !important;\n          padding: 0;\n        }\n      }\n    }\n  }\n}\n\n/******** VALIDATION */\n.form-error {\n  // Block element that visually flags an error and contains a message\n  background-color: $colorFormFieldErrorBg;\n  color: $colorFormFieldErrorFg;\n  border-radius: $basicCr;\n  display: block;\n  padding: 1px 6px;\n  &:before {\n    content: $glyph-icon-alert-triangle;\n    display: inline-block;\n    font-family: symbolsfont;\n    margin-right: $interiorMarginSm;\n  }\n}\n\nbody.desktop .form-row > .label {\n  &:after {\n    position: absolute;\n    right: $interiorMargin;\n    height: 100%;\n    line-height: 200%;\n  }\n}\n\n.req {\n  color: $colorFormRequired;\n}\n"
  },
  {
    "path": "src/styles/_global.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@use 'sass:math';\n\n/******************************************************** RESETS */\n*,\n:before,\n:after {\n  box-sizing: border-box;\n}\n\ndiv {\n  position: relative;\n}\n\n/******************************************************** UTILITIES */\n.u-contents {\n  display: contents;\n}\n\n.u-menu-to {\n  &--left {\n    .c-menu {\n      left: auto !important;\n      right: 0;\n    }\n  }\n\n  &--center {\n    .c-menu {\n      left: 50% !important;\n      transform: translateX(-50%);\n    }\n  }\n}\n\n.u-space {\n  // Provides a separator space between elements\n  &--right {\n    + [class*='__'] {\n      margin-left: $interiorMarginLg !important;\n    }\n  }\n}\n\n.u-flex-spreader {\n  // Pushes against elements in a flex layout to spread them out\n  flex: 1 1 auto;\n}\n\n.visually-hidden {\n  // Provides a way to add accessible text to elements\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n\n/******************************************************** BROWSER ELEMENTS */\nbody.desktop {\n  ::-webkit-scrollbar {\n    box-sizing: border-box;\n    box-shadow: inset $scrollbarTrackShdw;\n    background-color: $scrollbarTrackColorBg;\n    height: $scrollbarTrackSize;\n    width: $scrollbarTrackSize;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    box-sizing: border-box;\n    background: $scrollbarThumbColor;\n    &:hover {\n      background: $scrollbarThumbColorHov;\n    }\n  }\n\n  ::-webkit-scrollbar-corner {\n    background: transparent;\n  }\n\n  .c-menu ::-webkit-scrollbar-thumb {\n    background: $scrollbarThumbColorMenu;\n    &:hover {\n      background: $scrollbarThumbColorMenuHov;\n    }\n  }\n\n  div,\n  ul,\n  span {\n    // Firefox\n    scrollbar-color: $scrollbarThumbColor $scrollbarTrackColorBg;\n    scrollbar-width: thin;\n  }\n}\n\n/******************************************************** FONTS */\n@mixin fontAndSize() {\n  @each $size in $listFontSizes {\n    &[data-font-size='#{$size}'] {\n      font-size: #{$size}px;\n\n      // Set row heights in telemetry tables\n      tr {\n        min-height: #{$size + ($tabularTdPadTB * 2)};\n      }\n    }\n  }\n\n  &[data-font*='bold'] {\n    font-weight: bold;\n  }\n\n  &[data-font*='narrow'] {\n    font-family: 'Arial Narrow', sans-serif;\n  }\n\n  &[data-font*='monospace'] {\n    font-family: 'Andale Mono', sans-serif;\n  }\n}\n\n.u-style-receiver {\n  @include fontAndSize();\n}\n\n/******************************************************** HTML ENTITIES */\na {\n  color: $colorA;\n  cursor: pointer;\n  text-decoration: none;\n\n  &:focus {\n    outline: none !important;\n  }\n}\n\nbody,\nhtml {\n  height: $bodySize;\n  width: 100%;\n}\n\n#openmct-app {\n  @include abs();\n}\n\nbody {\n  -webkit-font-smoothing: subpixel-antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  @include bodyFont($fontBaseSize);\n  // background-color: $colorBodyBg;\n  background: $bodyBg;\n  background-size: $bodyBgSize;\n  color: $colorBodyFg;\n}\n\nem {\n  font-style: normal;\n}\n\np {\n  margin-bottom: $interiorMarginLg;\n}\n\nol,\nul {\n  list-style: none;\n  margin: 0;\n  padding-left: 0;\n}\n\ntable {\n  border-spacing: 0;\n  border-collapse: collapse;\n}\n\nli {\n  list-style-type: none;\n  margin: 0;\n  padding: 0;\n}\n\n/******************************************************** HAS */\n// Local Controls: Controls placed in proximity to or overlaid on components and views\nbody.desktop .has-local-controls {\n  // Provides hover ability to show local controls\n  [class*='local-controls--hidden'] {\n    transition: opacity 500ms ease-in-out;\n    opacity: 0;\n    pointer-events: none;\n  }\n\n  // Look down up to two levels and display hidden LC's on hover\n  &:hover {\n    > [class*='local-controls--hidden'],\n    > * > [class*='local-controls--hidden'],\n    > * > * > [class*='local-controls--hidden'] {\n      transition: opacity 50ms ease-in-out;\n      opacity: 1;\n      pointer-events: inherit;\n    }\n  }\n}\n\n/******************************************************** ICON BACKGROUNDS */\n// Used with elements that utilize an SVG background element where specific coloring is needed\n.u-icon-bg-color {\n  // Messages and notifications\n  &-info {\n    @include glyphBg($bg-icon-info);\n    filter: $colorStatusInfoFilter;\n  }\n\n  &-alert {\n    @include glyphBg($bg-icon-alert-rect);\n    filter: $colorStatusAlertFilter;\n  }\n\n  &-error {\n    @include glyphBg($bg-icon-alert-triangle);\n    filter: $colorStatusErrorFilter;\n  }\n}\n\n/******************************************************** SELECTION AND EDITING */\n// Provides supporting styles for Display Layouts and augmented legacy Fixed Position view\n.c-grid,\n.c-grid__x,\n.c-grid__y {\n  @include abs();\n}\n\n.c-grid .c-grid {\n  pointer-events: none;\n\n  &__x {\n    @include bgTicks($editUIGridColorFg, 'x');\n  }\n  &__y {\n    @include bgTicks($editUIGridColorFg, 'y');\n  }\n}\n\n/*************************** SELECTION */\n.u-inspectable {\n  &:hover {\n    box-shadow: $browseSelectableShdwHov;\n  }\n}\n\n/**************************** EDITING */\n.is-editing {\n  .is-moveable {\n    cursor: move;\n  }\n\n  .u-links {\n    // Applied in markup to objects that provide links. Disable while editing.\n    pointer-events: none;\n  }\n}\n\n::placeholder {\n  opacity: 0.7;\n  font-style: italic;\n}\n\n/******************************************************** STATES */\n@mixin spinner($b: 5, $c: $colorKey) {\n  animation-name: rotation-centered;\n  animation-duration: 0.5s;\n  animation-iteration-count: infinite;\n  animation-timing-function: linear;\n  border-radius: 100%;\n  box-sizing: border-box;\n  border-color: rgba($c, 0.25);\n  border-top-color: rgba($c, 1);\n  border-style: solid;\n  border-width: $b;\n  display: block;\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform-origin: center;\n  transform: translate(-50%, -50%);\n}\n\n@keyframes geartooth {\n  to { transform: translate(-50%, -50%) rotate(50deg);  }\n}\n\n@mixin gearSpinner($anim: \"geartooth\", $animDur: 0.25s, $steps: 6, $color: \"#ffffff\") {\n  @include absCenter();\n  @include bgDashedCircle($color: $color);\n  animation: #{$anim} $animDur steps($steps) infinite;\n  background-size: contain;\n  background-position: center;\n  background-repeat: no-repeat;\n  width: 100%;\n  height: 100%;\n}\n\n.gear-spinner {\n  @include gearSpinner($color: $colorKey);\n}\n\n.wait-spinner {\n  @include spinner($waitSpinnerBorderW, $colorKey);\n  pointer-events: none;\n  z-index: 2;\n  &.inline {\n    display: inline-block !important;\n    margin-right: $interiorMargin;\n    position: relative !important;\n    vertical-align: middle;\n  }\n}\n\n.loading {\n  // Can be applied to any block element with height and width\n  pointer-events: none;\n  &:before,\n  &:after {\n    content: '';\n  }\n  &:before {\n    @include spinner($waitSpinnerBorderW, $colorLoadingFg);\n    height: $waitSpinnerD;\n    width: $waitSpinnerD;\n    z-index: 10;\n  }\n  &:after {\n    @include abs();\n    background: $colorLoadingBg;\n    display: block;\n    z-index: 9;\n  }\n  &.c-tree__item {\n    $d: $waitSpinnerTreeD;\n    $spinnerL: 19 + math.div($d, 2);\n\n    display: flex;\n    align-items: center;\n    margin-left: $treeNavArrowD + $interiorMargin;\n    min-height: 5px + $d;\n\n    .c-tree__item__label {\n      font-style: italic;\n      margin-left: $interiorMargin;\n      opacity: 0.6;\n    }\n    &:before {\n      left: auto;\n      top: auto;\n      transform: translate(0);\n      height: $d;\n      width: $d;\n      border-width: 3px;\n      //left: $spinnerL;\n      position: relative;\n    }\n    &:after {\n      display: none;\n    }\n  }\n\n  &.c-loading--overlay {\n    @include abs();\n  }\n}\n\n[aria-disabled='true'],\n*[disabled],\n.disabled {\n  opacity: $controlDisabledOpacity;\n  cursor: not-allowed !important;\n  pointer-events: none !important;\n}\n\n/******************************************************** RESPONSIVE CONTAINERS */\n@mixin responsiveContainerWidths($dimension) {\n  // 3/21/22: `--width-less-than*` classes set in ObjectView.vue\n  .--show-if-less-than-#{$dimension} {\n    // Hide anything that displays within a given width by default.\n    // `display` property must be set within a more specific class\n    // for the particular item to be displayed.\n    display: none !important;\n  }\n\n  .--width-less-than-#{$dimension} {\n    .--hide-if-less-than-#{$dimension} {\n      display: none;\n    }\n  }\n}\n\n//.--hide-by-default { display: none !important; }\n@include responsiveContainerWidths('220');\n@include responsiveContainerWidths('600');\n\n.u-fade-truncate,\n.u-fade-truncate--lg {\n  &:after {\n    display: block;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    content: '';\n    right: 0;\n    width: $fadeTruncateW * 1.5;\n    z-index: 2;\n  }\n\n  &.--no-sep {\n    border-right: none;\n  }\n}\n\n.u-fade-truncate--lg {\n  flex-basis: 100% !important;\n}\n"
  },
  {
    "path": "src/styles/_glyphs.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@font-face {\n    // Use https://icomoon.io/app with `Icomoon.Open MCT Symbols 2018.json` to generate font files\n    font-family: 'symbolsfont';\n    src: url('./fonts/Open-MCT-Symbols-16px.woff') format('woff'),\n    url('./fonts/Open-MCT-Symbols-16px.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    // Use https://icomoon.io/app with icomoon-project-Open-MCT-Symbols-12px.json to generate font files\n    font-family: 'symbolsfont-12px';\n    src: url('./fonts/Open-MCT-Symbols-12px.woff') format('woff'),\n    url('./fonts/Open-MCT-Symbols-12px.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n/************************** 16 PX CLASSES */\n.icon-alert-rect {\n    @include glyphBefore($glyph-icon-alert-rect);\n}\n\n.icon-alert-triangle {\n    @include glyphBefore($glyph-icon-alert-triangle);\n}\n\n.icon-arrow-up {\n    @include glyphBefore($glyph-icon-arrow-up);\n}\n\n.icon-arrow-double-up {\n    @include glyphBefore($glyph-icon-arrow-double-up);\n}\n\n.icon-arrow-tall-up {\n    @include glyphBefore($glyph-icon-arrow-tall-up);\n}\n\n.icon-arrow-right {\n    @include glyphBefore($glyph-icon-arrow-right);\n}\n\n.icon-arrow-right-equilateral {\n    @include glyphBefore($glyph-icon-arrow-right-equilateral);\n}\n\n.icon-arrow-down {\n    @include glyphBefore($glyph-icon-arrow-down);\n}\n\n.icon-arrow-double-down {\n    @include glyphBefore($glyph-icon-arrow-double-down);\n}\n\n.icon-arrow-tall-down {\n    @include glyphBefore($glyph-icon-arrow-tall-down);\n}\n\n.icon-arrow-left {\n    @include glyphBefore($glyph-icon-arrow-left);\n}\n\n.icon-asterisk {\n    @include glyphBefore($glyph-icon-asterisk);\n}\n\n.icon-bell {\n    @include glyphBefore($glyph-icon-bell);\n}\n\n.icon-box-round-corners {\n    @include glyphBefore($glyph-icon-box-round-corners);\n}\n\n.icon-box-with-arrow {\n    @include glyphBefore($glyph-icon-box-with-arrow);\n}\n\n.icon-check {\n    @include glyphBefore($glyph-icon-check);\n}\n\n.icon-connectivity {\n    @include glyphBefore($glyph-icon-connectivity);\n}\n\n.icon-database-in-brackets {\n    @include glyphBefore($glyph-icon-database-in-brackets);\n}\n\n.icon-eye-open {\n    @include glyphBefore($glyph-icon-eye-open);\n}\n\n.icon-gear {\n    @include glyphBefore($glyph-icon-gear);\n}\n\n.icon-gear-after {\n    @include glyphAfter($glyph-icon-gear);\n}\n\n.icon-hourglass {\n    @include glyphBefore($glyph-icon-hourglass);\n}\n\n.icon-info {\n    @include glyphBefore($glyph-icon-info);\n}\n\n.icon-link {\n    @include glyphBefore($glyph-icon-link);\n}\n\n.icon-lock {\n    @include glyphBefore($glyph-icon-lock);\n}\n\n.icon-minus {\n    @include glyphBefore($glyph-icon-minus);\n}\n\n.icon-people {\n    @include glyphBefore($glyph-icon-people);\n}\n\n.icon-person {\n    @include glyphBefore($glyph-icon-person);\n}\n\n.icon-plus {\n    @include glyphBefore($glyph-icon-plus);\n}\n\n.icon-plus-in-rect {\n    @include glyphBefore($glyph-icon-plus-in-rect);\n}\n\n.icon-trash {\n    @include glyphBefore($glyph-icon-trash);\n}\n\n.icon-x {\n    @include glyphBefore($glyph-icon-x);\n}\n\n.icon-brackets {\n    @include glyphBefore($glyph-icon-brackets);\n}\n\n.icon-crosshair {\n    @include glyphBefore($glyph-icon-crosshair);\n}\n\n.icon-grippy {\n    @include glyphBefore($glyph-icon-grippy);\n}\n\n.icon-grid {\n    @include glyphBefore($glyph-icon-grid);\n}\n\n.icon-grippy-ew {\n    @include glyphBefore($glyph-icon-grippy-ew);\n}\n\n.icon-columns {\n    @include glyphBefore($glyph-icon-columns);\n}\n\n.icon-rows {\n    @include glyphBefore($glyph-icon-rows);\n}\n\n.icon-filter {\n    @include glyphBefore($glyph-icon-filter);\n}\n\n.icon-filter-outline {\n    @include glyphBefore($glyph-icon-filter-outline);\n}\n\n.icon-suitcase {\n    @include glyphBefore($glyph-icon-suitcase);\n}\n\n.icon-cursor-lock {\n    @include glyphBefore($glyph-icon-cursor-lock);\n}\n\n.icon-flag {\n    @include glyphBefore($glyph-icon-flag);\n}\n\n.icon-eye-disabled {\n    @include glyphBefore($glyph-icon-eye-disabled);\n}\n\n.icon-notebook-page {\n    @include glyphBefore($glyph-icon-notebook-page);\n}\n\n.icon-unlocked {\n    @include glyphBefore($glyph-icon-unlocked);\n}\n\n.icon-circle {\n    @include glyphBefore($glyph-icon-circle);\n}\n\n.icon-draft {\n    @include glyphBefore($glyph-icon-draft);\n}\n\n.icon-question-mark {\n    @include glyphBefore($glyph-icon-question-mark);\n}\n\n.icon-circle-slash {\n    @include glyphBefore($glyph-icon-circle-slash);\n}\n\n.icon-status-poll-check {\n    @include glyphBefore($glyph-icon-status-poll-check);\n}\n\n.icon-status-poll-caution {\n    @include glyphBefore($glyph-icon-status-poll-caution);\n}\n\n.icon-status-poll-circle-slash {\n    @include glyphBefore($glyph-icon-status-poll-circle-slash);\n}\n\n.icon-status-poll-question-mark {\n    @include glyphBefore($glyph-icon-status-poll-question-mark);\n}\n\n.icon-status-poll-edit {\n    @include glyphBefore($glyph-icon-status-poll-edit);\n}\n\n.icon-stale {\n    @include glyphBefore($glyph-icon-stale);\n}\n\n.icon-arrows-right-left {\n    @include glyphBefore($glyph-icon-arrows-right-left);\n}\n\n.icon-arrows-up-down {\n    @include glyphBefore($glyph-icon-arrows-up-down);\n}\n\n.icon-bullet {\n    @include glyphBefore($glyph-icon-bullet);\n}\n\n.icon-calendar {\n    @include glyphBefore($glyph-icon-calendar);\n}\n\n.icon-chain-links {\n    @include glyphBefore($glyph-icon-chain-links);\n}\n\n.icon-download {\n    @include glyphBefore($glyph-icon-download);\n}\n\n.icon-duplicate {\n    @include glyphBefore($glyph-icon-duplicate);\n}\n\n.icon-folder-new {\n    @include glyphBefore($glyph-icon-folder-new);\n}\n\n.icon-fullscreen-collapse {\n    @include glyphBefore($glyph-icon-fullscreen-collapse);\n}\n\n.icon-fullscreen-expand {\n    @include glyphBefore($glyph-icon-fullscreen-expand);\n}\n\n.icon-layers {\n    @include glyphBefore($glyph-icon-layers);\n}\n\n.icon-line-horz {\n    @include glyphBefore($glyph-icon-line-horz);\n}\n\n.icon-magnify {\n    @include glyphBefore($glyph-icon-magnify);\n}\n\n.icon-magnify-in {\n    @include glyphBefore($glyph-icon-magnify-in);\n}\n\n.icon-magnify-out {\n    @include glyphBefore($glyph-icon-magnify-out);\n}\n\n.icon-menu-hamburger {\n    @include glyphBefore($glyph-icon-menu-hamburger);\n}\n\n.icon-move {\n    @include glyphBefore($glyph-icon-move);\n}\n\n.icon-new-window {\n    @include glyphBefore($glyph-icon-new-window);\n}\n\n.icon-paint-bucket {\n    @include glyphBefore($glyph-icon-paint-bucket);\n}\n\n.icon-pencil {\n    @include glyphBefore($glyph-icon-pencil);\n}\n\n.icon-pencil-in-brackets {\n    @include glyphBefore($glyph-icon-pencil-in-brackets);\n}\n\n.icon-play {\n    @include glyphBefore($glyph-icon-play);\n}\n\n.icon-pause {\n    @include glyphBefore($glyph-icon-pause);\n}\n\n.icon-plot-resource {\n    @include glyphBefore($glyph-icon-plot-resource);\n}\n\n.icon-pointer-left {\n    @include glyphBefore($glyph-icon-pointer-left);\n}\n\n.icon-pointer-right {\n    @include glyphBefore($glyph-icon-pointer-right);\n}\n\n.icon-refresh {\n    @include glyphBefore($glyph-icon-refresh);\n}\n\n.icon-save {\n    @include glyphBefore($glyph-icon-save);\n}\n\n.icon-save-as {\n    @include glyphBefore($glyph-icon-save-as);\n}\n\n.icon-sine {\n    @include glyphBefore($glyph-icon-sine);\n}\n\n.icon-font {\n    @include glyphBefore($glyph-icon-font);\n}\n\n.icon-thumbs-strip {\n    @include glyphBefore($glyph-icon-thumbs-strip);\n}\n\n.icon-two-parts-both {\n    @include glyphBefore($glyph-icon-two-parts-both);\n}\n\n.icon-two-parts-one-only {\n    @include glyphBefore($glyph-icon-two-parts-one-only);\n}\n\n.icon-resync {\n    @include glyphBefore($glyph-icon-resync);\n}\n\n.icon-reset {\n    @include glyphBefore($glyph-icon-reset);\n}\n\n.icon-x-in-circle {\n    @include glyphBefore($glyph-icon-x-in-circle);\n}\n\n.icon-brightness {\n    @include glyphBefore($glyph-icon-brightness);\n}\n\n.icon-contrast {\n    @include glyphBefore($glyph-icon-contrast);\n}\n\n.icon-expand {\n    @include glyphBefore($glyph-icon-expand);\n}\n\n.icon-list-view {\n    @include glyphBefore($glyph-icon-list-view);\n}\n\n.icon-grid-snap-to {\n    @include glyphBefore($glyph-icon-grid-snap-to);\n}\n\n.icon-grid-snap-no {\n    @include glyphBefore($glyph-icon-grid-snap-no);\n}\n\n.icon-frame-show {\n    @include glyphBefore($glyph-icon-frame-show);\n}\n\n.icon-frame-hide {\n    @include glyphBefore($glyph-icon-frame-hide);\n}\n\n.icon-import {\n    @include glyphBefore($glyph-icon-import);\n}\n\n.icon-export {\n    @include glyphBefore($glyph-icon-export);\n}\n\n.icon-font-size {\n    @include glyphBefore($glyph-icon-font-size);\n}\n\n.icon-clear-data {\n    @include glyphBefore($glyph-icon-clear-data);\n}\n\n.icon-history {\n    @include glyphBefore($glyph-icon-history);\n}\n\n.icon-arrow-nav-to-parent {\n    @include glyphBefore($glyph-icon-arrow-nav-to-parent);\n}\n\n.icon-crosshair-in-circle {\n    @include glyphBefore($glyph-icon-crosshair-in-circle);\n}\n\n.icon-target {\n    @include glyphBefore($glyph-icon-target);\n}\n\n.icon-items-collapse {\n    @include glyphBefore($glyph-icon-items-collapse);\n}\n\n.icon-items-expand {\n    @include glyphBefore($glyph-icon-items-expand);\n}\n\n.icon-3-dots {\n    @include glyphBefore($glyph-icon-3-dots);\n}\n\n.icon-grid-on {\n    @include glyphBefore($glyph-icon-grid-on);\n}\n\n.icon-grid-off {\n    @include glyphBefore($glyph-icon-grid-off);\n}\n\n.icon-camera {\n    @include glyphBefore($glyph-icon-camera);\n}\n\n.icon-folders-collapse {\n    @include glyphBefore($glyph-icon-folders-collapse);\n}\n\n.icon-multiline {\n    @include glyphBefore($glyph-icon-multiline);\n}\n\n.icon-singleline {\n    @include glyphBefore($glyph-icon-singleline);\n}\n\n\n.icon-activity {\n    @include glyphBefore($glyph-icon-activity);\n}\n\n.icon-activity-mode {\n    @include glyphBefore($glyph-icon-activity-mode);\n}\n\n.icon-autoflow-tabular {\n    @include glyphBefore($glyph-icon-autoflow-tabular);\n}\n\n.icon-clock {\n    @include glyphBefore($glyph-icon-clock);\n}\n\n.icon-database {\n    @include glyphBefore($glyph-icon-database);\n}\n\n.icon-database-query {\n    @include glyphBefore($glyph-icon-database-query);\n}\n\n.icon-dataset {\n    @include glyphBefore($glyph-icon-dataset);\n}\n\n.icon-datatable {\n    @include glyphBefore($glyph-icon-datatable);\n}\n\n.icon-dictionary {\n    @include glyphBefore($glyph-icon-dictionary);\n}\n\n.icon-folder {\n    @include glyphBefore($glyph-icon-folder);\n}\n\n.icon-image {\n    @include glyphBefore($glyph-icon-image);\n}\n\n.icon-layout {\n    @include glyphBefore($glyph-icon-layout);\n}\n\n.icon-object {\n    @include glyphBefore($glyph-icon-object);\n}\n\n.icon-object-unknown {\n    @include glyphBefore($glyph-icon-object-unknown);\n}\n\n.icon-packet {\n    @include glyphBefore($glyph-icon-packet);\n}\n\n.icon-page {\n    @include glyphBefore($glyph-icon-page);\n}\n\n.icon-plot-overlay {\n    @include glyphBefore($glyph-icon-plot-overlay);\n}\n\n.icon-plot-stacked {\n    @include glyphBefore($glyph-icon-plot-stacked);\n}\n\n.icon-session {\n    @include glyphBefore($glyph-icon-session);\n}\n\n.icon-tabular {\n    @include glyphBefore($glyph-icon-tabular);\n}\n\n.icon-tabular-lad {\n    @include glyphBefore($glyph-icon-tabular-lad);\n}\n\n.icon-tabular-lad-set {\n    @include glyphBefore($glyph-icon-tabular-lad-set);\n}\n\n.icon-tabular-realtime {\n    @include glyphBefore($glyph-icon-tabular-realtime);\n}\n\n.icon-tabular-scrolling {\n    @include glyphBefore($glyph-icon-tabular-scrolling);\n}\n\n.icon-telemetry {\n    @include glyphBefore($glyph-icon-telemetry);\n}\n\n.icon-timeline {\n    @include glyphBefore($glyph-icon-timeline);\n}\n\n.icon-timer {\n    @include glyphBefore($glyph-icon-timer);\n}\n\n.icon-topic {\n    @include glyphBefore($glyph-icon-topic);\n}\n\n.icon-box-with-dashed-lines {\n    @include glyphBefore($glyph-icon-box-with-dashed-lines);\n}\n\n.icon-summary-widget {\n    @include glyphBefore($glyph-icon-summary-widget);\n}\n\n.icon-notebook {\n    @include glyphBefore($glyph-icon-notebook);\n}\n\n.icon-tabs-view {\n    @include glyphBefore($glyph-icon-tabs-view);\n}\n\n.icon-flexible-layout {\n    @include glyphBefore($glyph-icon-flexible-layout);\n}\n\n.icon-generator-telemetry {\n    @include glyphBefore($glyph-icon-generator-telemetry);\n}\n\n.icon-generator-events {\n    @include glyphBefore($glyph-icon-generator-events);\n}\n\n.icon-gauge {\n    @include glyphBefore($glyph-icon-gauge);\n}\n\n.icon-spectra {\n    @include glyphBefore($glyph-icon-spectra);\n}\n\n.icon-spectra-telemetry {\n    @include glyphBefore($glyph-icon-spectra-telemetry);\n}\n\n.icon-command {\n    @include glyphBefore($glyph-icon-command);\n}\n\n.icon-conditional {\n    @include glyphBefore($glyph-icon-conditional);\n}\n\n.icon-condition-widget {\n    @include glyphBefore($glyph-icon-condition-widget);\n}\n\n.icon-alphanumeric {\n    @include glyphBefore($glyph-icon-alphanumeric);\n}\n\n.icon-image-telemetry {\n    @include glyphBefore($glyph-icon-image-telemetry);\n}\n\n.icon-telemetry-aggregate {\n    @include glyphBefore($glyph-icon-telemetry-aggregate);\n}\n\n.icon-bar-chart {\n    @include glyphBefore($glyph-icon-bar-chart);\n}\n\n.icon-map {\n    @include glyphBefore($glyph-icon-map);\n}\n\n.icon-plan {\n    @include glyphBefore($glyph-icon-plan);\n}\n\n.icon-timelist {\n    @include glyphBefore($glyph-icon-timelist);\n}\n\n.icon-notebook-shift-log {\n    @include glyphBefore($glyph-icon-notebook-shift-log);\n}\n\n\n.icon-derived-telemetry {\n      @include glyphBefore($glyph-icon-derived-telemetry);\n}\n\n/************************** 12 PX CLASSES */\n// TODO: sync with 16px redo as of 10/25/18\n.icon-filter-12px {\n    @include glyphBefore($glyph-icon-filter, 'symbolsfont-12px');\n}\n\n.icon-filter-outline-12px {\n    @include glyphBefore($glyph-icon-filter-outline, 'symbolsfont-12px');\n}\n\n.icon-crosshair-12px {\n    @include glyphBefore($glyph-icon-crosshair, 'symbolsfont-12px');\n}\n\n.icon-folder-12px {\n    @include glyphBefore($glyph-icon-folder, 'symbolsfont-12px');\n}\n\n.icon-list-view-12px {\n    @include glyphBefore($glyph-icon-list-view, 'symbolsfont-12px');\n}\n\n.icon-grippy-12px {\n    @include glyphBefore($glyph-icon-grippy, 'symbolsfont-12px');\n}\n\n/************************** GLYPH BG CLASSES */\n.bg-icon-alert-rect {\n    @include glyphBg($bg-icon-alert-rect);\n}\n\n.bg-icon-alert-triangle {\n    @include glyphBg($bg-icon-alert-triangle);\n}\n\n.bg-icon-bell {\n    @include glyphBg($bg-icon-bell);\n}\n\n.bg-icon-info {\n    @include glyphBg($bg-icon-info);\n}\n\n.bg-icon-plus {\n    @include glyphBg($bg-icon-plus);\n}\n\n.bg-icon-grippy-ew {\n    @include glyphBg($bg-icon-grippy-ew);\n}\n\n.bg-icon-chain-links {\n    @include glyphBg($bg-icon-chain-links);\n}\n\n.bg-icon-clock {\n    @include glyphBg($bg-icon-clock);\n}\n\n.bg-icon-database {\n    @include glyphBg($bg-icon-database);\n}\n\n.bg-icon-database-query {\n    @include glyphBg($bg-icon-database-query);\n}\n\n.bg-icon-dataset {\n    @include glyphBg($bg-icon-dataset);\n}\n\n.bg-icon-datatable {\n    @include glyphBg($bg-icon-datatable);\n}\n\n.bg-icon-dictionary {\n    @include glyphBg($bg-icon-dictionary);\n}\n\n.bg-icon-folder {\n    @include glyphBg($bg-icon-folder);\n}\n\n.bg-icon-image {\n    @include glyphBg($bg-icon-image);\n}\n\n.bg-icon-layout {\n    @include glyphBg($bg-icon-layout);\n}\n\n.bg-icon-object {\n    @include glyphBg($bg-icon-object);\n}\n\n.bg-icon-object-unknown {\n    @include glyphBg($bg-icon-object-unknown);\n}\n\n.bg-icon-packet {\n    @include glyphBg($bg-icon-packet);\n}\n\n.bg-icon-page {\n    @include glyphBg($bg-icon-page);\n}\n\n.bg-icon-plot-overlay {\n    @include glyphBg($bg-icon-plot-overlay);\n}\n\n.bg-icon-plot-stacked {\n    @include glyphBg($bg-icon-plot-stacked);\n}\n\n.bg-icon-session {\n    @include glyphBg($bg-icon-session);\n}\n\n.bg-icon-tabular {\n    @include glyphBg($bg-icon-tabular);\n}\n\n.bg-icon-tabular-lad {\n    @include glyphBg($bg-icon-tabular-lad);\n}\n\n.bg-icon-tabular-lad-set {\n    @include glyphBg($bg-icon-tabular-lad-set);\n}\n\n.bg-icon-tabular-scrolling {\n    @include glyphBg($bg-icon-tabular-scrolling);\n}\n\n.bg-icon-telemetry {\n    @include glyphBg($bg-icon-telemetry);\n}\n\n.bg-icon-timeline {\n    @include glyphBg($bg-icon-timeline);\n}\n\n.bg-icon-timer {\n    @include glyphBg($bg-icon-timer);\n}\n\n.bg-icon-box-with-dashed-lines {\n    @include glyphBg($bg-icon-box-with-dashed-lines);\n}\n\n.bg-icon-summary-widget {\n    @include glyphBg($bg-icon-summary-widget);\n}\n\n.bg-icon-notebook {\n    @include glyphBg($bg-icon-notebook);\n}\n\n.bg-icon-tabs-view {\n    @include glyphBg($bg-icon-tabs-view);\n}\n\n.bg-icon-flexible-layout {\n    @include glyphBg($bg-icon-flexible-layout);\n}\n\n.bg-icon-generator-telemetry {\n    @include glyphBg($bg-icon-generator-telemetry);\n}\n\n.bg-icon-generator-events {\n    @include glyphBg($bg-icon-generator-events);\n}\n\n.bg-icon-gauge {\n    @include glyphBg($bg-icon-gauge);\n}\n\n.bg-icon-spectra {\n    @include glyphBg($bg-icon-spectra);\n}\n\n.bg-icon-spectra-telemetry {\n    @include glyphBg($bg-icon-spectra-telemetry);\n}\n\n.bg-icon-command {\n    @include glyphBg($bg-icon-command);\n}\n\n.bg-icon-conditional {\n    @include glyphBg($bg-icon-conditional);\n}\n\n.bg-icon-condition-widget {\n    @include glyphBg($bg-icon-condition-widget);\n}\n\n.bg-icon-bar-chart {\n    @include glyphBg($bg-icon-bar-chart);\n}\n\n.bg-icon-map {\n    @include glyphBg($bg-icon-map);\n}\n\n.bg-icon-plan {\n    @include glyphBg($bg-icon-plan);\n}\n\n.bg-icon-timelist {\n    @include glyphBg($bg-icon-timelist);\n}\n\n.bg-icon-plot-scatter {\n    @include glyphBg($bg-icon-plot-scatter);\n}\n\n.bg-icon-notebook-shift-log {\n    @include glyphBg($bg-icon-notebook-shift-log);\n}\n\n.bg-icon-telemetry-aggregate {\n    @include glyphBg($bg-icon-telemetry-aggregate);\n}\n\n.bg-icon-trash {\n    @include glyphBg($bg-icon-trash);\n}\n\n.bg-icon-eye-open {\n    @include glyphBg($bg-icon-eye-open);\n}\n\n.bg-icon-camera {\n    @include glyphBg($bg-icon-camera);\n}\n\n.bg-icon-derived-telemetry {\n    @include glyphBg($bg-icon-derived-telemetry);\n}\n\n/************************** COLOR-ABLE BG SVG GLYPHS */\n@mixin bgDashedCircle($r: 14, $sw: 5, $sda: \"6.2 6.2\", $color: \"#000000\") {\n  // Renders a 6 segmented dashed circle as a background-image using SVG\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg fill='none' width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='16' cy='16' r='#{$r}' stroke='%23#{str-slice(#{$color}, 2)}' stroke-width='#{$sw}' stroke-dasharray='#{$sda}'/%3e%3c/svg%3e\");\n}\n\n@mixin bgCheckMark($color: \"#000000\") {\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg width='21' height='20' viewBox='0 0 21 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M21 6.04102L7.04102 20H6.95898L0 13.041V6.95898L7 13.959L20.959 0H21V6.04102Z' fill='%23#{str-slice(#{$color}, 2)}'/%3e%3c/svg%3e \");\n}\n\n@mixin bgCircleSlash($color: \"#000000\") {\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill-rule='evenodd' clip-rule='evenodd' d='M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM6.7627 16.0654C7.72753 16.5815 8.82935 16.875 10 16.875C13.797 16.875 16.875 13.797 16.875 10C16.875 8.82935 16.5815 7.72753 16.0654 6.7627L6.7627 16.0654ZM10 3.125C6.20304 3.125 3.125 6.20304 3.125 10C3.125 11.1702 3.41794 12.2718 3.93359 13.2363L13.2363 3.93359C12.2718 3.41794 11.1702 3.125 10 3.125Z' fill='%23#{str-slice(#{$color}, 2)}'/%3e%3c/svg%3e\");\n}\n\n@mixin bgSkip($color: \"#000000\") {\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12 3C14.7614 3 17 5.23858 17 8V12H20L15 17L10 12H13V8C13 7.44772 12.5523 7 12 7H8C7.44772 7 7 7.44772 7 8V11C7 13.7616 4.76097 15.9998 2 16H0V12H2C2.55243 11.9998 3 11.5519 3 11V8C3 5.23858 5.23858 3 8 3H12Z' fill='%23#{str-slice(#{$color}, 2)}'/%3e%3c/svg%3e \");\n}\n\n"
  },
  {
    "path": "src/styles/_legacy-messages.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@use 'sass:math';\n\n/******************************************************************* MESSAGES */\n.w-message-contents {\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n\n  > * + * {\n    margin-bottom: $interiorMargin;\n  }\n\n  .message-body {\n    flex: 1 1 100%;\n  }\n}\n\n// Singleton in an overlay dialog\n.t-message-single .l-message,\n.t-message-single.l-message {\n  $iconW: $messageListIconD;\n  &:before {\n    font-size: $iconW;\n    width: $iconW + 2;\n  }\n  .title {\n    font-size: 1.2em;\n  }\n}\n\n// Singleton inline in a view\n.t-message-inline .l-message,\n.t-message-inline.l-message {\n  border-radius: $controlCr;\n  &.message-severity-info {\n    background-color: rgba($colorInfo, 0.3);\n  }\n  &.message-severity-alert {\n    background-color: rgba($colorWarningLo, 0.3);\n  }\n  &.message-severity-error {\n    background-color: rgba($colorWarningHi, 0.3);\n  }\n\n  .w-message-contents.l-message-body-only {\n    .message-body {\n      margin-top: $interiorMargin;\n    }\n  }\n}\n\n// In a list\n.c-overlay__messages {\n  //@include abs();\n  display: flex;\n  flex-direction: column;\n  overflow: auto;\n  padding-right: $interiorMargin;\n\n  > div,\n  > span {\n    margin-bottom: $interiorMargin;\n  }\n\n  .w-messages {\n    flex: 1 1 100%;\n    overflow-y: auto;\n    padding-right: $interiorMargin;\n  }\n  // Each message\n  .c-message {\n    @include discreteItem();\n    flex: 0 0 auto;\n    margin-bottom: $interiorMarginSm;\n\n    .hint,\n    .bottom-bar {\n      text-align: left;\n    }\n  }\n}\n\n@include phonePortrait {\n  .t-message-single .l-message,\n  .t-message-single.l-message {\n    flex-direction: column;\n    &:before {\n      margin-right: 0;\n      margin-bottom: $interiorMarginLg;\n      text-align: center;\n      width: 100%;\n    }\n\n    .bottom-bar {\n      text-align: center;\n      .s-button {\n        display: block;\n        width: 100%;\n      }\n    }\n  }\n}\n\nbody.desktop .t-message-list {\n  .w-message-contents {\n    padding-right: $interiorMargin;\n  }\n}\n\n// Alert elements in views\n@mixin sUnSynced {\n  $c: $colorPausedBg;\n  border: 1px solid $c;\n}\n\n.s-unsynced {\n  @include sUnsynced();\n}\n\n.s-status-timeconductor-unsynced {\n  // Plot areas\n  .gl-plot .gl-plot-display-area {\n    @include sUnsynced();\n  }\n\n  // Object headers\n  .object-header {\n    .t-object-alert {\n      display: inline;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/_legacy-plots.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/********************************************************************* PLOTS */\nmct-plot {\n  display: contents;\n}\n\n/*********************** STACKED PLOT LAYOUT */\n.is-editing {\n  .gl-plot.child-frame {\n    @include hover {\n      background: rgba($editUIColorBg, 0.1);\n      box-shadow: inset rgba($editUIColorBg, 0.3) 0 0 0 1px;\n    }\n\n    &[s-selected] {\n      background: rgba($editUIColorBg, 0.2);\n      box-shadow: inset rgba($editUIColorBg, 0.8) 0 0 0 1px;\n      z-index: 2;\n    }\n  }\n\n  .plot-wrapper-axis-and-display-area {\n    pointer-events: none;\n  }\n}\n\n.c-plot,\n.gl-plot {\n  .s-status-taking-snapshot & {\n    .c-control-bar {\n      display: none;\n    }\n    .gl-plot-x-label__select,\n    .gl-plot-y-label__select {\n      display: none;\n    }\n  }\n\n  /*********************** MISSING ITEM INDICATORS */\n  .is-status__indicator {\n    font-size: 0.8em;\n  }\n}\n\n.c-plot {\n  @include abs($mainViewPad);\n  display: flex;\n  overflow: hidden;\n  min-height: $plotMinH;\n\n  .c-control-bar {\n    flex: 0 0 auto;\n    margin-bottom: $interiorMargin;\n  }\n\n  .l-view-section {\n    display: flex;\n    flex: 1 1 auto;\n    flex-direction: column;\n    overflow-y: auto;\n    overflow-x: hidden;\n  }\n\n  &.is-stale {\n    @include isStaleHolder();\n  }\n\n  .c-plot--stacked-container {\n    border: 1px solid transparent;\n    display: flex;\n    flex: 1 1 auto;\n    flex-direction: column;\n    min-height: $plotMinH;\n    overflow: hidden;\n\n    &[s-selected] {\n      .is-editing & {\n        border: $editMarqueeBorder;\n      }\n    }\n  }\n  &--stacked {\n    min-height: auto !important;\n\n    .child-frame {\n      .has-control-bar {\n        .c-control-bar {\n          // Hides buttons per plot element in a stacked plot\n          display: none;\n        }\n      }\n\n      mct-plot {\n        display: flex;\n        flex: 1 1 auto;\n        height: 100%;\n        position: relative;\n      }\n      flex: 1 1 auto;\n    }\n\n    .s-status-timeconductor-unsynced .holder-plot {\n      .t-object-alert.t-alert-unsynced {\n        display: block;\n      }\n    }\n  }\n\n  .--width-less-than-600 & {\n    .c-control-bar {\n      display: none;\n    }\n  }\n}\n\n.gl-plot {\n  display: flex;\n  flex: 1 1 auto;\n\n  /*********************** AXIS AND DISPLAY AREA */\n  .plot-wrapper-axis-and-display-area {\n    position: relative;\n    flex: 1 1 auto;\n    overflow: hidden;\n    //min-height: $plotMinH;\n  }\n\n  .gl-plot-wrapper-display-area-and-x-axis {\n    // Holds the plot area and the X-axis only\n    position: absolute;\n    top: nth($plotDisplayArea, 1);\n    right: 0;\n    bottom: 0;\n    left: nth($plotDisplayArea, 4);\n\n    .gl-plot-display-area {\n      overflow: hidden;\n      position: absolute;\n      top: 0;\n      right: 0;\n      bottom: nth($plotDisplayArea, 3);\n      left: 0;\n    }\n\n    .gl-plot-chart-area,\n    .gl-plot-chart-wrapper {\n      position: absolute;\n      top: 0;\n      right: 0;\n      bottom: 0;\n      left: 0;\n    }\n    .alt-pressed {\n      // When alt is being pressed and user is hovering over the plot, set the cursor\n      @include cursorGrab();\n    }\n\n    .gl-plot-axis-area.gl-plot-x {\n      top: auto;\n      right: 0;\n      bottom: 0;\n      left: 0;\n      height: $plotXBarH;\n      width: auto;\n      overflow: hidden;\n    }\n  }\n\n  .gl-plot-axis-area {\n    position: absolute;\n    &.gl-plot-y {\n      top: nth($plotDisplayArea, 1);\n      right: auto;\n      bottom: nth($plotDisplayArea, 3);\n      left: 0;\n      width: $plotYBarW;\n\n      &:hover {\n        .gl-plot-y-label__select {\n          display: block;\n        }\n      }\n    }\n\n    &.gl-plot-x {\n      @include hover {\n        .gl-plot-x-label__select {\n          display: block;\n        }\n      }\n    }\n  }\n\n  .gl-plot-coords {\n    // This does not appear to be in use in Open MCT\n    box-sizing: border-box;\n    border-radius: $controlCr;\n    background: black;\n    padding: 2px 5px;\n    position: absolute;\n    top: nth($plotDisplayArea, 1) + $interiorMarginLg;\n    right: auto;\n    bottom: auto;\n    left: nth($plotDisplayArea, 4) + $interiorMarginLg;\n    z-index: 10;\n    &:empty {\n      display: none;\n    }\n  }\n\n  .gl-plot-ticks,\n  .gl-plot-tick-wrapper {\n    height: 100%;\n  }\n\n  .gl-plot-label,\n  .l-plot-label {\n    position: absolute;\n    text-align: center;\n\n    &.gl-plot-x-label,\n    &.l-plot-x-label {\n      top: auto;\n      right: 0;\n      bottom: 0;\n      left: 0;\n      height: auto;\n    }\n\n    &.gl-plot-y-label {\n      display: flex;\n      left: 0;\n      right: auto;\n      bottom: 0;\n      text-orientation: mixed;\n      writing-mode: vertical-lr;\n      //z-index allows clicking on visibility icon\n      z-index: 2;\n\n      .icon-gear-after:after {\n        // Icon denoting configurability\n        margin-top: $interiorMargin; // Uses margin-top due to writing-mode\n      }\n\n      .icon-eye-open:before,\n      .icon-eye-disabled:before {\n        padding-top: 5px;\n      }\n\n      .plot-series-color-swatch {\n        @include colorSwatch();\n        display: inline-block;\n        flex: 0 0 auto;\n        margin-bottom: $interiorMargin; // Uses margin-bottom due to writing-mode\n      }\n    }\n  }\n  .gl-plot-y-label-swatch-container {\n    display: flex;\n    flex-direction: row;\n    overflow: auto;\n  }\n  .plot-yaxis-right {\n    &.gl-plot-y {\n      margin-left: 100%;\n    }\n\n    .gl-plot-label {\n      &.gl-plot-y-label {\n        left: auto;\n        right: 0;\n      }\n    }\n\n    .gl-plot-y-label__select {\n      left: 0;\n      right: auto;\n    }\n  }\n\n  .gl-plot-x-label__select {\n    position: absolute;\n    left: 50%;\n    bottom: 0;\n    transform: translateX(-50%);\n    z-index: 10;\n  }\n\n  .gl-plot-y-label__select {\n    position: absolute;\n    bottom: 2%;\n    transform: translateY(-50%);\n    left: 0;\n    z-index: 10;\n  }\n\n  .gl-plot-x-options,\n  .gl-plot-y-options {\n    $h: 24px;\n    position: absolute;\n    height: $h;\n    min-height: $h;\n    z-index: 2;\n  }\n\n  .gl-plot-x-options {\n    transform: translateX(-50%);\n    bottom: 0;\n    left: 50%;\n  }\n\n  .gl-plot-y-options {\n    transform: translateY(-50%);\n    min-width: 150px; // Need this due to enclosure of .select\n    top: 50%;\n    left: $plotYLabelW + $interiorMargin * 2;\n  }\n\n  .t-plot-display-controls {\n    position: absolute;\n    top: $interiorMargin;\n    right: $interiorMargin;\n  }\n\n  .gl-plot-hash {\n    position: absolute;\n    opacity: $opacityPlotHash;\n    &.hash-v {\n      border-right: 1px $colorPlotHash $stylePlotHash;\n      height: 100%;\n    }\n    &.hash-h {\n      border-bottom: 1px $colorPlotHash $stylePlotHash;\n      width: 100%;\n    }\n  }\n\n  &__local-controls {\n    // Plot local controls\n    $m: $interiorMargin;\n    display: flex;\n    align-items: center;\n    position: absolute;\n    top: $m;\n    left: $m;\n    z-index: 9;\n\n\n    &__reset {\n      transition: right 100ms;\n      top: $m;\n      right: $m;\n    }\n\n    &__zoom-and-guides {\n      top: $m;\n      right: $m;\n    }\n\n    .c-button {\n      box-shadow: $colorLocalControlOvrBg 0 0 0 2px;\n    }\n  }\n\n  .l-state-indicators {\n    color: $colorPausedBg;\n    position: absolute;\n    cursor: help;\n    font-size: 1.2em;\n    bottom: $interiorMarginSm;\n    left: $interiorMarginSm;\n    z-index: 2;\n\n    > * + * {\n      margin-left: $interiorMarginSm;\n    }\n\n    .t-alert-unsynced {\n      display: none;\n    }\n  }\n}\n\n.gl-plot-display-area,\n.plot-display-area {\n  @if $colorPlotBg != none {\n    background-color: $colorPlotBg;\n  }\n  cursor: crosshair;\n  border: 1px solid $colorPlotAreaBorder;\n}\n\n.tick {\n  position: absolute;\n  border: 0 $colorPlotHash solid;\n  &.tick-x {\n    border-right-width: 1px;\n    height: 100%; // Assumption is that the tick will be in a holder that will set it's height;\n  }\n}\n\n.gl-plot-tick,\n.tick-label {\n  @include reverseEllipsis();\n  font-size: 0.7rem;\n  position: absolute;\n  &.gl-plot-x-tick-label,\n  &.tick-label-x {\n    right: auto;\n    bottom: auto;\n    left: auto;\n    height: auto;\n    width: 20%;\n    margin-left: -10%;\n    text-align: center;\n  }\n  &.gl-plot-y-tick-label,\n  &.tick-label-y {\n    top: auto;\n    height: 1em;\n    width: auto;\n    margin-bottom: -0.5em;\n    text-align: right;\n  }\n}\n\n.gl-plot-tick {\n  &.gl-plot-x-tick-label {\n    top: $interiorMarginSm;\n  }\n  &.gl-plot-y-tick-label {\n    right: $interiorMarginSm;\n    left: auto;\n  }\n}\n\n.plot-yaxis-right {\n  .gl-plot-tick {\n    &.gl-plot-y-tick-label {\n      left: $interiorMarginSm;\n      right: auto;\n    }\n  }\n}\n.tick-label {\n  &.tick-label-x {\n    top: 0;\n  }\n  &.tick-label-y {\n    right: 0;\n    left: 0;\n  }\n}\n\n.export-plot {\n  $bg: white;\n  $fg: black;\n  $gry: #999;\n\n  background: $bg !important;\n  z-index: -10;\n\n  .l-view-section {\n    .s-status-timeconductor-unsynced .holder-plot {\n      .t-object-alert.t-alert-unsynced {\n        display: none;\n      }\n    }\n  }\n\n  .gl-plot-display-area {\n    background: none !important;\n    border-color: $gry !important;\n\n    .gl-plot-local-controls,\n    .h-local-controls {\n      opacity: 0;\n    }\n  }\n\n  .gl-plot {\n    color: $fg;\n\n    .gl-plot-hash {\n      opacity: 0.1;\n      border-color: $fg;\n    }\n  }\n\n  table {\n    thead {\n      border-bottom: none;\n\n      th {\n        background: #eee;\n        border-left-color: $bg;\n        color: #666;\n      }\n\n      tr {\n        border: none;\n      }\n    }\n    tbody {\n      tr {\n        border-top: 1px solid #ccc;\n      }\n\n      td {\n        color: $fg;\n      }\n    }\n  }\n}\n\n/****************** _LEGEND.SCSS */\n.gl-plot-legend,\n.c-plot-legend {\n  overflow: hidden;\n  flex: 0 0 auto; // Prevents clipping for all legend placements (top, bottom, etc.)\n\n  &__wrapper {\n    // Holds view-control and both collapsed and expanded legends\n    flex: 1 1 auto;\n    height: 100%;\n    overflow: auto;\n    padding: 2px;\n  }\n\n  &__view-control {\n    padding-top: 4px;\n    margin-right: $interiorMarginSm;\n  }\n\n  &__header {\n    @include propertiesHeader();\n    margin-bottom: $interiorMarginSm;\n  }\n\n  .--width-less-than-600 & {\n    &.is-legend-hidden {\n      display: none;\n    }\n  }\n}\n\n.c-plot--stacked {\n  .is-legend-hidden {\n    // Always show the legend in a stacked plot\n    display: flex !important;\n  }\n}\n\n.gl-plot-legend {\n  display: flex;\n  align-items: flex-start;\n\n  table {\n    table-layout: fixed;\n    th,\n    td {\n      @include ellipsize(); // Note: this won't work if table-layout uses anything other than fixed.\n      padding: 1px 3px; // Tighter than standard tabular padding\n    }\n  }\n}\n\n*[class*='-legend'] {\n  &.hover-on-plot {\n    // User is hovering over the plot to get a value at a point\n    .hover-value-enabled {\n      background-color: $legendHoverValueBg;\n      border-radius: $smallCr;\n      padding: 0 $interiorMarginSm;\n\n      &.value-to-display-min:before {\n        content: 'MIN ';\n      }\n      &.value-to-display-max:before {\n        content: 'MAX ';\n      }\n    }\n  }\n}\n\n/***************** GENERAL STYLES, ALL STATES */\n.plot-legend-item,\n.plot-series-limit-label {\n  // General styles for legend items, both expanded and collapsed legend states\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n\n  &.is-stale {\n    @include isStaleElement();\n  }\n\n  .plot-series-color-swatch {\n    @include colorSwatch();\n    display: inline-block;\n    flex: 0 0 auto;\n  }\n  .plot-series-name {\n    display: inline;\n    @include ellipsize();\n  }\n\n  .plot-series-value {\n    @include ellipsize();\n    @include isLimit();\n  }\n}\n\n.plot-series-swatch-and-name {\n  display: flex;\n  flex: 0 1 auto;\n  align-items: center;\n\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n}\n\n.plot-wrapper-expanded-legend {\n  flex: 1 1 auto;\n}\n\n.plot-legend-top .gl-plot-legend {\n  margin-bottom: $interiorMargin;\n}\n.plot-legend-bottom .gl-plot-legend {\n  margin-top: $interiorMargin;\n}\n.plot-legend-left .gl-plot-legend {\n  margin-right: $interiorMargin;\n}\n.plot-legend-right .gl-plot-legend {\n  margin-left: $interiorMargin;\n}\n\n.gl-plot,\n.c-plot {\n  &.plot-legend-collapsed .icon-cursor-lock::before {\n    padding-right: 5px;\n  }\n  &.plot-legend-expanded .icon-cursor-lock::before {\n    padding-right: 5px;\n  }\n\n  &.plot-legend-top .gl-plot-legend {\n    margin-bottom: $interiorMargin;\n  }\n  &.plot-legend-bottom .gl-plot-legend {\n    margin-top: $interiorMargin;\n  }\n  &.plot-legend-right .gl-plot-legend {\n    margin-left: $interiorMargin;\n  }\n  &.plot-legend-left .gl-plot-legend {\n    margin-right: $interiorMargin;\n  }\n\n  /***************** GENERAL STYLES, COLLAPSED */\n  &.plot-legend-collapsed {\n    // .plot-legend-item is a span of spans.\n\n    .plot-legend-item {\n      border-radius: $smallCr;\n      display: flex;\n      justify-content: stretch;\n\n      .plot-series-swatch-and-name,\n      .plot-series-value {\n        @include ellipsize();\n        flex: 1 1 auto;\n      }\n\n      .plot-series-value {\n        text-align: left;\n      }\n    }\n  }\n\n  /***************** GENERAL STYLES, EXPANDED */\n  &.plot-legend-expanded {\n    .gl-plot-legend {\n      max-height: 70%;\n    }\n\n    .plot-wrapper-expanded-legend {\n      overflow-y: auto;\n      table thead th {\n        background: $legendTableHeadBg;\n      }\n    }\n  }\n\n  /***************** TOP OR BOTTOM */\n  &.plot-legend-top,\n  &.plot-legend-bottom,\n  &.plot-legend-hidden {\n    // General styles when legend is on the top or bottom\n    // -hidden included for legacy plots\n    flex-direction: column;\n\n    &.plot-legend-collapsed {\n      // COLLAPSED ON TOP OR BOTTOM\n      .plot-wrapper-collapsed-legend {\n        display: flex;\n        flex: 1 1 auto;\n        overflow: hidden;\n\n        > .plot-legend-item + .plot-legend-item {\n          // Space between plot items\n          margin-left: $interiorMarginLg;\n        }\n      }\n    }\n  }\n\n  /***************** LEFT OR RIGHT */\n  &.plot-legend-left,\n  &.plot-legend-right {\n    // General styles when legend is on left or right\n\n    .gl-plot-legend {\n      max-height: inherit;\n    }\n\n    &.plot-legend-expanded {\n      // EXPANDED, ON EITHER SIDE\n      .gl-plot-legend {\n        width: $plotLegendWidthExpanded;\n      }\n    }\n\n    &.plot-legend-collapsed {\n      // COLLAPSED, ON EITHER SIDE\n      .gl-plot-legend {\n        width: $plotLegendWidthCollapsed;\n      }\n\n      .plot-wrapper-collapsed-legend {\n        display: flex;\n        flex-flow: column nowrap;\n        min-width: 0;\n        flex: 1 1 auto;\n        overflow-y: auto;\n\n        > * + * {\n          // Space between plot items\n          margin-top: $interiorMarginSm;\n        }\n      }\n      .plot-legend-item {\n        margin-bottom: $interiorMarginSm;\n        margin-left: 0;\n        flex-wrap: nowrap;\n        .plot-series-swatch-and-name {\n          @include ellipsize();\n          flex: 0 1 auto;\n          min-width: 20%;\n        }\n        .plot-series-value {\n          flex: 0 1 auto;\n          width: auto;\n        }\n      }\n    }\n  }\n\n  /***************** ON BOTTOM OR RIGHT */\n  &.plot-legend-right,\n  &.plot-legend-bottom {\n    .gl-plot-legend {\n      order: 2;\n    }\n    .plot-wrapper-axis-and-display-area {\n      order: 1;\n    }\n  }\n}\n\n/***************** STACKED PLOT LEGEND OVERRIDES */\n.c-plot--stacked {\n  // Always show the legend on top, ignore any position setting\n  .c-plot,\n  .gl-plot {\n    flex-direction: column !important;\n\n    .c-plot-legend,\n    .gl-plot-legend {\n      margin: 0;\n      margin-bottom: $interiorMargin;\n      order: 1 !important;\n      width: 100% !important;\n\n      .plot-wrapper-collapsed-legend {\n        flex-direction: row !important;\n      }\n    }\n    .plot-wrapper-axis-and-display-area {\n      order: 2 !important;\n    }\n  }\n}\n\n/***************** BAR GRAPHS */\n.c-bar-chart {\n  flex: 1 1 auto;\n  overflow: hidden;\n}\n\n/***************** SCATTER PLOTS */\n.c-scatter-chart {\n  flex: 1 1 auto;\n  overflow: hidden;\n}\n\n/***************** CURSOR GUIDES */\n[class*='c-cursor-guide'] {\n  box-shadow: $shdwCursorGuide;\n  background-color: $colorCursorGuide;\n  display: none; // Displayed when an element with has-cursor-guides gets a hover; see below\n  pointer-events: none;\n  position: absolute;\n}\n\n.has-cursor-guides:hover [class*='c-cursor-guide'] {\n  display: block;\n}\n\n.c-cursor-guide {\n  &--h {\n    height: 1px;\n    left: 0;\n    right: 0;\n  }\n\n  &--v {\n    width: 1px;\n    top: 0;\n    bottom: 0;\n  }\n}\n\n.s-status-timeconductor-unsynced {\n  .t-alert-unsynced {\n    display: inline-block !important;\n  }\n}\n\n/*********************** CURSOR LOCK INDICATOR */\n[class*='c-state-indicator__alert-cursor-lock'] {\n  display: none;\n}\n\n[class*='is-cursor-locked'] {\n  background: rgba($colorInfo, 0.1);\n\n  [class*='c-state-indicator__alert-cursor-lock'] {\n    @include userSelectNone();\n    color: $colorInfo;\n    display: block;\n    margin-right: $interiorMarginSm;\n\n    &[class*='--verbose'] {\n      padding: $interiorMarginSm;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/_legacy.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@use 'sass:math';\n\n/*********************************************************************** CLOCKS AND TIMERS */\n.c-clock,\n.c-timer {\n  display: flex;\n  align-items: center;\n  font-size: 1.25em;\n  overflow: hidden;\n\n  > * {\n    flex: 0 0 auto;\n    display: flex;\n    align-items: center;\n  }\n\n  .c-frame & {\n    // When in a Display or Flexible Layout\n    @include abs();\n    padding: $interiorMargin;\n  }\n}\n\n.c-clock {\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n\n  &__timezone-selection .c-menu {\n    // Menu for selecting timezones in properties dialog\n    max-height: 200px;\n  }\n}\n\n.c-timer {\n  $ctrlW: 22px;\n\n  &__controls {\n    font-size: 1rem !important;\n    margin-right: 0;\n    min-width: 0;\n    overflow: hidden;\n    @include transition($prop: width, $dur: $transOutTime);\n    width: 0;\n\n    .c-icon-button:before {\n      font-size: 1em;\n    }\n  }\n\n  &__direction {\n    font-size: 0.7em !important;\n    margin-right: $interiorMargin;\n  }\n\n  &__ng-controller {\n    font-size: 0;\n    width: 0;\n  }\n\n  &:hover {\n    .c-timer__controls {\n      @include transition(\n        $prop: width,\n        $dur: $transOutTime\n      ); // On purpose: want this to take a bit longer\n      margin-right: $interiorMargin;\n      width: $ctrlW * 2;\n    }\n\n    &.is-stopped .c-timer__controls {\n      width: $ctrlW;\n    }\n  }\n\n  &__direction,\n  &__value {\n    opacity: 0.5;\n  }\n\n  &.is-started {\n    .c-timer {\n      &__direction,\n      &__value {\n        opacity: 1;\n      }\n    }\n  }\n}\n\n/*********************************************************************** SUMMARY WIDGETS */\n/************************* WIDGET OBJECT */\n@mixin cSummaryWidget() {\n  box-shadow: $shdwBtns;\n  border-radius: $basicCr;\n  border-style: solid;\n  border-width: 1px;\n  cursor: default;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  &[href] {\n    cursor: pointer;\n  }\n\n  &__icon {\n    // Hide the icon holder element. Selector below shows this once 'icon-*' is added.\n    display: none;\n    font-size: 0.9em;\n  }\n\n  &__label {\n    @include ellipsize();\n  }\n\n  [class*='icon-'] {\n    // When 'icon-*' is added, show this element and add margin\n    display: block;\n    margin-right: $interiorMarginSm;\n  }\n}\n\n.c-summary-widget,\n.c-sw {\n  @include cSummaryWidget();\n  padding: $interiorMarginLg $interiorMarginLg * 2;\n\n  &--thumb {\n    max-width: 30%;\n    padding: $interiorMarginSm $interiorMargin;\n  }\n}\n\n.widget-edit-holder {\n  // Hide edit area when in browse mode\n  display: none;\n}\n\n.widget-rule-header {\n  display: flex;\n  align-items: center;\n\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n}\n\n[class*='action-buttons-wrapper'] {\n  white-space: nowrap;\n  line-height: $btnStdH;\n}\n\n.widget-rules-wrapper,\n.widget-rule-content,\n.w-widget-test-data-content {\n  min-height: 0;\n  height: 0;\n  opacity: 0;\n  overflow: hidden;\n  pointer-events: none;\n}\n\n.widget-rules-wrapper {\n  flex: 1 1 auto !important;\n}\n\n.widget-rule-content.expanded {\n  overflow: visible !important;\n  min-height: 50px;\n  height: auto;\n  margin-top: $interiorMargin;\n  opacity: 1;\n  pointer-events: inherit;\n}\n\n.w-widget-test-data-content {\n  .l-enable {\n    padding: $interiorMargin 0;\n  }\n\n  .w-widget-test-data-items {\n    max-height: 20vh;\n    overflow-y: scroll !important;\n    padding-right: $interiorMargin;\n  }\n}\n\n.l-widget-thumb-wrapper,\n.l-compact-form label {\n  $ruleLabelW: 40%;\n  $ruleLabelMaxW: 150px;\n  display: flex;\n  max-width: $ruleLabelMaxW;\n  width: $ruleLabelW;\n}\n\n.js-summary-widget__message {\n  display: none;\n}\n\n/**************\\ EDITING A WIDGET */\n.w-summary-widget {\n  // Classes for editor layout while editing a widget\n  // This selector is ugly and brittle, but needed to prevent interface from showing when widget is in a layout\n  // being edited.\n  @include abs();\n  display: flex;\n  flex-direction: column;\n\n  > .l-summary-widget {\n    // Main view of the summary widget\n    // Give some airspace and center the widget in the area\n    margin: 30px auto;\n  }\n\n  .widget-edit-holder {\n    display: flex; // Overrides `display: none` during Browse mode\n    flex: 1 1 auto;\n    overflow: hidden;\n\n    .flex-accordion-holder {\n      // Needed because otherwise accordion elements \"creep\" when contents expand and contract\n      display: block !important;\n    }\n    &.expanded-widget-test-data {\n      .w-widget-test-data-content {\n        min-height: 50px;\n        height: auto;\n        opacity: 1;\n        pointer-events: inherit;\n      }\n      &:not(.expanded-widget-rules) {\n        // Test data is expanded and rules are collapsed\n        // Make text data take up all the vertical space\n        .flex-accordion-holder {\n          display: flex;\n        }\n      }\n    }\n    &.expanded-widget-rules {\n      .widget-rules-wrapper {\n        min-height: 50px;\n        height: 100%; // Fix for Chrome 73 scrolling bug\n        opacity: 1;\n        pointer-events: inherit;\n      }\n    }\n  }\n\n  &.s-status-no-data {\n    .widget-edit-holder {\n      opacity: 0.3;\n      pointer-events: none;\n    }\n    .js-summary-widget__message {\n      display: flex;\n    }\n  }\n\n  .l-compact-form {\n    // Overrides on .l-compact-form\n    ul {\n      &:last-child {\n        margin: 0;\n      }\n\n      li {\n        &:not(.widget-rule-header) {\n          &:not(.connects-to-previous) {\n            border-top: 1px solid $colorFormLines;\n          }\n        }\n        &.connects-to-previous {\n          padding: $interiorMargin 0;\n        }\n\n        > label {\n          display: block; // Needed to align text to right\n          text-align: right;\n          width: 90px;\n          flex: 0 0 auto;\n        }\n\n        .controls {\n          display: flex;\n          flex-wrap: wrap;\n          align-items: center;\n          align-content: stretch;\n          > * + * {\n            margin-left: $interiorMarginSm;\n          }\n        }\n      }\n    }\n\n    &.s-widget-test-data-item {\n      // Single line of ul li label span, etc.\n      ul {\n        li {\n          border: none !important;\n          > label {\n            display: inline-block;\n            width: auto;\n            text-align: left;\n          }\n        }\n      }\n    }\n  }\n\n  .t-condition .controls {\n    > * {\n      margin-bottom: $interiorMargin;\n    }\n  }\n}\n\n.widget-edit-holder {\n  font-size: 0.8rem;\n}\n\n.widget-rules-wrapper {\n  // Wrapper area that holds n rules\n  box-sizing: border-box;\n  overflow-y: scroll;\n  padding-right: $interiorMargin;\n}\n\n.l-widget-rule,\n.l-widget-test-data-item {\n  box-sizing: border-box;\n  margin-bottom: $interiorMarginSm;\n  padding: $interiorMargin $interiorMarginLg;\n}\n\n.rule-title {\n  flex: 0 1 auto;\n  color: pullForward($colorBodyFg, 50%);\n}\n\n.rule-description {\n  flex: 1 1 auto;\n  @include ellipsize();\n  color: pushBack($colorBodyFg, 20%);\n}\n\n.s-widget-rule,\n.s-widget-test-data-item {\n  background-color: rgba($colorBodyFg, 0.1);\n  border-radius: $basicCr;\n}\n\n.c-sw-edit {\n  padding: $interiorMargin;\n\n  &__ui {\n    display: flex;\n    flex-direction: column;\n\n    &__header {\n      border-top: 1px solid $colorInteriorBorder;\n      display: flex;\n      align-items: center;\n      margin: $interiorMargin 0;\n      padding: $interiorMargin 0;\n      text-transform: uppercase;\n      > * + * {\n        margin-left: $interiorMarginSm;\n      }\n    }\n  }\n}\n\n.c-sw-rule {\n  &__grippy-wrapper {\n    $d: 8px;\n    flex: 0 0 auto;\n    cursor: move;\n    width: $d;\n    height: $d;\n    transform: translateY(-1px);\n  }\n\n  &__grippy {\n    @include grippy($c: $colorItemTreeVC, $dir: 'y');\n    @include abs();\n  }\n}\n\n/******************************************************************* CHANNEL SELECTOR */\n.channel-selector {\n  .line {\n    margin-bottom: $interiorMargin;\n    min-height: $formInputH;\n  }\n  .treeview {\n    $myBg: darken($colorBodyBg, 2%);\n    @include reactive-input();\n    min-height: 300px;\n    max-height: 400px;\n    overflow: auto;\n    padding: $interiorMargin;\n  }\n\n  .btns-add-remove {\n    margin-top: 150px;\n    .s-button {\n      display: block;\n      margin-bottom: $interiorMargin;\n      text-align: center;\n    }\n  }\n}\n\n/******************************************************************* AUTOFLOW TABULAR */\n// NOT UNIT TESTED AS OF 3/12/19\n.autoflow {\n  $headerH: $formInputH;\n  $colMargin: $interiorMargin;\n  $colW: 225px;\n  $valW: 70px;\n  $valPad: 5px;\n  $rowH: 15px;\n  font-size: 0.75rem;\n\n  &:hover {\n    .l-autoflow-header .s-button.change-column-width {\n      opacity: 1;\n    }\n  }\n\n  .l-autoflow-header {\n    bottom: auto;\n    height: $headerH;\n    line-height: $headerH;\n    min-width: $colW;\n    .t-last-update {\n      overflow: hidden;\n    }\n    .s-button.change-column-width {\n      @include transition($prop: opacity, $dur: $transOutTime);\n      opacity: 0;\n    }\n    .l-filter {\n      display: block;\n      margin-right: $interiorMargin;\n      input.t-filter-input {\n        width: 150px;\n      }\n    }\n  }\n\n  .l-autoflow-items {\n    overflow-x: scroll;\n    overflow-y: hidden;\n    top: $headerH + $interiorMargin * 2;\n    white-space: nowrap;\n    .l-autoflow-col {\n      box-sizing: border-box;\n      border-left: 1px solid $colorInteriorBorder;\n      display: inline-block;\n      padding-left: $colMargin;\n      padding-right: $colMargin;\n      vertical-align: top;\n      width: $colW;\n      .l-autoflow-row {\n        box-sizing: border-box;\n        border-bottom: 1px solid rgba(#fff, 0.05);\n        display: block;\n        height: $rowH;\n        line-height: $rowH;\n        margin-bottom: 1px;\n        margin-top: 1px;\n        position: relative;\n        &:first-child {\n          border-top: none;\n        }\n        &:hover {\n          background: rgba(#fff, 0.1);\n        }\n        .l-autoflow-item.r {\n          color: lighten($colorBodyFg, 10%);\n        }\n        &.first-in-group {\n          border-top: 1px solid lighten($colorInteriorBorder, 20%);\n        }\n        .l-autoflow-item {\n          display: block;\n          &.l {\n            float: none;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n            width: auto;\n          }\n          &.r {\n            border-radius: $smallCr;\n            float: right;\n            margin-left: $interiorMargin;\n            padding-left: $valPad;\n            padding-right: $valPad;\n            text-align: right;\n          }\n        }\n      }\n\n      &:first-child {\n        border-left: none;\n        padding-left: 0;\n      }\n    }\n  }\n}\n\n.frame {\n  &.child-frame.panel {\n    .autoflow .l-autoflow-header .l-filter {\n      display: none;\n    }\n  }\n}\n\n/******************************************************************* INDICATORS */\n/* Indicators are generally only displayed in the ue-bottom-bar element of the main interface */\n\n.h-indicator,\nmct-indicators mct-include {\n  display: inline; // Fallback for display: contents\n  display: contents;\n}\n\n/************************************************* DATETIME UI */\n@mixin complexFieldHolder($myW) {\n  width: $myW + $interiorMargin;\n  input[type='text'] {\n    width: $myW;\n  }\n}\n\n.complex.datetime {\n  span {\n    display: inline-block;\n    margin-right: $interiorMargin;\n  }\n  .fields {\n    margin-top: $interiorMarginSm 0;\n    padding: $interiorMarginSm 0;\n  }\n\n  .date {\n    @include complexFieldHolder(80px);\n  }\n\n  .time.md {\n    @include complexFieldHolder(60px);\n  }\n\n  .time.sm {\n    @include complexFieldHolder(40px);\n  }\n}\n\n/************************************************* INFO BUBBLES */\n.l-infobubble-wrapper {\n  $arwSize: 5px;\n  box-shadow: rgba(black, 0.4) 0 1px 5px;\n  position: relative;\n  z-index: 50;\n  .l-infobubble {\n    display: inline-block;\n    min-width: $bubbleMinW;\n    max-width: $bubbleMaxW;\n    padding: 5px 10px;\n    &:before {\n      content: '';\n      position: absolute;\n      width: 0;\n      height: 0;\n    }\n    table {\n      width: 100%;\n      tr {\n        td {\n          padding: 2px 0;\n          vertical-align: top;\n          &.label {\n            padding-right: $interiorMargin * 2;\n            white-space: nowrap;\n          }\n          &.value {\n            //word-wrap: break-word; // Doesn't work in <td>?\n            word-break: break-all;\n          }\n          &.align-wrap {\n            white-space: normal;\n          }\n        }\n      }\n    }\n    .title {\n      @include ellipsize();\n      margin-bottom: $interiorMargin;\n    }\n  }\n\n  &.arw-down {\n    margin-bottom: $arwSize * 2;\n    .l-infobubble::before {\n      left: 50%;\n      top: 100%;\n      margin-left: -1 * $arwSize;\n      border-left: $arwSize solid transparent;\n      border-right: $arwSize solid transparent;\n      border-top: ($arwSize * 1.5) solid $colorInfoBubbleBg;\n    }\n  }\n  .arw {\n    z-index: 2;\n  }\n  &.arw-up .arw.arw-down,\n  &.arw-down .arw.arw-up {\n    display: none;\n  }\n}\n\nbody.desktop {\n  .l-infobubble {\n    &.arw-left {\n      margin-left: $bubbleArwSize * 2;\n      &:before {\n        @include triangle('left', $bubbleArwSize, 1.5, $colorInfoBubbleBg);\n        right: 100%;\n      }\n    }\n\n    &.arw-right {\n      margin-right: $bubbleArwSize * 2;\n      &:before {\n        @include triangle('right', $bubbleArwSize, 1.5, $colorInfoBubbleBg);\n        left: 100%;\n      }\n    }\n\n    &.arw-top {\n      &:before {\n        top: $bubbleArwSize * 2;\n      }\n    }\n\n    &.arw-btm {\n      &:before {\n        bottom: $bubbleArwSize * 2;\n      }\n    }\n  }\n}\n\n.l-thumbsbubble-wrapper {\n  .arw-up {\n    @include triangle('up', $bubbleArwSize, 1.5, $colorThumbsBubbleBg);\n  }\n  .arw-down {\n    @include triangle('down', $bubbleArwSize, 1.5, $colorThumbsBubbleBg);\n  }\n}\n\n.s-infobubble {\n  $emFg: darken($colorInfoBubbleFg, 20%);\n  border-radius: $basicCr;\n  box-shadow: rgba(black, 0.4) 0 1px 5px;\n  background: $colorInfoBubbleBg;\n  color: $colorInfoBubbleFg;\n  font-size: 0.8rem;\n  .title {\n    color: $emFg;\n    font-weight: bold;\n  }\n  table {\n    tr {\n      td {\n        border: none;\n        border-top: 1px solid darken($colorInfoBubbleBg, 10%) !important;\n        font-size: 0.9em;\n      }\n\n      &:first-child td {\n        border-top: none !important;\n      }\n    }\n  }\n  &:first-child td {\n    border-top: none;\n  }\n\n  .label {\n    color: lighten($emFg, 30%);\n  }\n\n  .value {\n    color: $emFg;\n  }\n}\n\n.s-thumbsbubble {\n  background: $colorThumbsBubbleBg;\n  color: $colorThumbsBubbleFg;\n}\n\n/***************************************************************** SPLITTERS */\n.splitter {\n  display: block;\n  position: absolute;\n  z-index: 3;\n  &:after {\n    // The handle\n    content: '';\n    pointer-events: none;\n    @include abs(0);\n    background: $colorSplitterBg;\n    display: block;\n    z-index: 4;\n  }\n  &:active {\n    &:after {\n      background-color: $colorSplitterActive !important;\n    }\n  }\n\n  @if $colorSplitterHover != 'none' {\n    &:not(:active) {\n      &:hover {\n        &:after {\n          background-color: $colorSplitterHover !important;\n          @include transition($prop: background-color, $dur: 150ms);\n        }\n      }\n    }\n  }\n}\n\n.split-layout {\n  $inset: $splitterHandleHitMargin;\n  &.horizontal {\n    // Slides vertically up and down, splitting the element horizontally\n    overflow: hidden; // Suppress overall scroll; each internal pane handles its own overflow\n    .pane {\n      left: 0;\n      right: 0;\n      &.top {\n        bottom: auto;\n      }\n      &.bottom {\n        top: auto;\n      }\n    }\n    > .splitter {\n      cursor: row-resize;\n      left: 0;\n      right: 0;\n      height: $splitterHandleD;\n      &:after {\n        top: $inset;\n        bottom: $inset;\n      }\n    }\n  }\n\n  &.vertical {\n    // Slides horizontally left to right, splitting the element vertically\n    .pane {\n      top: 0;\n      bottom: 0;\n      &.left {\n        right: auto;\n      }\n      &.right {\n        left: auto;\n      }\n    }\n    > .splitter {\n      cursor: col-resize;\n      top: 0;\n      bottom: 0;\n      width: $splitterHandleD;\n      &:after {\n        left: $inset;\n        right: $inset;\n        //width: $splitterHandleD;\n      }\n      &.flush-right {\n        width: ceil(math.div($splitterHandleD, 2));\n        &:after {\n          width: $splitterHandleD;\n          left: auto;\n          right: 0;\n        }\n        &.edge-shdw {\n          background-image: linear-gradient(\n            90deg,\n            rgba(black, 0) 40%,\n            rgba(black, 0.05) 70%,\n            rgba(black, 0.2) 100%\n          );\n        }\n      }\n    }\n  }\n}\n\n/******************************************************************* FLEX STYLES */\n.l-flex-row,\n.l-flex-col {\n  display: flex;\n  flex-wrap: nowrap;\n  .flex-elem {\n    min-height: 0; // Needed to allow element to shrink within parent\n    position: relative;\n    &:not(.grows) {\n      flex: 0 0 auto;\n      &.flex-can-shrink {\n        flex: 0 1 auto;\n      }\n    }\n    &.grows {\n      flex: 1 1 auto;\n    }\n    &.contents-align-right {\n      text-align: right;\n    }\n  }\n  .flex-container {\n    // Apply to wrapping elements, mct-includes, etc.\n    display: flex;\n    flex-wrap: nowrap;\n    flex: 1 1 auto;\n    min-height: 0;\n  }\n}\n\n.l-flex-row {\n  flex-direction: row;\n  &.flex-elem {\n    flex: 1 1 auto;\n  }\n  > .flex-elem {\n    min-width: 0;\n    &.holder:not(:last-child) {\n      margin-right: $interiorMargin;\n    }\n  }\n  .flex-container {\n    flex-direction: row;\n  }\n}\n\n.l-flex-col {\n  flex-direction: column;\n  > .flex-elem {\n    min-height: 0;\n    &.holder:not(:last-child) {\n      margin-bottom: $interiorMarginLg;\n    }\n  }\n  &.l-flex-accordion .flex-accordion-holder {\n    display: flex;\n    flex-direction: column;\n  }\n  .flex-container {\n    flex-direction: column;\n  }\n}\n\n.flex-fixed {\n  flex: 0 0 auto;\n}\n\n.flex-justify-end {\n  justify-content: flex-end;\n}\n\n/******************************************************************* GRID STYLES */\n.grid-two-column,\n.grid-properties {\n  display: grid;\n  grid-row-gap: 0;\n  grid-template-columns: 1fr 2fr;\n}\n\n.grid-span-all,\n.grid-two-column-span-cols {\n  grid-column: 1 / 3;\n}\n\n.grid-elem {\n  &:not(:first-child) {\n    border-top: 1px solid $colorInteriorBorder;\n  }\n  &.label {\n    background-color: rgba(0, 0, 128, 0.2);\n  }\n  &.value {\n    background-color: rgba(0, 128, 0, 0.2);\n  }\n}\n\n.grid-row {\n  display: contents;\n}\n\n.grid-row {\n  .grid-cell {\n    padding: 3px $interiorMarginLg 3px 0;\n    &[title]:not([title='']) {\n      // When a cell has a title, assume it's helpful text\n      cursor: help;\n    }\n  }\n  &.force-border,\n  &:not(:first-of-type) {\n    // Row borders, effected via border-top on child elements of the row\n    .grid-cell {\n      border-top: 1px solid $colorInspectorSectionHeaderBg;\n    }\n  }\n}\n\n/******************************************************************* ABOUT SCREEN */\n.l-about {\n  &.abs {\n    overflow: auto;\n  }\n  $contentH: 200px;\n  .l-splash {\n    position: relative;\n    height: 45%;\n  }\n  .l-content {\n    position: relative;\n    margin-top: $interiorMarginLg;\n  }\n}\n\n.s-about {\n  line-height: 120%;\n\n  a {\n    color: $colorAboutLink;\n  }\n\n  h1,\n  h2,\n  h3 {\n    color: pullForward($colorBodyFg, 20%);\n    margin-bottom: 1em;\n  }\n\n  h1 {\n    font-size: 2.25em;\n  }\n\n  h2 {\n    border-top: 1px solid $colorInteriorBorder;\n    font-size: 1.5em;\n    margin-top: 2em;\n    padding-top: 1em;\n  }\n\n  h3 {\n    margin-top: 2em;\n  }\n\n  .s-description,\n  .s-button {\n    line-height: 2em;\n  }\n  .l-licenses-software {\n    .l-license-software {\n      border-top: 1px solid $colorInteriorBorder;\n      padding: 0.5em 0;\n      &:first-child {\n        border-top: none;\n      }\n      em {\n        color: pushBack($colorBodyFg, 20%);\n      }\n      h3 {\n        font-size: 1.25em;\n      }\n      .s-license-text {\n        font-size: 0.9em;\n      }\n    }\n  }\n}\n\n/******************************************************************* STARTUP / SPLASH SCREEN */\n@mixin splashElem($m: 20%) {\n  top: $m;\n  right: $m * 1.25;\n  bottom: $m;\n  left: $m * 1.25;\n}\n\n.l-splash,\n.l-splash:before,\n.l-splash:after {\n  background-position: center;\n  background-repeat: no-repeat;\n  position: absolute;\n}\n\n.l-splash {\n  background-size: cover;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  &:before,\n  &:after {\n    background-size: contain;\n    content: '';\n  }\n\n  &:before {\n    // NASA logo, dude\n    $w: 5%;\n    $m: 10px;\n    background-image: url('../ui/layout/assets/images/logo-nasa.svg');\n    top: $m;\n    right: auto;\n    bottom: auto;\n    left: $m;\n    height: auto;\n    width: $w * 2;\n    padding-bottom: $w;\n    padding-top: $w;\n  }\n\n  &:after {\n    // App logo\n    top: 0;\n    right: 15%;\n    bottom: 0;\n    left: 15%;\n  }\n}\n\n.l-splash-holder {\n  // Main outer holder for splash.\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 10000;\n  opacity: 1;\n  .l-splash {\n    // The splash element.\n    @include splashElem();\n  }\n}\n\n@media only screen and (max-device-width: 767px) {\n  .l-splash-holder .l-splash {\n    @include splashElem(0);\n    border-radius: 0;\n    box-shadow: none;\n  }\n}\n\n@media only screen and (max-device-width: 767px) and (orientation: portrait) {\n  .l-splash-holder .l-splash {\n    &:before {\n      // Make the NASA logo a bit bigger when we're in portrait mode.\n      $w: 12%;\n      width: $w * 2;\n      padding-bottom: $w;\n      padding-top: $w;\n    }\n  }\n}\n\n/******************************************************************* VARIOUS */\n.c-overlay mct-include {\n  display: inline; // Fallback for display: contents\n  display: contents;\n}\n\nmct-container {\n  display: block;\n}\n\n.overlay {\n  .outer-holder {\n    background: $colorMenuBg;\n    color: $colorMenuFg !important;\n  }\n}\n\n.t-popup {\n  z-index: 75;\n}\n\n.form .form-row {\n  .label {\n    color: $colorMenuFg !important;\n  }\n  .selector-list {\n    @include reactive-input();\n    background: $colorInputBg !important;\n    color: $colorInputFg !important;\n  }\n}\n\n.ui-symbol.view-control {\n  display: block;\n  transform-origin: center center;\n\n  &:before {\n    content: $glyph-icon-arrow-right-equilateral;\n  }\n\n  &.expanded {\n    transform: rotate(90deg);\n  }\n}\n\n.t-frame-outer {\n  min-width: 200px;\n  min-height: 200px;\n}\n\n.l-iframe {\n  iframe {\n    display: block;\n    height: 100%;\n    width: 100%;\n    border: none;\n  }\n}\n\n// Alert elements in views\n.s-unsynced {\n  @include sUnsynced();\n}\n\n.s-status-timeconductor-unsynced {\n  // Plot areas\n  .gl-plot .gl-plot-display-area {\n    @include sUnsynced();\n  }\n\n  // Object headers\n  .object-header {\n    .t-object-alert {\n      display: inline;\n    }\n  }\n}\n\n.abs {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  height: auto;\n  width: auto;\n}\n\n.code {\n  font-family: 'Lucida Console', monospace;\n  font-size: 0.7em;\n  line-height: 150%;\n  white-space: pre;\n}\n\n.codehilite {\n  @extend .code;\n  background-color: rgba($colorBodyFg, 0.1);\n  padding: 1em;\n}\n\n.s-status-missing {\n  // Labels. Expects .s-status-missing to be applied to mct-representation that contains\n  .t-object-label .t-item-icon:before {\n    content: $glyph-icon-object-unknown;\n  }\n\n  // Item, grid item. Expects .s-status-missing to be applied to mct-representation that contains .item.grid-item\n  .item .t-item-icon-glyph:before {\n    content: $glyph-icon-object-unknown;\n  }\n\n  // Object header. Expects .s-status-missing to be applied to mct-representation.object-header\n  &.object-header {\n    .type-icon:before {\n      content: $glyph-icon-object-unknown;\n    }\n  }\n\n  // Tree item. Expects .s-status-missing to be applied to .tree-item,\n  // and mct-representation.search-item\n  &.tree-item,\n  &.search-item {\n    > .rep-object-label .t-item-icon:before {\n      content: $glyph-icon-object-unknown;\n    }\n  }\n}\n\n.align-right {\n  text-align: right;\n}\n\n.centered {\n  text-align: center;\n}\n\n.no-selection {\n  // aka selection = \"None\". Used in palettes and their menu buttons.\n  $c: red;\n  $s: 48%;\n  $e: 52%;\n  background-image: linear-gradient(-45deg, transparent $s - 5%, $c $s, $c $e, transparent $e + 5%);\n  box-shadow: inset rgba(black, 0.3) 0 0 0 1px;\n  background-repeat: no-repeat;\n  background-size: contain;\n}\n\n.scrolling,\n.scroll {\n  overflow: auto;\n}\n\n.vscroll {\n  overflow-x: hidden;\n  overflow-y: auto;\n  &.scroll-pad {\n    padding-right: $interiorMargin;\n  }\n}\n\n.vscroll--persist {\n  overflow-x: hidden;\n  overflow-y: scroll;\n}\n\n.slidable {\n  cursor: move; // Fallback\n  cursor: grab;\n  cursor: -moz-grab;\n  cursor: -webkit-grab;\n  &.horz {\n    cursor: col-resize;\n  }\n  &.vert {\n    cursor: row-resize;\n  }\n}\n\n.no-margin {\n  margin: 0;\n}\n\n.ds {\n  box-shadow: rgba(#000, 0.7) 0 4px 10px 2px;\n}\n\n.capitalize {\n  text-transform: capitalize;\n}\n\n.hide,\n.hidden,\n.t-main-view .hide-in-t-main-view {\n  display: none !important;\n}\n\n.hide-nice {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.invisible {\n  display: block;\n  visibility: hidden;\n  height: 0;\n  padding: 0;\n  border: 0;\n  margin: 0 !important;\n  transform: scale(0);\n  pointer-events: none;\n  position: absolute;\n}\n\n.sep {\n  color: rgba(#fff, 0.2);\n}\n\n.comma-list span {\n  &:not(:first-child) {\n    &:before {\n      content: ', ';\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/_limits.scss",
    "content": "$plotLimitLineSize: 1px;\n$plotLimitDashWidthOffset: 10px;\n$lineBlocker: $colorPlotLimitLineBg;\n$plotLimitDashWidthSeverity: 50px;\n$plotLimitDashWidthCritical: $plotLimitDashWidthSeverity - $plotLimitDashWidthOffset;\n$plotLimitDashWidthDistress: $plotLimitDashWidthCritical - $plotLimitDashWidthOffset;\n$plotLimitDashWidthWarning: $plotLimitDashWidthDistress - $plotLimitDashWidthOffset;\n$plotLimitDashWidthWatch: $plotLimitDashWidthWarning - $plotLimitDashWidthOffset;\n\n@mixin plotLimitLine($c, $breakPerc) {\n  background: $lineBlocker\n    linear-gradient(90deg, $c $breakPerc, transparent $breakPerc, transparent 100%) repeat-x;\n}\n\n@mixin plotLimitDirectionGradient($c, $deg: 0deg) {\n  background: linear-gradient($deg, $c, transparent);\n}\n\n@mixin plotLimitLineUpper($c) {\n  $breakPerc: 80%;\n  @include plotLimitLine($c: $c, $breakPerc: $breakPerc);\n}\n\n@mixin plotLimitLineLower($c) {\n  $breakPerc: 30%;\n  @include plotLimitLine($c: $c, $breakPerc: $breakPerc);\n}\n\n.c-plot-limit-line {\n  box-shadow: $lineBlocker 0 0 0 2px;\n  height: $plotLimitLineSize;\n  width: 100%;\n  position: absolute;\n  z-index: 1;\n\n  // Colors and directions\n  &--purple.c-plot-limit-line--upr {\n    @include plotLimitLineUpper($colorLimitPurpleIc);\n  }\n\n  &--purple.c-plot-limit-line--lwr {\n    @include plotLimitLineLower($colorLimitPurpleIc);\n  }\n\n  &--red.c-plot-limit-line--upr {\n    @include plotLimitLineUpper($colorLimitRedIc);\n  }\n\n  &--red.c-plot-limit-line--lwr {\n    @include plotLimitLineLower($colorLimitRedIc);\n  }\n\n  &--orange.c-plot-limit-line--upr {\n    @include plotLimitLineUpper($colorLimitOrangeIc);\n  }\n\n  &--orange.c-plot-limit-line--lwr {\n    @include plotLimitLineLower($colorLimitOrangeIc);\n  }\n\n  &--yellow.c-plot-limit-line--upr {\n    @include plotLimitLineUpper($colorLimitYellowIc);\n  }\n\n  &--yellow.c-plot-limit-line--lwr {\n    @include plotLimitLineLower($colorLimitYellowIc);\n  }\n\n  &--cyan.c-plot-limit-line--upr {\n    @include plotLimitLineUpper($colorLimitCyanIc);\n  }\n\n  &--cyan.c-plot-limit-line--lwr {\n    @include plotLimitLineLower($colorLimitCyanIc);\n  }\n\n  // Severities\n  &--severe {\n    background-size: $plotLimitDashWidthSeverity 100% !important;\n  }\n\n  &--critical {\n    background-size: $plotLimitDashWidthCritical 100% !important;\n  }\n\n  &--distress {\n    background-size: $plotLimitDashWidthDistress 100% !important;\n  }\n\n  &--warning {\n    background-size: $plotLimitDashWidthWarning 100% !important;\n  }\n\n  &--watch {\n    background-size: $plotLimitDashWidthWatch 100% !important;\n  }\n}\n\n.c-plot-limit {\n  // Holds both label and directional gradient\n  $labelCr: $basicCr;\n  display: flex;\n  position: absolute;\n  width: 100%;\n  z-index: 0;\n\n  &__label {\n    border-width: 1px 1px 0 0;\n    border-style: solid;\n    border-radius: 0 $labelCr 0 0;\n    display: flex;\n    flex: 0 0 auto;\n    align-items: center;\n    padding: 2px 4px;\n    transform: translateY(-100%);\n\n    > * + * {\n      margin-left: $interiorMarginSm;\n    }\n  }\n\n  &.--align-label-right {\n    justify-content: flex-end;\n    .c-plot-limit__label {\n      border-radius: $labelCr 0 0 0;\n      border-width: 1px 0 0 1px;\n    }\n  }\n\n  &.--align-label-below {\n    .c-plot-limit__label {\n      border-radius: 0 0 $labelCr 0;\n      border-width: 0 1px 1px 0;\n      transform: translateY(0);\n    }\n    &.--align-label-right {\n      .c-plot-limit__label {\n        border-radius: 0 0 0 $labelCr;\n        border-width: 0 0 1px 1px;\n      }\n    }\n  }\n\n  [class*='icon'] {\n    &:before {\n      display: block;\n      font-family: symbolsfont;\n      font-size: 0.9em;\n    }\n  }\n\n  &__series-color-swatch {\n    @include colorSwatch();\n    display: block;\n    flex: 0 0 auto;\n  }\n\n  &:before {\n    // Direction gradient\n    content: '';\n    display: block;\n    position: absolute;\n    left: 0;\n    right: 0;\n    height: 100%;\n    opacity: 0.2;\n  }\n\n  &--upr:before {\n    transform: translateY(-100%);\n  }\n\n  &--lwr:before {\n    transform: scaleY(-1); // This inverts the gradient direction\n  }\n\n  // Label styling\n  &--purple [class*='label'] {\n    background-color: $colorLimitPurpleBg;\n    border-color: $colorLimitPurpleIc;\n    color: $colorLimitPurpleFg;\n  }\n\n  &--red [class*='label'] {\n    background-color: $colorLimitRedBg;\n    border-color: $colorLimitRedIc;\n    color: $colorLimitRedFg;\n  }\n\n  &--orange [class*='label'] {\n    background-color: $colorLimitOrangeBg;\n    border-color: $colorLimitOrangeIc;\n    color: $colorLimitOrangeFg;\n  }\n\n  &--yellow [class*='label'] {\n    background-color: $colorLimitYellowBg;\n    border-color: $colorLimitYellowIc;\n    color: $colorLimitYellowFg;\n  }\n\n  &--cyan [class*='label'] {\n    background-color: $colorLimitCyanBg;\n    border-color: $colorLimitCyanIc;\n    color: $colorLimitCyanFg;\n  }\n\n  // Directional gradients\n  &--purple:before {\n    @include plotLimitDirectionGradient($c: $colorLimitPurpleIc);\n  }\n\n  &--red:before {\n    @include plotLimitDirectionGradient($c: $colorLimitRedIc);\n  }\n\n  &--orange:before {\n    @include plotLimitDirectionGradient($c: $colorLimitOrangeIc);\n  }\n\n  &--yellow:before {\n    @include plotLimitDirectionGradient($c: $colorLimitYellowIc);\n  }\n\n  &--cyan:before {\n    @include plotLimitDirectionGradient($c: $colorLimitCyanIc);\n  }\n}\n\n// Severity icons\n.c-plot-limit__label .c-plot-limit__severity-icon:before {\n  .c-plot-limit--severe & {\n    content: $glyph-icon-alert-triangle;\n  }\n\n  .c-plot-limit--critical & {\n    content: $glyph-icon-alert-rect;\n  }\n\n  .c-plot-limit--distress & {\n    content: $glyph-icon-bell;\n  }\n\n  .c-plot-limit--warning & {\n    content: $glyph-icon-asterisk;\n  }\n\n  .c-plot-limit--watch & {\n    content: $glyph-icon-eye-open;\n  }\n}\n\n// Direction icons\n.c-plot-limit__label .c-plot-limit__direction-icon:before {\n  .c-plot-limit--upr & {\n    content: $glyph-icon-arrow-up;\n  }\n\n  .c-plot-limit--lwr & {\n    content: $glyph-icon-arrow-down;\n  }\n}\n"
  },
  {
    "path": "src/styles/_mixins.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n@use 'sass:math';\n\n/************************** GLYPHS */\n@mixin glyphBefore($unicode, $family: 'symbolsfont') {\n  &:before {\n    content: $unicode;\n    font-family: $family;\n  }\n}\n\n@mixin glyphAfter($unicode, $family: 'symbolsfont') {\n  &:after {\n    content: $unicode;\n    font-family: $family;\n  }\n}\n\n@mixin glyphBg($glyphUrl) {\n  background-image: $glyphUrl;\n  background-position: center;\n  background-size: contain;\n  background-repeat: no-repeat;\n}\n\n[class*='icon-'] {\n  &:before {\n    -webkit-font-smoothing: antialiased;\n  }\n}\n\n/************************** EFFECTS */\n@mixin flash(\n  $animName: flash,\n  $dur: 500ms,\n  $dir: alternate,\n  $iter: 20,\n  $prop: background,\n  $valStart: rgba($colorOk, 1),\n  $valEnd: rgba($colorOk, 0)\n) {\n  @keyframes #{$animName} {\n    0% {\n      #{$prop}: $valStart;\n    }\n    100% {\n      #{$prop}: $valEnd;\n    }\n  }\n  animation-name: $animName;\n  animation-duration: $dur;\n  animation-direction: $dir;\n  animation-iteration-count: $iter;\n  animation-timing-function: ease-out;\n}\n\n@mixin mixedBg() {\n  $c1: nth($mixedSettingBg, 1);\n  $c2: nth($mixedSettingBg, 2);\n  $mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;\n  @include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);\n}\n\n@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {\n  @keyframes #{$animName} {\n    0% {\n      opacity: $opacity0;\n    }\n    100% {\n      opacity: $opacity100;\n    }\n  }\n  animation-name: $animName;\n  animation-duration: $dur;\n  animation-direction: alternate;\n  animation-iteration-count: $iteration;\n  animation-timing-function: ease-in-out;\n}\n\n@mixin pulseProp(\n  $animName: pulseProp,\n  $dur: 500ms,\n  $iter: 5,\n  $prop: opacity,\n  $valStart: 0,\n  $valEnd: 1\n) {\n  @keyframes #{$animName} {\n    0% {\n      #{$prop}: $valStart;\n    }\n    100% {\n      #{$prop}: $valEnd;\n    }\n  }\n  animation-name: $animName;\n  animation-duration: $dur;\n  animation-direction: alternate;\n  animation-iteration-count: $iter;\n  animation-timing-function: ease-in-out;\n}\n\n@mixin transition($prop: all, $dur: $transInTime, $timing: ease-in-out, $delay: 0ms) {\n  transition-property: $prop;\n  transition-duration: $dur;\n  transition-timing-function: $timing;\n  transition-delay: $delay;\n}\n\n/************************** VISUALS */\n@mixin ancillaryIcon($d, $c) {\n  // Used for small icons used in combination with larger icons,\n  // like the link and alert icons in tree items.\n  color: $c;\n  font-size: $d;\n  line-height: $d;\n  height: $d;\n  width: $d;\n}\n\n@mixin appearanceNone() {\n  -moz-appearance: none;\n  -webkit-appearance: none;\n  appearance: none;\n\n  &:focus {\n    outline: none;\n  }\n}\n\n@mixin isAlias() {\n  &:after {\n    color: $colorIconAlias;\n    content: $glyph-icon-link;\n    display: block;\n    font-family: symbolsfont;\n    position: absolute;\n    text-shadow: rgba(black, 0.5) 0 1px 4px;\n    top: auto;\n    left: 0;\n    bottom: 10%;\n    right: auto;\n    transform-origin: left bottom;\n    transform: scale(0.5);\n  }\n}\n\n@mixin isStatus($absPos: false, $glyph: '', $color: $colorBodyFg) {\n  // Supports CSS classing as follows:\n  // is-status--missing, is-status--suspect, etc.\n  // Common styles to be applied to tree items, object labels, grid and list item views\n\n  .is-status__indicator {\n    display: block; // Set to display: none in status.scss\n    text-shadow: $colorBodyBg 0 0 2px;\n    font-family: symbolsfont;\n\n    @if $absPos {\n      position: absolute;\n      z-index: 3;\n    }\n\n    &:before {\n      color: $color;\n      content: $glyph;\n    }\n  }\n}\n\n@mixin isStaleGlyph() {\n  content: $glyph-icon-stale;\n  display: block;\n  font-family: symbolsfont;\n  font-style: normal;\n  pointer-events: none;\n}\n\n@mixin isStaleHolder() {\n  // Applied to objects that frame content, like Display Layout frames, plots, etc.\n  border-radius: 3px;\n  border: 2px solid rgba($colorTelemStale, 0.8) !important;\n\n  &:before {\n    @include isStaleGlyph();\n    color: $colorTelemStale;\n    position: absolute;\n    bottom: 5px;\n    left: 3px;\n    width: 12px;\n    z-index: 10;\n  }\n}\n\n@mixin isStaleElement() {\n  // Applied directly to values, like LAD Table cells, alphanumerics, plot legend items\n  background: $colorTelemStale !important;\n  color: $colorTelemStaleFg !important;\n  font-style: italic;\n}\n\n@mixin isLimit() {\n  &[class*='is-limit'] {\n    &:before {\n      display: inline-block;\n      font-family: symbolsfont;\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &.is-limit--lwr:before {\n    content: $glyph-icon-arrow-down;\n  }\n  &.is-limit--upr:before {\n    content: $glyph-icon-arrow-up;\n  }\n\n  &.is-limit--purple {\n    background: $colorLimitPurpleBg !important;\n    color: $colorLimitPurpleFg !important;\n  }\n  &.is-limit--red {\n    background: $colorLimitRedBg !important;\n    color: $colorLimitRedFg !important;\n  }\n  &.is-limit--orange {\n    background: $colorLimitOrangeBg !important;\n    color: $colorLimitOrangeFg !important;\n  }\n  &.is-limit--yellow {\n    background: $colorLimitYellowBg !important;\n    color: $colorLimitYellowFg !important;\n  }\n  &.is-limit--cyan {\n    background: $colorLimitCyanBg !important;\n    color: $colorLimitCyanFg !important;\n  }\n}\n\n@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {\n  background-image: linear-gradient(\n      -45deg,\n      rgba($c, $a) 25%,\n      transparent 25%,\n      transparent 50%,\n      rgba($c, $a) 50%,\n      rgba($c, $a) 75%,\n      transparent 75%,\n      transparent 100%\n    )\n    repeat;\n  background-size: $d $d;\n}\n\n@mixin bgStripes($c: yellow, $a: 0.1, $bgsize: 5px, $angle: 90deg) {\n  background: linear-gradient(\n      $angle,\n      rgba($c, $a) 25%,\n      transparent 25%,\n      transparent 50%,\n      rgba($c, $a) 50%,\n      rgba($c, $a) 75%,\n      transparent 75%,\n      transparent 100%\n    )\n    repeat;\n  background-size: $bgsize $bgsize;\n}\n\n@mixin bgStripes2Color($c1, $c2, $bgSize, $angle: -45deg) {\n  background: linear-gradient(\n      -45deg,\n      $c1 0%,\n      $c1 25%,\n      $c2 25%,\n      $c2 50%,\n      $c1 50%,\n      $c1 75%,\n      $c2 75%,\n      $c2 100%\n    )\n    repeat;\n  background-size: $bgSize;\n}\n\n@mixin bgCheckerboard($c: $colorBodyFg, $opacity: 0.3, $size: 32px, $imp: false) {\n  $color: rgba($c, $opacity);\n  $bgPos: floor(math.div($size, 2));\n\n  $impStr: null;\n  @if $imp {\n    $impStr: !important;\n  }\n\n  background-image:\n    linear-gradient(45deg, $color 25%, transparent 25%),\n    linear-gradient(45deg, transparent 75%, $color 75%),\n    linear-gradient(45deg, transparent 75%, $color 75%),\n    linear-gradient(45deg, $color 25%, transparent 25%) $impStr;\n  background-size: $size $size;\n  background-position:\n    0 0,\n    0 0,\n    -1 * $bgPos -1 * $bgPos,\n    $bgPos $bgPos;\n}\n\n@mixin disabled() {\n  opacity: $controlDisabledOpacity;\n  pointer-events: none !important;\n  cursor: default !important;\n}\n\n@mixin grippy($c: rgba(black, 0.5), $dir: 'x') {\n  $deg: 90deg;\n  $bgSize: 3px 100%;\n\n  @if $dir != 'x' {\n    // Grippy texture runs 'vertically'\n    $deg: 0deg;\n    $bgSize: 100% 3px;\n  }\n\n  background: linear-gradient($deg, $c 1px, transparent 1px, transparent 100%) repeat;\n  background-size: $bgSize;\n}\n\n@mixin colorSwatch() {\n  border-radius: 30%;\n  border: 1px solid $colorBodyBg;\n  height: $plotSwatchD;\n  width: $plotSwatchD;\n}\n\n@mixin dropDownArrowBg() {\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat, no-repeat;\n  background-position:\n    right 0.4em top 80%,\n    0 0;\n}\n\n@mixin noColor() {\n  // A \"no fill/stroke\" selection option. Used in palettes.\n  $c: red;\n  $s: 48%;\n  $e: 52%;\n  background-image: linear-gradient(-45deg, transparent $s - 5%, $c $s, $c $e, transparent $e + 5%);\n  background-repeat: no-repeat;\n  background-size: contain;\n}\n\n@mixin bgTicks($c: $colorBodyFg, $repeatDir: 'x') {\n  $deg: 90deg;\n  @if ($repeatDir != 'x') {\n    $deg: 0deg;\n    $repeatDir: repeat-y;\n  } @else {\n    $repeatDir: repeat-x;\n  }\n\n  background-image: linear-gradient($deg, $c 1px, transparent 1px, transparent 100%);\n  background-repeat: $repeatDir;\n}\n\n@mixin sliderTrack($bg: $scrollbarTrackColorBg) {\n  border-radius: 2px;\n  box-sizing: border-box;\n  background-color: $bg;\n}\n\n@mixin triangle($dir: 'left', $size: 5px, $ratio: 1, $color: red) {\n  width: 0;\n  height: 0;\n  $slopedB: math.div($size, $ratio) solid transparent;\n  $straightB: $size solid $color;\n  @if $dir == 'up' {\n    border-left: $slopedB;\n    border-right: $slopedB;\n    border-bottom: $straightB;\n  } @else if $dir == 'right' {\n    border-top: $slopedB;\n    border-bottom: $slopedB;\n    border-left: $straightB;\n  } @else if $dir == 'down' {\n    border-left: $slopedB;\n    border-right: $slopedB;\n    border-top: $straightB;\n  } @else {\n    border-top: $slopedB;\n    border-bottom: $slopedB;\n    border-right: $straightB;\n  }\n}\n\n@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) {\n  background-image: linear-gradient(\n    -90deg,\n    rgba($c, $a) 0%,\n    rgba($c, $a) 50%,\n    transparent 50%,\n    transparent 100%\n  );\n  background-repeat: repeat;\n  background-size: $d $d;\n}\n\n/************************** LAYOUT */\n@mixin abs($m: 0) {\n  position: absolute;\n  top: $m;\n  right: $m;\n  bottom: $m;\n  left: $m;\n}\n\n@mixin absCenter() {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  transform-origin: center center;\n}\n\n@mixin propertiesHeader() {\n  border-radius: $smallCr;\n  background-color: $colorInspectorSectionHeaderBg;\n  color: $colorInspectorSectionHeaderFg;\n  font-weight: normal;\n  padding: $interiorMarginSm $interiorMargin;\n  text-transform: uppercase;\n}\n\n@mixin modalFullScreen() {\n  // Optional modifier that makes a c-menu more mobile-friendly\n  position: fixed;\n  border-radius: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n}\n\n/************************** TEXT */\n@mixin ellipsize() {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n@mixin fadeTruncate($color: $colorBodyBg, $angle: 90deg) {\n  background-image: linear-gradient($angle, transparent 0%, $color 100%);\n}\n\n@mixin reverseEllipsis() {\n  @include ellipsize();\n  direction: ltr;\n  unicode-bidi: bidi-override;\n}\n\n/************************** CONTROLS, BUTTONS, INPUTS */\n@mixin hover {\n  body.desktop & {\n    &:hover {\n      @content;\n    }\n  }\n}\n\n@mixin htmlInputReset() {\n  @include appearanceNone();\n  background: transparent;\n  border: none;\n  border-radius: 0;\n  outline: none;\n  text-align: inherit;\n\n  &:focus {\n    outline: 0;\n  }\n}\n\n@mixin input-base() {\n  @include htmlInputReset();\n  border-radius: $controlCr;\n\n  &.error {\n    background: $colorFormFieldErrorBg;\n    color: $colorFormFieldErrorFg;\n  }\n}\n\n@mixin nice-input(\n  $bg: $colorInputBg,\n  $fg: $colorInputFg,\n  $shdw: inset rgba(black, 0.5) 0 0 2px 1px\n) {\n  @include input-base();\n  background: $bg;\n  color: $fg;\n  box-shadow: $shdw;\n}\n\n@mixin reactive-input($bg: $colorInputBg, $fg: $colorInputFg) {\n  @include input-base();\n  background: $bg;\n  box-shadow: $shdwInput;\n  color: $fg;\n\n  &:focus {\n    box-shadow: $shdwInputFoc;\n  }\n\n  &::selection {\n    background: rgba($colorKeySelectedBg, 0.5);\n  }\n}\n\n@mixin inlineInput() {\n  @include reactive-input($bg: transparent);\n  box-shadow: none;\n  display: block !important;\n  min-width: 0;\n  padding-left: 0;\n  padding-right: 0;\n  overflow: hidden;\n  transition: all 250ms ease;\n  white-space: nowrap;\n\n  &:not(:focus) {\n    text-overflow: ellipsis;\n  }\n}\n\n@mixin button($bg: $colorBtnBg, $fg: $colorBtnFg, $radius: $controlCr, $shdw: none) {\n  background: $bg;\n  color: $fg;\n  border-radius: $radius;\n  box-shadow: $shdw;\n}\n\n@mixin cControl() {\n  $fs: 1em;\n  @include userSelectNone();\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  line-height: $fs; // Remove effect on top and bottom padding\n  overflow: hidden;\n\n  &:before,\n  &:after {\n    font-family: symbolsfont;\n    display: block;\n    flex: 0 0 auto;\n  }\n\n  &:before {\n    font-size: 0.9em;\n  }\n\n  &:after {\n    font-size: 0.8em;\n  }\n\n  [class*='__label'] {\n    @include ellipsize();\n    display: block;\n    font-size: $fs;\n  }\n\n  &[class*='icon'] > [class*='__label'] {\n    // When button holds both an icon and a label, provide margin between them.\n    margin-left: $interiorMarginSm;\n  }\n}\n\n@mixin cControlHov($styleConst: $shdwBtnHov) {\n  transition: box-shadow $transOutTime;\n\n  @include hover() {\n    transition: box-shadow $transInTime;\n    box-shadow: $styleConst !important;\n  }\n}\n\n@function cButtonPadding($padding: $interiorMargin, $compact: false) {\n  @if $compact {\n    @return floor(math.div($padding, 1.5)) $padding;\n  } @else {\n    @return $padding floor($padding * 1.25);\n  }\n}\n\n@mixin cButtonLayout() {\n  $pad: $interiorMargin;\n  padding: cButtonPadding($pad);\n\n  &:after,\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n\n  &[class*='--compact'] {\n    //padding: floor(math.div($pad, 1.5)) $pad;\n    padding: cButtonPadding($pad, true);\n  }\n}\n\n@mixin cButton() {\n  @include cControl();\n  @include cControlHov();\n  @include themedButton();\n  @include cButtonLayout();\n  border-radius: $controlCr;\n  color: $colorBtnFg;\n  cursor: pointer;\n\n  &[class*='--major'],\n  &[class*='is-active'] {\n    background: $colorBtnMajorBg !important;\n    color: $colorBtnMajorFg !important;\n  }\n\n  &[class*='--caution'] {\n    background: $colorBtnCautionBg !important;\n    color: $colorBtnCautionFg !important;\n  }\n}\n\n@mixin cClickIcon() {\n  @include cControl();\n  @include cControlHov();\n  color: $colorBodyFg;\n  cursor: pointer;\n  padding: 4px; // Bigger hit area\n  opacity: 0.7;\n  transform-origin: center;\n\n  &[class*='--major'] {\n    color: $colorBtnMajorBg !important;\n    opacity: 0.8;\n  }\n\n  @include hover() {\n    transform: scale(1.1);\n    opacity: 1;\n  }\n}\n\n@mixin cClickIconButtonLayout() {\n  $pLR: 5px;\n  $pTB: 5px;\n  padding: $pTB $pLR;\n\n  &:before,\n  *:before {\n    // *:before handles any nested containers that may contain glyph elements\n    // Needed for c-togglebutton.\n    font-size: 1.15em;\n  }\n}\n\n@mixin cClickIconButton() {\n  // A clickable element that just includes the icon\n  // Background is displayed on hover\n  // Padding is included to facilitate a bigger hit area\n  // Make the icon bigger relative to its container\n  @include cControl();\n  @include cControlHov();\n  @include cClickIconButtonLayout();\n  background: none;\n  color: $colorClickIconButton;\n  box-shadow: none;\n  cursor: pointer;\n  border-radius: $controlCr;\n\n  &[class*='--major'] {\n    color: $colorBtnMajorBg !important;\n  }\n}\n\n@mixin cCtrlWrapper {\n  // Provides a wrapper around  buttons and other controls\n  // Contains control and provides positioning context for contained menu/palette.\n  // Wraps --menu elements, contains button and menu\n  overflow: visible;\n\n  .c-menu {\n    // Default position of contained menu\n    top: 100%;\n    left: 0;\n  }\n\n  &[class*='--menus-up'] {\n    .c-menu {\n      top: auto;\n      bottom: 100%;\n    }\n  }\n\n  &[class*='--menus-bottom'] {\n    .c-menu {\n      top: auto;\n      bottom: 100%;\n    }\n  }\n\n  &[class*='--menus-down'] {\n    .c-menu {\n      top: auto;\n      bottom: 100%;\n    }\n  }\n\n  &[class*='--menus-right'] {\n    .c-menu {\n      left: 0;\n      right: auto;\n    }\n  }\n\n  &[class*='--menus-left'],\n  &[class*='menus-to-left'] {\n    .c-menu {\n      left: auto;\n      right: 0;\n    }\n  }\n}\n\n@mixin cArrowButtonBase($colorBg: transparent, $colorFg: $colorBtnFg, $filterHov: $filterHov) {\n  // Copied from branch new-tree-refactor\n\n  background: $colorBg;\n\n  &:before {\n    // Arrow shape\n    border-style: solid;\n    content: '';\n    display: block;\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    transform-origin: center;\n  }\n\n  &:before {\n    border-color: $colorFg;\n  }\n\n  &--up,\n  &--prev {\n    &:before {\n      transform: translate(-30%, -50%) rotate(135deg);\n    }\n  }\n\n  &--down,\n  &--next {\n    &:before {\n      transform: translate(-70%, -50%) rotate(-45deg);\n    }\n  }\n}\n\n@mixin cArrowButtonSizing($dimOuter: 48px) {\n  height: $dimOuter;\n  width: $dimOuter;\n  $dimInner: floor($dimOuter * 0.6);\n  $borderW: floor($dimInner * 0.3);\n  $backOffsetW: floor($dimInner * 0.4);\n\n  &:before {\n    height: $dimInner;\n    width: $dimInner;\n  }\n\n  &:before {\n    // Arrow shape\n    border-width: 0 $borderW $borderW 0;\n  }\n}\n\n@mixin cArrowButton() {\n  @include cArrowButtonBase();\n  @include cArrowButtonSizing();\n}\n\n@mixin hasMenu() {\n  &:after {\n    content: $glyph-icon-arrow-down;\n    font-family: symbolsfont;\n    font-size: 0.7em;\n    margin-left: floor($interiorMarginSm * 0.8);\n    opacity: 0.5;\n  }\n}\n\n@mixin cSelect($bg, $fg, $arwClr, $shdw) {\n  $svgArwClr: str-slice(\n    inspect($arwClr),\n    2,\n    str-length(inspect($arwClr))\n  ); // Remove initial # in color value\n  background: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{$svgArwClr}' d='M5 5l5-5H0z'/%3e%3c/svg%3e\"),\n    $bg;\n  color: $fg;\n  box-shadow: $shdw;\n}\n\n@mixin smallerControlButtons() {\n  .c-click-icon,\n  .c-button,\n  .c-icon-button {\n    // Shrink buttons a bit when they appear in containers\n    font-size: 0.9em;\n    padding: 4px;\n  }\n}\n\n@mixin wrappedInput() {\n  // An input that is wrapped. Optionally includes a __label or icon element.\n  // Based on .c-search.\n  @include nice-input($shdw: $shdwInput);\n  display: flex;\n  align-items: center;\n  padding-left: 4px;\n  padding-right: 4px;\n\n  &:before,\n  [class*='__label'] {\n    opacity: 0.5;\n  }\n\n  &:before {\n    // Adds an icon. Content defined in class.\n    direction: rtl; // Aligns glyph to right-hand side of container, for transition\n    display: block;\n    font-family: symbolsfont;\n    flex: 0 0 auto;\n    overflow: hidden;\n    padding: 2px 0; // Prevents clipping\n    transition: width 250ms ease;\n    width: 1em;\n  }\n\n  &:hover {\n    box-shadow: inset rgba(black, 0.8) 0 0px 2px;\n    &:before {\n      opacity: 0.9;\n    }\n  }\n\n  &--major {\n    padding: 4px;\n  }\n\n  &__input,\n  input[type='text'],\n  input[type='search'],\n  input[type='number'] {\n    background: none !important;\n    box-shadow: none !important; // !important needed to override default for [input]\n    flex: 1 1 auto;\n    padding-left: 2px !important;\n    padding-right: 2px !important;\n    min-width: 10px; // Must be set to allow input to collapse below browser min\n  }\n\n  &.is-active {\n    &:before {\n      padding: 2px 0px;\n      width: 0px;\n    }\n  }\n}\n\n/************************** MATH */\n@function percentToDecimal($p) {\n  @return $p / 100%;\n}\n\n@function decimalToPercent($d) {\n  @return percentage($d);\n}\n\n/************************** UTILITIES */\n@mixin browserPrefix($prop, $val) {\n  #{$prop}: $val;\n  -ms-#{$prop}: $val;\n  -moz-#{$prop}: $val;\n  -webkit-#{$prop}: $val;\n}\n\n@mixin userSelectNone() {\n  @include browserPrefix(user-select, none);\n}\n\n@mixin cursorGrab() {\n  cursor: grab;\n  cursor: -webkit-grab;\n  &:active {\n    cursor: grabbing;\n    cursor: -webkit-grabbing;\n  }\n}\n\n@function svgColorFromHex($hexColor) {\n  // Remove initial # in color value\n  @return str-slice(inspect($hexColor), 2, str-length(inspect($hexColor)));\n}\n\n@mixin test($c: deeppink, $a: 0.3) {\n  background: rgba($c, $a) !important;\n  background-color: rgba($c, $a) !important;\n}\n\n@mixin sUnsynced {\n  $c: $colorPausedBg;\n  border: 1px solid $c;\n}\n\n// @mixin telemetryView(){\n//   border: 1px solid $colorBodyFg;\n//   border-radius: $controlCr;\n// }\n"
  },
  {
    "path": "src/styles/_status.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/*************************************************** MIXINS */\n@mixin statusStyle($bg, $fg, $imp: false) {\n  $impStr: null;\n  @if $imp {\n    $impStr: !important;\n  }\n  background: $bg $impStr;\n  background-color: $bg $impStr;\n  color: $fg $impStr;\n}\n\n@mixin statusIcon($ic, $glyph: null, $imp: false) {\n  $impStr: null;\n  @if $imp {\n    $impStr: !important;\n  }\n  &:before {\n    color: $ic;\n    display: inline-block;\n    font-family: symbolsfont;\n    font-size: 0.8em;\n    margin-right: $interiorMarginSm;\n    @if $glyph != null {\n      content: $glyph $impStr;\n    }\n  }\n}\n\n@mixin statusStyleCombined($bg, $fg, $ic) {\n  @include statusStyle($bg, $fg, $imp: true);\n  @include statusIcon($ic);\n}\n\n@mixin elementStatusColors($c) {\n  // Sets bg and icon colors for elements\n  &:before {\n    color: $c !important;\n  }\n}\n\n@mixin indicatorStatusColors($c) {\n  &:before,\n  .c-indicator__count {\n    color: $c;\n  }\n}\n\n@mixin uIndicator($bg, $fg, $glyph) {\n  background: $bg;\n  color: $fg;\n\n  &[class*='--with-icon'] {\n    &:before {\n      color: $fg;\n      display: inline-block;\n      font-family: symbolsfont;\n      margin-right: $interiorMarginSm;\n      @if $glyph != null {\n        content: $glyph;\n      }\n    }\n  }\n\n  &[class*='--block'] {\n    border-radius: $controlCr;\n    display: inline-block;\n    padding: 2px $interiorMargin;\n  }\n}\n\n/*************************************************** STYLES */\ntr {\n  &.is-limit--yellow {\n    @include statusStyle($colorLimitYellowBg, $colorLimitYellowFg);\n    td:first-child {\n      @include statusIcon($colorLimitYellowIc, $glyph-icon-alert-rect);\n    }\n    td {\n      color: $colorLimitYellowFg;\n    }\n  }\n\n  &.is-limit--red {\n    @include statusStyle($colorLimitRedBg, $colorLimitRedFg);\n    td:first-child {\n      @include statusIcon($colorLimitRedIc, $glyph-icon-alert-triangle);\n    }\n    td {\n      color: $colorLimitRedFg;\n    }\n  }\n\n  &.is-limit--upr {\n    td:first-child:before {\n      content: $glyph-icon-arrow-up !important;\n    }\n  }\n  &.is-limit--lwr {\n    td:first-child:before {\n      content: $glyph-icon-arrow-down !important;\n    }\n  }\n}\n\n/*************************************************** STATUS */\n[class*='s-status-icon'] {\n  &:before {\n    font-family: symbolsfont;\n    margin-right: $interiorMargin;\n  }\n}\n\n.s-status-warning-hi,\n.s-status-icon-warning-hi {\n  @include elementStatusColors($colorWarningHi);\n}\n.s-status-warning-lo,\n.s-status-icon-warning-lo {\n  @include elementStatusColors($colorWarningLo);\n}\n.s-status-diagnostic,\n.s-status-icon-diagnostic {\n  @include elementStatusColors($colorDiagnostic);\n}\n.s-status-info,\n.s-status-icon-info {\n  @include elementStatusColors($colorInfo);\n}\n.s-status-ok,\n.s-status-icon-ok {\n  @include elementStatusColors($colorOk);\n}\n\n.s-status-icon-warning-hi:before {\n  content: $glyph-icon-alert-triangle;\n}\n.s-status-icon-warning-lo:before {\n  content: $glyph-icon-alert-rect;\n}\n.s-status-icon-diagnostic:before {\n  content: $glyph-icon-eye-open;\n}\n.s-status-icon-info:before {\n  content: $glyph-icon-info;\n}\n.s-status-icon-ok:before {\n  content: $glyph-icon-check;\n}\n\n/*************************************************** INDICATOR COLORING */\n.c-indicator {\n  &.s-status-info {\n    @include indicatorStatusColors($colorInfo);\n  }\n\n  &.s-status-disabled {\n    @include indicatorStatusColors($colorIndicatorDisabled);\n  }\n\n  &.s-status-available {\n    @include indicatorStatusColors($colorIndicatorAvailable);\n  }\n\n  &.s-status-on,\n  &.s-status-enabled {\n    @include indicatorStatusColors($colorIndicatorOn);\n  }\n\n  &.s-status-off {\n    @include indicatorStatusColors($colorIndicatorOff);\n  }\n\n  &.s-status-caution,\n  &.s-status-warning,\n  &.s-status-alert {\n    @include indicatorStatusColors($colorStatusAlert);\n  }\n\n  &.s-status-error {\n    @include indicatorStatusColors($colorStatusError);\n  }\n}\n\n.s-status {\n  &--partial {\n    // Partially completed things, such as a file downloading or process that's running\n    background-color: $colorStatusPartialBg;\n  }\n\n  &--complete {\n    // Completed things, such as a file downloaded or process that's finished\n    background-color: $colorStatusCompleteBg;\n  }\n}\n\n.u-alert {\n  @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle);\n}\n.u-error {\n  @include uIndicator($colorError, $colorErrorFg, $glyph-icon-alert-triangle);\n}\n\n.is-status {\n  &__indicator {\n    display: none; // Default state; is set to block when within an actual is-status class\n  }\n\n  &--missing {\n    @include isStatus($glyph: $glyph-icon-alert-triangle, $color: $colorAlert);\n  }\n\n  &--suspect {\n    @include isStatus($glyph: $glyph-icon-alert-rect, $color: $colorWarningLo);\n  }\n}\n\n.is-event {\n  &--purple {\n    background-color: $colorEventPurpleBg !important;\n    color: $colorEventPurpleFg !important;\n  }\n  &--red {\n    background-color: $colorEventRedBg !important;\n    color: $colorEventRedFg !important;\n  }\n  &--orange {\n    background-color: $colorEventOrangeBg !important;\n    color: $colorEventOrangeFg !important;\n  }\n  &--yellow {\n    background-color: $colorEventYellowBg !important;\n    color: $colorEventYellowFg !important;\n  }\n}\n\n\n"
  },
  {
    "path": "src/styles/_table.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/******************************************************** TABLE */\ntable {\n  $minW: 50px;\n  width: 100%;\n\n  thead {\n    th {\n      background: $colorTabHeaderBg;\n\n      + th {\n        border-left: 1px solid $colorTabHeaderBorder;\n      }\n    }\n  }\n\n  tbody {\n    tr + tr {\n      border-top: 1px solid $colorTabBorder;\n    }\n  }\n\n  th,\n  td {\n    white-space: nowrap;\n    min-width: $minW;\n    padding: $tabularTdPadTB $tabularTdPadLR;\n  }\n\n  td {\n    vertical-align: top;\n  }\n}\n\n.is-editing {\n  td.is-selectable {\n    &:hover {\n      background: rgba($editUIColorBg, 0.1);\n      box-shadow: inset rgba($editUIColorBg, 0.8) 0 0 0 1px;\n    }\n\n    &[s-selected] {\n      background: $editUIColorBg !important;\n      border: 1px solid $editUIColorFg !important;\n      color: $editUIColorFg !important;\n      box-shadow: $editFrameSelectedShdw;\n      z-index: 2;\n    }\n  }\n}\n\n/******************************************************** C-TABLE */\ndiv.c-table {\n  // When c-table is used as a wrapper element in more complex table views\n  height: 100%;\n}\n\n.c-table-wrapper {\n  // Wraps .c-control-bar and .c-table\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n\n  // Using absolute here because sizing table can't calc width properly if padding is used\n  $p: $mainViewPad;\n  position: absolute;\n  top: $p;\n  right: $p;\n  bottom: $p;\n  left: $p;\n\n  > .c-table {\n    height: auto;\n    flex: 1 1 auto;\n  }\n\n  &.is-stale {\n    @include isStaleHolder();\n  }\n\n  .--width-less-than-600 & {\n    &:not(.is-paused) {\n      .c-table-control-bar {\n        display: none;\n      }\n    }\n    .c-table-control-bar {\n      .c-icon-button,\n      .c-click-icon,\n      .c-button {\n        &__label {\n          display: none;\n        }\n      }\n    }\n  }\n}\n\n.c-table-control-bar {\n  display: flex;\n  flex: 0 0 auto;\n  //margin-bottom: $interiorMarginSm; // This approach to allow margin to go away when control bar is hidden\n  padding: $interiorMarginSm 0;\n\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n}\n\n.c-table {\n  // Can be used by any type of table, scrolling, LAD, etc.\n  $min-w: 50px;\n\n  width: 100%;\n\n  &__headers-w {\n    flex: 0 0 auto;\n  }\n\n  /******************************* ELEMENTS */\n  thead tr,\n  &.c-table__headers {\n    background: $colorTabHeaderBg;\n\n    th {\n      &:not(:first-child) {\n        border-left: 1px solid $colorTabHeaderBorder;\n      }\n    }\n  }\n\n  tbody,\n  &__body {\n    tr:not(.c-table__group-header) + tr:not(.c-table__group-header) {\n      border-top: 1px solid $colorTabBorder;\n    }\n    transition: $transOut;\n  }\n\n  &__selectable-row {\n    cursor: pointer;\n    &:hover {\n      background: $colorListItemBgHov;\n      filter: $filterHov;\n      transition: $transIn;\n    }\n  }\n\n  &__group-header {\n    // tr element found in LAD Table Sets\n    border-top: 1px solid $colorTabHeaderBorder;\n    background: $colorTabGroupHeaderBg;\n    td {\n      color: $colorTabGroupHeaderFg;\n    }\n  }\n\n  &--sortable {\n    .is-sorting {\n      &:after {\n        color: $colorIconAlias;\n        content: $glyph-icon-arrow-tall-up;\n        font-family: symbolsfont;\n        font-size: 8px;\n        display: inline-block;\n        margin-left: $interiorMarginSm;\n      }\n      &.desc:after {\n        content: $glyph-icon-arrow-tall-down;\n      }\n    }\n    .is-sortable {\n      cursor: pointer;\n    }\n  }\n}\n\n.c-lad-table-wrapper {\n  width: 100%;\n  height: 100%;\n  padding: $mainViewPad;\n\n  &.is-stale {\n    @include isStaleHolder();\n  }\n}\n\n.c-lad-table {\n  &.fixed-layout {\n    table-layout: fixed;\n    td {\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n  }\n  th,\n  td {\n    width: 33%; // Needed to prevent size jumping as values dynamically update\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  tbody tr {\n    &:hover {\n      background: $colorItemTreeHoverBg;\n    }\n  }\n\n  td {\n    user-select: none; // Table supports context-click to display Actions menu, don't allow text selection.\n\n    &.is-stale {\n      @include isStaleElement();\n    }\n  }\n}\n\n/************************************** TABLE AND SUMMARY VIEWS */\n// Displays summary values above a table.\n\n.c-table-and-summary {\n  height: 100%;\n  width: 100%;\n  overflow: auto;\n  display: flex;\n  flex-direction: column;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__summary {\n    display: flex;\n    justify-items: stretch;\n\n    > * + * {\n      margin-left: 1px;\n    }\n  }\n\n  &__summary-item {\n    background: $colorSummaryBg;\n    color: $colorSummaryFg;\n    flex: 1 1 auto;\n    padding: $interiorMargin $interiorMarginLg;\n\n    em {\n      font-weight: bold;\n      color: $colorSummaryFgEm;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/fonts/Open MCT Symbols 12px.json",
    "content": "{\n  \"metadata\": {\n    \"name\": \"Open MCT Symbols 12px\",\n    \"lastOpened\": 0,\n    \"created\": 1561483556329\n  },\n  \"iconSets\": [\n    {\n      \"selection\": [\n        {\n          \"order\": 12,\n          \"id\": 10,\n          \"name\": \"icon12-filter\",\n          \"prevSize\": 12,\n          \"code\": 59686,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 14,\n          \"id\": 11,\n          \"name\": \"icon12-filter-outline\",\n          \"prevSize\": 12,\n          \"code\": 59687,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 9,\n          \"id\": 6,\n          \"name\": \"icon12-crosshair\",\n          \"prevSize\": 12,\n          \"code\": 59696,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 11,\n          \"id\": 8,\n          \"name\": \"icon12-grippy\",\n          \"prevSize\": 12,\n          \"code\": 59697,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 10,\n          \"id\": 7,\n          \"name\": \"icon12-list-view\",\n          \"prevSize\": 12,\n          \"code\": 921666,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 6,\n          \"id\": 3,\n          \"prevSize\": 12,\n          \"code\": 921865,\n          \"name\": \"icon12-folder\",\n          \"tempChar\": \"\"\n        }\n      ],\n      \"id\": 0,\n      \"metadata\": {\n        \"name\": \"Open MCT Symbols 12px\",\n        \"importSize\": {\n          \"width\": 384,\n          \"height\": 384\n        },\n        \"designer\": \"Charles Hacskaylo\"\n      },\n      \"height\": 1024,\n      \"prevSize\": 12,\n      \"icons\": [\n        {\n          \"id\": 10,\n          \"paths\": [\n            \"M853.333 0h-682.667c-94.135 0.302-170.364 76.532-170.667 170.638l-0 0.029v682.667c0.302 94.135 76.532 170.364 170.638 170.667l0.029 0h256v-341.333l-341.333-341.333h853.333l-341.333 341.333 1.067 341.333h254.933c94.135-0.302 170.364-76.532 170.667-170.638l0-0.029v-682.667c-0.302-94.135-76.532-170.364-170.638-170.667l-0.029-0z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 0,\n          \"tags\": [\"icon12-filter\"]\n        },\n        {\n          \"id\": 11,\n          \"paths\": [\n            \"M853.333 0h-682.667c-94.135 0.302-170.364 76.532-170.667 170.638l-0 0.029v682.667c0.302 94.135 76.532 170.364 170.638 170.667l0.029 0h682.667c94.135-0.302 170.364-76.532 170.667-170.638l0-0.029v-682.667c-0.302-94.135-76.532-170.364-170.638-170.667l-0.029-0zM170.933 853.333h-0.267v-512l256 256v256zM853.067 853.333h-255.2l-0.533-256 256-256v511.733zM853.333 341.333h-682.667v-170.4h682.667z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 0,\n          \"tags\": [\"icon12-filter-outline\"]\n        },\n        {\n          \"id\": 6,\n          \"paths\": [\n            \"M597.333 0h-170.667v256h170.667v-256z\",\n            \"M1024 426.667h-256v170.667h256v-170.667z\",\n            \"M597.333 768h-170.667v256h170.667v-256z\",\n            \"M256 426.667h-256v170.667h256v-170.667z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 0,\n          \"tags\": [\"icon12-crosshair\"]\n        },\n        {\n          \"id\": 8,\n          \"paths\": [\n            \"M186.347 232.64c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M186.347 511.867c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M186.347 791.36c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M465.573 93.173c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M465.573 372.4c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M379.028 558.728c51.328 3.652 89.978 48.223 86.325 99.551s-48.223 89.978-99.551 86.325c-51.328-3.652-89.978-48.223-86.325-99.551s48.223-89.978 99.551-86.325z\",\n            \"M379.017 837.96c51.328 3.652 89.978 48.223 86.325 99.551s-48.223 89.978-99.551 86.325c-51.328-3.652-89.978-48.223-86.325-99.551s48.223-89.978 99.551-86.325z\",\n            \"M744.773 232.64c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M744.773 511.867c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\",\n            \"M744.773 791.36c0 51.458-41.715 93.173-93.173 93.173s-93.173-41.715-93.173-93.173c0-51.458 41.715-93.173 93.173-93.173s93.173 41.715 93.173 93.173z\"\n          ],\n          \"attrs\": [],\n          \"width\": 745,\n          \"grid\": 0,\n          \"tags\": [\"icon12-grippy\"]\n        },\n        {\n          \"id\": 7,\n          \"paths\": [\n            \"M0 0h1024v170.667h-1024v-170.667z\",\n            \"M0 426.667h1024v170.667h-1024v-170.667z\",\n            \"M0 853.333h1024v170.667h-1024v-170.667z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 0,\n          \"tags\": [\"icon12-list-view\"]\n        },\n        {\n          \"id\": 3,\n          \"paths\": [\n            \"M938.667 170.667h-341.333l-110.32-110.32c-33.2-33.2-98.667-60.347-145.68-60.347h-256c-47.073 0.136-85.197 38.26-85.333 85.32l-0 341.346c0.136-47.073 38.26-85.197 85.32-85.333l853.346-0c47.073 0.136 85.197 38.26 85.333 85.32l0-170.654c-0.136-47.073-38.26-85.197-85.32-85.333z\",\n            \"M85.333 426.667h853.333c47.128 0 85.333 38.205 85.333 85.333v426.667c0 47.128-38.205 85.333-85.333 85.333h-853.333c-47.128 0-85.333-38.205-85.333-85.333v-426.667c0-47.128 38.205-85.333 85.333-85.333z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 0,\n          \"tags\": [\"icon12-folder\"]\n        }\n      ],\n      \"invisible\": false,\n      \"colorThemes\": [],\n      \"colorThemeIdx\": 0\n    }\n  ],\n  \"preferences\": {\n    \"showGlyphs\": true,\n    \"showCodes\": true,\n    \"showQuickUse\": true,\n    \"showQuickUse2\": true,\n    \"showSVGs\": true,\n    \"fontPref\": {\n      \"prefix\": \"openmct-symbols-\",\n      \"metadata\": {\n        \"fontFamily\": \"Open-MCT-Symbols-12px\",\n        \"majorVersion\": 1,\n        \"minorVersion\": 0\n      },\n      \"metrics\": {\n        \"emSize\": 1024,\n        \"baseline\": 6.25,\n        \"whitespace\": 50\n      },\n      \"embed\": false,\n      \"noie8\": true,\n      \"ie7\": false,\n      \"showMetadata\": false,\n      \"includeMetadata\": false,\n      \"showMetrics\": true\n    },\n    \"imagePref\": {\n      \"prefix\": \"icon-\",\n      \"png\": true,\n      \"useClassSelector\": true,\n      \"color\": 0,\n      \"bgColor\": 16777215\n    },\n    \"historySize\": 100,\n    \"gridSize\": 16\n  },\n  \"uid\": -1\n}\n"
  },
  {
    "path": "src/styles/fonts/Open MCT Symbols 16px.json",
    "content": "{\n  \"metadata\": {\n    \"name\": \"Open MCT Symbols 16px\",\n    \"lastOpened\": 0,\n    \"created\": 1726681576505\n  },\n  \"iconSets\": [\n    {\n      \"selection\": [\n        {\n          \"order\": 77,\n          \"id\": 47,\n          \"name\": \"icon-alert-rect-v2\",\n          \"prevSize\": 16,\n          \"code\": 59648,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 76,\n          \"id\": 48,\n          \"name\": \"icon-alert-triangle-v2\",\n          \"prevSize\": 16,\n          \"code\": 59649,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 20,\n          \"id\": 112,\n          \"name\": \"icon-arrow-up\",\n          \"prevSize\": 16,\n          \"code\": 59650,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 36,\n          \"id\": 94,\n          \"name\": \"icon-arrow-double-up\",\n          \"prevSize\": 16,\n          \"code\": 59651,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 143,\n          \"id\": 96,\n          \"name\": \"icon-arrow-tall-up\",\n          \"prevSize\": 16,\n          \"code\": 59652,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 21,\n          \"id\": 111,\n          \"name\": \"icon-arrow-right\",\n          \"prevSize\": 16,\n          \"code\": 59653,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 102,\n          \"id\": 18,\n          \"name\": \"icon-arrow-right-equilateral\",\n          \"prevSize\": 16,\n          \"code\": 59654,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 19,\n          \"id\": 113,\n          \"name\": \"icon-arrow-down\",\n          \"prevSize\": 16,\n          \"code\": 59655,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 37,\n          \"id\": 93,\n          \"name\": \"icon-arrow-double-down\",\n          \"prevSize\": 16,\n          \"code\": 59656,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 35,\n          \"id\": 95,\n          \"name\": \"icon-arrow-tall-down\",\n          \"prevSize\": 16,\n          \"code\": 59657,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 18,\n          \"id\": 114,\n          \"name\": \"icon-arrow-left\",\n          \"prevSize\": 16,\n          \"code\": 59658,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 17,\n          \"id\": 115,\n          \"name\": \"icon-asterisk\",\n          \"prevSize\": 16,\n          \"code\": 59659,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 128,\n          \"id\": 59,\n          \"name\": \"icon-bell\",\n          \"prevSize\": 16,\n          \"code\": 59660,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 123,\n          \"id\": 103,\n          \"name\": \"icon-box-round-corners\",\n          \"prevSize\": 16,\n          \"code\": 59661,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 129,\n          \"id\": 58,\n          \"name\": \"icon-box-with-arrow-cursor\",\n          \"prevSize\": 16,\n          \"code\": 59662,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 142,\n          \"id\": 120,\n          \"name\": \"icon-check\",\n          \"prevSize\": 16,\n          \"code\": 59663,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 25,\n          \"id\": 107,\n          \"name\": \"icon-connectivity\",\n          \"prevSize\": 16,\n          \"code\": 59664,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 44,\n          \"id\": 84,\n          \"name\": \"icon-database-in-brackets\",\n          \"prevSize\": 16,\n          \"code\": 59665,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 75,\n          \"id\": 49,\n          \"name\": \"icon-eye-open\",\n          \"prevSize\": 16,\n          \"code\": 59666,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 5,\n          \"id\": 129,\n          \"name\": \"icon-gear\",\n          \"prevSize\": 16,\n          \"code\": 59667,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 66,\n          \"id\": 60,\n          \"name\": \"icon-hourglass\",\n          \"prevSize\": 16,\n          \"code\": 59668,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 65,\n          \"id\": 61,\n          \"name\": \"icon-info\",\n          \"prevSize\": 16,\n          \"code\": 59669,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 53,\n          \"id\": 75,\n          \"name\": \"icon-link\",\n          \"prevSize\": 16,\n          \"code\": 59670,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 42,\n          \"id\": 86,\n          \"name\": \"icon-lock\",\n          \"prevSize\": 16,\n          \"code\": 59671,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 49,\n          \"id\": 79,\n          \"name\": \"icon-minus\",\n          \"prevSize\": 16,\n          \"code\": 59672,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 22,\n          \"id\": 110,\n          \"name\": \"icon-people\",\n          \"prevSize\": 16,\n          \"code\": 59673,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 46,\n          \"id\": 82,\n          \"name\": \"icon-person\",\n          \"prevSize\": 16,\n          \"code\": 59674,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 38,\n          \"id\": 92,\n          \"name\": \"icon-plus\",\n          \"prevSize\": 16,\n          \"code\": 59675,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 164,\n          \"id\": 137,\n          \"name\": \"icon-plus-in-rect\",\n          \"prevSize\": 16,\n          \"code\": 59676,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 6,\n          \"id\": 128,\n          \"name\": \"icon-trash\",\n          \"prevSize\": 16,\n          \"code\": 59677,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 140,\n          \"id\": 122,\n          \"name\": \"icon-x-heavy\",\n          \"prevSize\": 16,\n          \"code\": 59678,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 78,\n          \"id\": 46,\n          \"name\": \"icon-brackets\",\n          \"prevSize\": 16,\n          \"code\": 59679,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 93,\n          \"id\": 27,\n          \"name\": \"icon-crosshair\",\n          \"prevSize\": 16,\n          \"code\": 59680,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 91,\n          \"id\": 31,\n          \"name\": \"icon-grippy\",\n          \"prevSize\": 16,\n          \"code\": 59681,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 121,\n          \"id\": 118,\n          \"name\": \"icon-grid\",\n          \"prevSize\": 16,\n          \"code\": 59682,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 154,\n          \"id\": 136,\n          \"name\": \"icon-grippy-ew\",\n          \"prevSize\": 16,\n          \"code\": 59683,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 152,\n          \"id\": 135,\n          \"name\": \"icon-columns\",\n          \"prevSize\": 16,\n          \"code\": 59684,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 153,\n          \"id\": 134,\n          \"name\": \"icon-rows\",\n          \"prevSize\": 16,\n          \"code\": 59685,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 162,\n          \"id\": 140,\n          \"name\": \"icon-filter\",\n          \"prevSize\": 16,\n          \"code\": 59686,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 161,\n          \"id\": 139,\n          \"name\": \"icon-filter-outline\",\n          \"prevSize\": 16,\n          \"code\": 59687,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 155,\n          \"id\": 142,\n          \"name\": \"icon-suitcase\",\n          \"prevSize\": 16,\n          \"code\": 59688,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 169,\n          \"id\": 145,\n          \"name\": \"icon-cursor-locked\",\n          \"prevSize\": 16,\n          \"code\": 59689,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 176,\n          \"id\": 150,\n          \"name\": \"icon-flag\",\n          \"prevSize\": 16,\n          \"code\": 59690,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 177,\n          \"id\": 152,\n          \"name\": \"icon-eye-disabled\",\n          \"prevSize\": 16,\n          \"code\": 59691,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 179,\n          \"id\": 153,\n          \"name\": \"icon-notebook-page\",\n          \"prevSize\": 16,\n          \"code\": 59692,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 186,\n          \"id\": 160,\n          \"name\": \"icon-unlocked\",\n          \"prevSize\": 16,\n          \"code\": 59693,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 197,\n          \"id\": 169,\n          \"name\": \"icon-circle\",\n          \"prevSize\": 16,\n          \"code\": 59694,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 201,\n          \"id\": 173,\n          \"name\": \"icon-draft\",\n          \"prevSize\": 16,\n          \"code\": 59695,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 212,\n          \"id\": 183,\n          \"name\": \"icon-circle-slash\",\n          \"prevSize\": 16,\n          \"code\": 59696,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 213,\n          \"id\": 182,\n          \"name\": \"icon-question-mark\",\n          \"prevSize\": 16,\n          \"code\": 59697,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 206,\n          \"id\": 179,\n          \"name\": \"icon-status-poll-check\",\n          \"prevSize\": 16,\n          \"code\": 59698,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 207,\n          \"id\": 178,\n          \"name\": \"icon-status-poll-caution\",\n          \"prevSize\": 16,\n          \"code\": 59699,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 210,\n          \"id\": 180,\n          \"name\": \"icon-status-poll-circle-slash\",\n          \"prevSize\": 16,\n          \"code\": 59700,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 211,\n          \"id\": 181,\n          \"name\": \"icon-status-poll-question-mark\",\n          \"prevSize\": 16,\n          \"code\": 59701,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 209,\n          \"id\": 176,\n          \"name\": \"icon-status-poll-edit\",\n          \"prevSize\": 16,\n          \"code\": 59702,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 215,\n          \"id\": 185,\n          \"name\": \"icon-stale\",\n          \"prevSize\": 16,\n          \"code\": 59703,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 27,\n          \"id\": 105,\n          \"name\": \"icon-arrows-right-left\",\n          \"prevSize\": 16,\n          \"code\": 59904,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 26,\n          \"id\": 106,\n          \"name\": \"icon-arrows-up-down\",\n          \"prevSize\": 16,\n          \"code\": 59905,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 68,\n          \"id\": 56,\n          \"name\": \"icon-bullet\",\n          \"prevSize\": 16,\n          \"code\": 59906,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 150,\n          \"id\": 133,\n          \"prevSize\": 16,\n          \"code\": 59907,\n          \"name\": \"icon-calendar\",\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 45,\n          \"id\": 83,\n          \"name\": \"icon-chain-links\",\n          \"prevSize\": 16,\n          \"code\": 59908,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 73,\n          \"id\": 51,\n          \"name\": \"icon-download\",\n          \"prevSize\": 16,\n          \"code\": 59909,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 39,\n          \"id\": 91,\n          \"name\": \"icon-duplicate\",\n          \"prevSize\": 16,\n          \"code\": 59910,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 50,\n          \"id\": 78,\n          \"name\": \"icon-folder-new\",\n          \"prevSize\": 16,\n          \"code\": 59911,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 138,\n          \"id\": 124,\n          \"name\": \"icon-fullscreen-collapse\",\n          \"prevSize\": 16,\n          \"code\": 59912,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 139,\n          \"id\": 123,\n          \"name\": \"icon-fullscreen-expand\",\n          \"prevSize\": 16,\n          \"code\": 59913,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 122,\n          \"id\": 104,\n          \"name\": \"icon-layers\",\n          \"prevSize\": 16,\n          \"code\": 59914,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 151,\n          \"id\": 102,\n          \"name\": \"icon-line-horz\",\n          \"prevSize\": 16,\n          \"code\": 59915,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 100,\n          \"id\": 20,\n          \"name\": \"icon-magnify\",\n          \"prevSize\": 16,\n          \"code\": 59916,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 99,\n          \"id\": 21,\n          \"name\": \"icon-magnify-in\",\n          \"prevSize\": 16,\n          \"code\": 59917,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 101,\n          \"id\": 19,\n          \"name\": \"icon-magnify-out-v2\",\n          \"prevSize\": 16,\n          \"code\": 59918,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 103,\n          \"id\": 17,\n          \"name\": \"icon-menu\",\n          \"prevSize\": 16,\n          \"code\": 59919,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 124,\n          \"id\": 89,\n          \"name\": \"icon-move\",\n          \"prevSize\": 16,\n          \"code\": 59920,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 7,\n          \"id\": 127,\n          \"name\": \"icon-new-window\",\n          \"prevSize\": 16,\n          \"code\": 59921,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 63,\n          \"id\": 63,\n          \"name\": \"icon-paint-bucket-v2\",\n          \"prevSize\": 16,\n          \"code\": 59922,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 15,\n          \"id\": 117,\n          \"name\": \"icon-pencil\",\n          \"prevSize\": 16,\n          \"code\": 59923,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 54,\n          \"id\": 72,\n          \"name\": \"icon-pencil-edit-in-place\",\n          \"prevSize\": 16,\n          \"code\": 59924,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 40,\n          \"id\": 90,\n          \"name\": \"icon-play\",\n          \"prevSize\": 16,\n          \"code\": 59925,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 125,\n          \"id\": 88,\n          \"name\": \"icon-pause\",\n          \"prevSize\": 16,\n          \"code\": 59926,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 119,\n          \"id\": 13,\n          \"name\": \"icon-plot-resource\",\n          \"prevSize\": 16,\n          \"code\": 59927,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 48,\n          \"id\": 80,\n          \"name\": \"icon-pointer-left\",\n          \"prevSize\": 16,\n          \"code\": 59928,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 47,\n          \"id\": 81,\n          \"name\": \"icon-pointer-right\",\n          \"prevSize\": 16,\n          \"code\": 59929,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 85,\n          \"id\": 37,\n          \"name\": \"icon-refresh\",\n          \"prevSize\": 16,\n          \"code\": 59930,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 55,\n          \"id\": 71,\n          \"name\": \"icon-save\",\n          \"prevSize\": 16,\n          \"code\": 59931,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 56,\n          \"id\": 70,\n          \"name\": \"icon-save-as\",\n          \"prevSize\": 16,\n          \"code\": 59932,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 58,\n          \"id\": 68,\n          \"name\": \"icon-sine\",\n          \"prevSize\": 16,\n          \"code\": 59933,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 113,\n          \"id\": 5,\n          \"name\": \"icon-font\",\n          \"prevSize\": 16,\n          \"code\": 59934,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 41,\n          \"id\": 87,\n          \"name\": \"icon-thumbs-strip\",\n          \"prevSize\": 16,\n          \"code\": 59935,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 146,\n          \"id\": 99,\n          \"name\": \"icon-two-parts-both\",\n          \"prevSize\": 16,\n          \"code\": 59936,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 145,\n          \"id\": 98,\n          \"name\": \"icon-two-parts-one-only\",\n          \"prevSize\": 16,\n          \"code\": 59937,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 82,\n          \"id\": 40,\n          \"name\": \"icon-resync\",\n          \"prevSize\": 16,\n          \"code\": 59938,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 86,\n          \"id\": 36,\n          \"name\": \"icon-reset\",\n          \"prevSize\": 16,\n          \"code\": 59939,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 61,\n          \"id\": 65,\n          \"name\": \"icon-x-in-circle\",\n          \"prevSize\": 16,\n          \"code\": 59940,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 84,\n          \"id\": 38,\n          \"name\": \"icon-brightness\",\n          \"prevSize\": 16,\n          \"code\": 59941,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 83,\n          \"id\": 39,\n          \"name\": \"icon-contrast\",\n          \"prevSize\": 16,\n          \"code\": 59942,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 87,\n          \"id\": 35,\n          \"name\": \"icon-expand\",\n          \"prevSize\": 16,\n          \"code\": 59943,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 89,\n          \"id\": 33,\n          \"name\": \"icon-list-view\",\n          \"prevSize\": 16,\n          \"code\": 59944,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 133,\n          \"id\": 28,\n          \"name\": \"icon-grid-snap-to\",\n          \"prevSize\": 16,\n          \"code\": 59945,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 132,\n          \"id\": 29,\n          \"name\": \"icon-grid-snap-no\",\n          \"prevSize\": 16,\n          \"code\": 59946,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 94,\n          \"id\": 26,\n          \"name\": \"icon-frame-show\",\n          \"prevSize\": 16,\n          \"code\": 59947,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 95,\n          \"id\": 25,\n          \"name\": \"icon-frame-hide\",\n          \"prevSize\": 16,\n          \"code\": 59948,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 97,\n          \"id\": 23,\n          \"name\": \"icon-import\",\n          \"prevSize\": 16,\n          \"code\": 59949,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 96,\n          \"id\": 24,\n          \"name\": \"icon-export\",\n          \"prevSize\": 16,\n          \"code\": 59950,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 194,\n          \"id\": 4,\n          \"name\": \"icon-font-size\",\n          \"prevSize\": 16,\n          \"code\": 59951,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 163,\n          \"id\": 141,\n          \"name\": \"icon-clear-data\",\n          \"prevSize\": 16,\n          \"code\": 59952,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 173,\n          \"id\": 149,\n          \"name\": \"icon-history\",\n          \"prevSize\": 16,\n          \"code\": 59953,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 181,\n          \"id\": 158,\n          \"name\": \"icon-arrow-up-to-parent\",\n          \"prevSize\": 16,\n          \"code\": 59954,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 184,\n          \"id\": 159,\n          \"name\": \"icon-crosshair-in-circle\",\n          \"prevSize\": 16,\n          \"code\": 59955,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 185,\n          \"id\": 161,\n          \"name\": \"icon-target\",\n          \"prevSize\": 16,\n          \"code\": 59956,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 187,\n          \"id\": 163,\n          \"name\": \"icon-items-collapse\",\n          \"prevSize\": 16,\n          \"code\": 59957,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 188,\n          \"id\": 162,\n          \"name\": \"icon-items-expand\",\n          \"prevSize\": 16,\n          \"code\": 59958,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 190,\n          \"id\": 164,\n          \"name\": \"icon-3-dots\",\n          \"prevSize\": 16,\n          \"code\": 59959,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 193,\n          \"id\": 165,\n          \"name\": \"icon-grid-on\",\n          \"prevSize\": 16,\n          \"code\": 59960,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 192,\n          \"id\": 166,\n          \"name\": \"icon-grid-off\",\n          \"prevSize\": 16,\n          \"code\": 59961,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 191,\n          \"id\": 167,\n          \"name\": \"icon-camera\",\n          \"prevSize\": 16,\n          \"code\": 59962,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 196,\n          \"id\": 168,\n          \"name\": \"icon-folders-collapse\",\n          \"prevSize\": 16,\n          \"code\": 59963,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 216,\n          \"id\": 187,\n          \"name\": \"icon-multiline\",\n          \"prevSize\": 16,\n          \"code\": 59964,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 217,\n          \"id\": 186,\n          \"name\": \"icon-singleline\",\n          \"prevSize\": 16,\n          \"code\": 59965,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 144,\n          \"id\": 97,\n          \"name\": \"icon-activity\",\n          \"prevSize\": 16,\n          \"code\": 60160,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 104,\n          \"id\": 16,\n          \"name\": \"icon-activity-mode\",\n          \"prevSize\": 16,\n          \"code\": 60161,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 137,\n          \"id\": 125,\n          \"name\": \"icon-autoflow-tabular\",\n          \"prevSize\": 16,\n          \"code\": 60162,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 115,\n          \"id\": 3,\n          \"name\": \"icon-clock\",\n          \"prevSize\": 16,\n          \"code\": 60163,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 2,\n          \"id\": 132,\n          \"name\": \"icon-database\",\n          \"prevSize\": 16,\n          \"code\": 60164,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 3,\n          \"id\": 131,\n          \"name\": \"icon-database-query\",\n          \"prevSize\": 16,\n          \"code\": 60165,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 67,\n          \"id\": 57,\n          \"name\": \"icon-dataset\",\n          \"prevSize\": 16,\n          \"code\": 60166,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 59,\n          \"id\": 67,\n          \"name\": \"icon-datatable\",\n          \"prevSize\": 16,\n          \"code\": 60167,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 136,\n          \"id\": 126,\n          \"name\": \"icon-dictionary\",\n          \"prevSize\": 16,\n          \"code\": 60168,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 51,\n          \"id\": 77,\n          \"name\": \"icon-folder\",\n          \"prevSize\": 16,\n          \"code\": 60169,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 147,\n          \"id\": 100,\n          \"name\": \"icon-image\",\n          \"prevSize\": 16,\n          \"code\": 60170,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 4,\n          \"id\": 130,\n          \"name\": \"icon-layout\",\n          \"prevSize\": 16,\n          \"code\": 60171,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 24,\n          \"id\": 108,\n          \"name\": \"icon-object\",\n          \"prevSize\": 16,\n          \"code\": 60172,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 52,\n          \"id\": 76,\n          \"name\": \"icon-object-unknown\",\n          \"prevSize\": 16,\n          \"code\": 60173,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 105,\n          \"id\": 15,\n          \"name\": \"icon-packet\",\n          \"prevSize\": 16,\n          \"code\": 60174,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 126,\n          \"id\": 74,\n          \"name\": \"icon-page\",\n          \"prevSize\": 16,\n          \"code\": 60175,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 130,\n          \"id\": 44,\n          \"name\": \"icon-plot-overlay\",\n          \"prevSize\": 16,\n          \"code\": 60176,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 80,\n          \"id\": 42,\n          \"name\": \"icon-plot-stacked\",\n          \"prevSize\": 16,\n          \"code\": 60177,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 134,\n          \"id\": 14,\n          \"name\": \"icon-session\",\n          \"prevSize\": 16,\n          \"code\": 60178,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 109,\n          \"id\": 9,\n          \"name\": \"icon-tabular\",\n          \"prevSize\": 16,\n          \"code\": 60179,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 107,\n          \"id\": 11,\n          \"name\": \"icon-tabular-lad\",\n          \"prevSize\": 16,\n          \"code\": 60180,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 106,\n          \"id\": 12,\n          \"name\": \"icon-tabular-lad-set\",\n          \"prevSize\": 16,\n          \"code\": 60181,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 70,\n          \"id\": 54,\n          \"name\": \"icon-tabular-realtime\",\n          \"prevSize\": 16,\n          \"code\": 60182,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 60,\n          \"id\": 66,\n          \"name\": \"icon-tabular-scrolling\",\n          \"prevSize\": 16,\n          \"code\": 60183,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 131,\n          \"id\": 43,\n          \"name\": \"icon-telemetry\",\n          \"prevSize\": 16,\n          \"code\": 60184,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 202,\n          \"id\": 10,\n          \"name\": \"icon-timeline\",\n          \"prevSize\": 16,\n          \"code\": 60185,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 81,\n          \"id\": 41,\n          \"name\": \"icon-timer\",\n          \"prevSize\": 16,\n          \"code\": 60186,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 69,\n          \"id\": 55,\n          \"name\": \"icon-topic\",\n          \"prevSize\": 16,\n          \"code\": 60187,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 79,\n          \"id\": 45,\n          \"name\": \"icon-box-with-dashed-lines-v2\",\n          \"prevSize\": 16,\n          \"code\": 60188,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 90,\n          \"id\": 32,\n          \"name\": \"icon-summary-widget\",\n          \"prevSize\": 16,\n          \"code\": 60189,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 92,\n          \"id\": 30,\n          \"name\": \"icon-notebook\",\n          \"prevSize\": 16,\n          \"code\": 60190,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 168,\n          \"id\": 0,\n          \"name\": \"icon-tabs-view\",\n          \"prevSize\": 16,\n          \"code\": 60191,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 117,\n          \"id\": 1,\n          \"name\": \"icon-flexible-layout\",\n          \"prevSize\": 16,\n          \"code\": 60192,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 166,\n          \"id\": 144,\n          \"name\": \"icon-generator-sine\",\n          \"prevSize\": 16,\n          \"code\": 60193,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 167,\n          \"id\": 143,\n          \"name\": \"icon-generator-event\",\n          \"prevSize\": 16,\n          \"code\": 60194,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 165,\n          \"id\": 138,\n          \"name\": \"icon-gauge-v2\",\n          \"prevSize\": 16,\n          \"code\": 60195,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 170,\n          \"id\": 148,\n          \"name\": \"icon-spectra\",\n          \"prevSize\": 16,\n          \"code\": 60196,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 171,\n          \"id\": 147,\n          \"name\": \"icon-telemetry-spectra\",\n          \"prevSize\": 16,\n          \"code\": 60197,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 172,\n          \"id\": 146,\n          \"name\": \"icon-pushbutton\",\n          \"prevSize\": 16,\n          \"code\": 60198,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 174,\n          \"id\": 151,\n          \"name\": \"icon-conditional\",\n          \"prevSize\": 16,\n          \"code\": 60199,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 178,\n          \"id\": 154,\n          \"name\": \"icon-condition-widget\",\n          \"prevSize\": 16,\n          \"code\": 60200,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 180,\n          \"id\": 155,\n          \"name\": \"icon-alphanumeric\",\n          \"prevSize\": 16,\n          \"code\": 60201,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 183,\n          \"id\": 156,\n          \"name\": \"icon-image-telemetry\",\n          \"prevSize\": 16,\n          \"code\": 60202,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 198,\n          \"id\": 170,\n          \"name\": \"icon-telemetry-aggregate\",\n          \"prevSize\": 16,\n          \"code\": 60203,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 199,\n          \"id\": 172,\n          \"name\": \"icon-bar-graph\",\n          \"prevSize\": 16,\n          \"code\": 60204,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 200,\n          \"id\": 171,\n          \"name\": \"icon-map\",\n          \"prevSize\": 16,\n          \"code\": 60205,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 203,\n          \"id\": 174,\n          \"name\": \"icon-plan\",\n          \"prevSize\": 16,\n          \"code\": 60206,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 204,\n          \"id\": 175,\n          \"name\": \"icon-timelist\",\n          \"prevSize\": 16,\n          \"code\": 60207,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 205,\n          \"id\": 176,\n          \"name\": \"icon-plot-scatter\",\n          \"prevSize\": 16,\n          \"code\": 60208,\n          \"tempChar\": \"\"\n        },\n        {\n          \"order\": 218,\n          \"id\": 184,\n          \"name\": \"icon-notebook-restricted\",\n          \"prevSize\": 16,\n          \"code\": 60209,\n          \"tempChar\": \"\"\n        }\n      ],\n      \"id\": 0,\n      \"metadata\": {\n        \"name\": \"Open MCT Symbols 16px\",\n        \"importSize\": {\n          \"width\": 576,\n          \"height\": 512\n        },\n        \"designer\": \"Charles Hacskaylo\"\n      },\n      \"height\": 1024,\n      \"prevSize\": 16,\n      \"icons\": [\n        {\n          \"id\": 47,\n          \"paths\": [\n            \"M896 0h-768c-70.6 0.2-127.8 57.4-128 128v768c0.2 70.6 57.4 127.8 128 128h768c70.6-0.2 127.8-57.4 128-128v-768c-0.2-70.6-57.4-127.8-128-128zM576 896h-128v-128h128v128zM597.8 512l-37.8 192h-96l-37.8-192v-384h171.8v384z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-alert-rect-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 48,\n          \"paths\": [\n            \"M998.2 848.8l-422.6-739.6c-35-61.2-92-61.2-127 0l-422.8 739.6c-35 61.2-6 111.2 64.4 111.2h843.4c70.6 0 99.6-50 64.6-111.2zM576 896h-128v-128h128v128zM597.8 512l-37.8 192h-96l-37.8-192v-256h171.8v256z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-alert-triangle-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 112,\n          \"paths\": [\n            \"M512 256l-512 512h1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-up\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 94,\n          \"paths\": [\n            \"M510 510l512 512h-1024z\",\n            \"M510-2l512 512h-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-double-up\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 96,\n          \"paths\": [\n            \"M512 0l512 1024h-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-tall-up\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 111,\n          \"paths\": [\n            \"M768 512l-512-512v1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-right\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 18,\n          \"paths\": [\n            \"M962 512l-896 512v-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-right-equilateral\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 113,\n          \"paths\": [\n            \"M512 768l512-512h-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-down\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 93,\n          \"paths\": [\n            \"M510 510l-512-512h1024z\",\n            \"M510 1022l-512-512h1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-double-down\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 95,\n          \"paths\": [\n            \"M512 1024l-512-1024h1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-tall-down\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 114,\n          \"paths\": [\n            \"M256 512l512 512v-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-left\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 115,\n          \"paths\": [\n            \"M1004.166 340.458l-97.522-168.916-330.534 229.414 33.414-400.956h-195.048l33.414 400.956-330.534-229.414-97.522 168.916 363.944 171.542-363.944 171.542 97.522 168.916 330.534-229.414-33.414 400.956h195.048l-33.414-400.956 330.534 229.414 97.522-168.916-363.944-171.542z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-asterisk\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 59,\n          \"paths\": [\n            \"M512 1024c106 0 192-86 192-192h-384c0 106 86 192 192 192z\",\n            \"M896 448v-64c0-212-172-384-384-384s-384 172-384 384v64c0 70.6-57.4 128-128 128v128h1024v-128c-70.6 0-128-57.4-128-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-bell\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 103,\n          \"paths\": [\n            \"M1024 832c0 105.6-86.4 192-192 192h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-box-round-corners\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 58,\n          \"paths\": [\n            \"M894-2h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h400c-2.2-3.8-4-7.6-5.8-11.4l-255.2-576.8c-21.4-48.4-10.8-105 26.6-142.4 24.4-24.4 57.2-37.4 90.4-37.4 17.4 0 35.2 3.6 51.8 11l576.6 255.4c4 1.8 7.8 3.8 11.4 5.8v-400.2c0.2-70.4-57.4-128-127.8-128z\",\n            \"M958.6 637.4l-576.6-255.4 255.4 576.6 64.6-128.6 192 192 128-128-192-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-box-with-arrow-cursor\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 120,\n          \"paths\": [\n            \"M1024 0l-640 640-384-384v384l384 384 640-640z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-check\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 107,\n          \"paths\": [\n            \"M704 576c0 70.4-57.6 128-128 128h-128c-70.4 0-128-57.6-128-128v-128c0-70.4 57.6-128 128-128h128c70.4 0 128 57.6 128 128v128z\",\n            \"M1024 512l-192-320v640z\",\n            \"M0 512l192-320v640z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-connectivity\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 84,\n          \"paths\": [\n            \"M768 352c0 53.019-114.615 96-256 96s-256-42.981-256-96c0-53.019 114.615-96 256-96s256 42.981 256 96z\",\n            \"M768 672v-256c0 53-114.6 96-256 96s-256-43-256-96v256c0 53 114.6 96 256 96s256-43 256-96z\",\n            \"M832 0h-128v192h127.6c0.2 0 0.2 0.2 0.4 0.4v639.4c0 0.2-0.2 0.2-0.4 0.4h-127.6v192h128c105.6 0 192-86.4 192-192v-640.2c0-105.6-86.4-192-192-192z\",\n            \"M192 831.6v-639.4c0-0.2 0.2-0.2 0.4-0.4h127.6v-191.8h-128c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h128v-192h-127.6c-0.2 0-0.4-0.2-0.4-0.4z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-database-in-brackets\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 49,\n          \"paths\": [\n            \"M512 116.4c-245.8 0-452.2 168-510.8 395.6 58.6 227.4 265 395.6 510.8 395.6s452.2-168 510.8-395.6c-58.6-227.4-265-395.6-510.8-395.6zM829.2 588.4c-22.6 34.4-50.6 64.8-83 90.4-32.8 25.8-69 45.6-108 59.4-40.4 14.2-82.8 21.4-126 21.4s-85.8-7.2-126-21.4c-39-13.8-75.4-33.8-108-59.4-32.4-25.6-60.4-55.8-83-90.4-15.8-24-28.8-49.6-38.6-76.4 10-26.8 23-52.4 38.6-76.4 22.6-34.4 50.6-64.8 83-90.4 32.8-25.8 69-45.6 108-59.4 40.4-14.2 82.8-21.4 126-21.4s85.8 7.2 126 21.4c39 13.8 75.4 33.8 108 59.4 32.4 25.6 60.4 55.8 83 90.4 15.8 24 28.8 49.6 38.6 76.4-9.8 26.8-22.8 52.4-38.6 76.4z\",\n            \"M704 512c0 106.039-85.961 192-192 192s-192-85.961-192-192c0-106.039 85.961-192 192-192s192 85.961 192 192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-eye-open-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 129,\n          \"paths\": [\n            \"M1024 576v-128l-140.976-35.244c-8.784-32.922-21.818-64.106-38.504-92.918l74.774-124.622-90.51-90.51-124.622 74.774c-28.812-16.686-59.996-29.72-92.918-38.504l-35.244-140.976h-128l-35.244 140.976c-32.922 8.784-64.106 21.818-92.918 38.504l-124.622-74.774-90.51 90.51 74.774 124.622c-16.686 28.812-29.72 59.996-38.504 92.918l-140.976 35.244v128l140.976 35.244c8.784 32.922 21.818 64.106 38.504 92.918l-74.774 124.622 90.51 90.51 124.622-74.774c28.812 16.686 59.996 29.72 92.918 38.504l35.244 140.976h128l35.244-140.976c32.922-8.784 64.106-21.818 92.918-38.504l124.622 74.774 90.51-90.51-74.774-124.622c16.686-28.812 29.72-59.996 38.504-92.918l140.976-35.244zM704 512c0 106.038-85.962 192-192 192s-192-85.962-192-192 85.962-192 192-192 192 85.962 192 192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-gear\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 60,\n          \"paths\": [\n            \"M1024 0h-1024c0 282.8 229.2 512 512 512s512-229.2 512-512zM512 384c-102.6 0-199-40-271.6-112.4-41.2-41.2-72-90.2-90.8-143.6h724.6c-18.8 53.4-49.6 102.4-90.8 143.6-72.4 72.4-168.8 112.4-271.4 112.4z\",\n            \"M512 512c-282.8 0-512 229.2-512 512h1024c0-282.8-229.2-512-512-512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-hourglass\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 61,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM512 128c70.6 0 128 57.4 128 128s-57.4 128-128 128c-70.6 0-128-57.4-128-128s57.4-128 128-128zM704 832h-384v-128h64v-256h256v256h64v128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-info\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 75,\n          \"paths\": [\n            \"M1024 512l-512-512v307.2l-512 204.8v256h512v256z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-link-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 86,\n          \"paths\": [\n            \"M702 384h-62v-128c0-141.385-114.615-256-256-256s-256 114.615-256 256v0 128h-64c-35.301 0.113-63.887 28.699-64 63.989v512.011c0.113 35.301 28.699 63.887 63.989 64h638.011c35.301-0.113 63.887-28.699 64-63.989v-512.011c-0.113-35.301-28.699-63.887-63.989-64h-0.011zM256 384v-128c0-70.692 57.308-128 128-128s128 57.308 128 128v0 128z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-lock\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          },\n          \"width\": 768\n        },\n        {\n          \"id\": 79,\n          \"paths\": [\n            \"M960 640c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64h-896c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64h896z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-minus\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 110,\n          \"paths\": [\n            \"M704 320h64c70.4 0 128-57.6 128-128v-64c0-70.4-57.6-128-128-128h-64c-70.4 0-128 57.6-128 128v64c0 70.4 57.6 128 128 128z\",\n            \"M256 320h64c70.4 0 128-57.6 128-128v-64c0-70.4-57.6-128-128-128h-64c-70.4 0-128 57.6-128 128v64c0 70.4 57.6 128 128 128z\",\n            \"M832 384h-192c-34.908 0-67.716 9.448-96 25.904 57.278 33.324 96 95.404 96 166.096v448h384v-448c0-105.6-86.4-192-192-192z\",\n            \"M384 384h-192c-105.6 0-192 86.4-192 192v448h576v-448c0-105.6-86.4-192-192-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-people\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 82,\n          \"paths\": [\n            \"M768 256c0 105.6-86.4 192-192 192h-128c-105.6 0-192-86.4-192-192v-64c0-105.6 86.4-192 192-192h128c105.6 0 192 86.4 192 192v64z\",\n            \"M64 1024v-192c0-140.8 115.2-256 256-256h384c140.8 0 256 115.2 256 256v192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-person\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 92,\n          \"paths\": [\n            \"M960 384h-330v-320c0-35.2-28.8-64-64-64h-108c-35.2 0-64 28.8-64 64v320h-330c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64h330v320c0 35.2 28.8 64 64 64h108c35.2 0 64-28.8 64-64v-320h330c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plus\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 137,\n          \"paths\": [\n            \"M830 0h-636c-106.6 0-194 87.2-194 194v636c0 106.8 87.4 194 194 194h636c106.6 0 194-87.2 194-194v-636c0-106.8-87.4-194-194-194zM896 608c0 17.673-14.327 32-32 32v0h-224v224c0 17.673-14.327 32-32 32v0h-192c-17.673 0-32-14.327-32-32v0-224h-224c-17.673 0-32-14.327-32-32v0-192c0-17.673 14.327-32 32-32v0h224v-224c0-17.673 14.327-32 32-32v0h192c17.673 0 32 14.327 32 32v0 224h224c17.673 0 32 14.327 32 32v0z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plus-in-rect\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 128,\n          \"paths\": [\n            \"M832 128h-192.36v-64c0-35.2-28.8-64-64-64h-128c-35.2 0-64 28.8-64 64v64h-191.64c-105.6 0-192 72-192 160s0 160 0 160h64v384c0 105.6 86.4 192 192 192h512c105.6 0 192-86.4 192-192v-384h64c0 0 0-72 0-160s-86.4-160-192-160zM320 832h-128v-384h128v384zM576 832h-128v-384h128v384zM832 832h-128v-384h128v384z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-trash\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 122,\n          \"paths\": [\n            \"M704 512l301.332 301.332c24.89 24.89 24.89 65.62 0 90.51l-101.49 101.49c-24.89 24.89-65.62 24.89-90.51 0l-301.332-301.332c0 0-301.332 301.332-301.332 301.332-24.89 24.89-65.62 24.89-90.51 0l-101.49-101.49c-24.89-24.89-24.89-65.62 0-90.51l301.332-301.332c0 0-301.332-301.332-301.332-301.332-24.89-24.89-24.89-65.62 0-90.51l101.49-101.49c24.89-24.89 65.62-24.89 90.51 0l301.332 301.332c0 0 301.332-301.332 301.332-301.332 24.89-24.89 65.62-24.89 90.51 0l101.49 101.49c24.89 24.89 24.89 65.62 0 90.51 0 0-301.332 301.332-301.332 301.332z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-x-heavy\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 46,\n          \"paths\": [\n            \"M832 0h-192v192h191.66l0.34 0.34v639.32l-0.34 0.34h-191.66v192h192c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192z\",\n            \"M384 832h-191.66l-0.34-0.34v-639.32l0.34-0.34h191.66v-192h-192c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h192v-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-brackets\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 27,\n          \"paths\": [\n            \"M574-2h-128v320h128v-320z\",\n            \"M1022 446h-320v128h320v-128z\",\n            \"M574 702h-128v320h128v-320z\",\n            \"M318 446h-320v128h320v-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-crosshair\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 31,\n          \"paths\": [\n            \"M365.4 182.8c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M365.4 402.2c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M365.4 621.8c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M365.4 841.2c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M584.8 73.2c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M584.8 292.6c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M584.8 512c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M584.8 731.4c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M584.8 950.8c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M804.2 182.8c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M804.2 402.2c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M804.2 621.8c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\",\n            \"M804.2 841.2c0 40.427-32.773 73.2-73.2 73.2s-73.2-32.773-73.2-73.2c0-40.427 32.773-73.2 73.2-73.2s73.2 32.773 73.2 73.2z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grippy-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 118,\n          \"paths\": [\n            \"M0 576v256c0 105.6 86.4 192 192 192h256v-448h-448z\",\n            \"M448 0h-256c-105.6 0-192 86.4-192 192v256h448v-448z\",\n            \"M832 0h-256v448h448v-256c0-105.6-86.4-192-192-192z\",\n            \"M576 1024h256c105.6 0 192-86.4 192-192v-256h-448v448z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grid-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 136,\n          \"paths\": [\n            \"M704 0h128v1024h-128v-1024z\",\n            \"M448 0h128v1024h-128v-1024z\",\n            \"M192 0h128v1024h-128v-1024z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grippy-ew\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 135,\n          \"paths\": [\n            \"M0 0h256v1024h-256v-1024z\",\n            \"M384 0h256v1024h-256v-1024z\",\n            \"M768 0h256v1024h-256v-1024z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-columns\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 134,\n          \"paths\": [\n            \"M0 0h1024v256h-1024v-256z\",\n            \"M0 384h1024v256h-1024v-256z\",\n            \"M0 768h1024v256h-1024v-256z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-rows\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 140,\n          \"paths\": [\n            \"M896 0h-768c-70.601 0.227-127.773 57.399-128 127.978l-0 0.022v768c0.227 70.601 57.399 127.773 127.978 128l0.022 0h256v-512l-192-192h640l-192 192v512h256c70.601-0.227 127.773-57.399 128-127.978l0-0.022v-768c-0.227-70.601-57.399-127.773-127.978-128l-0.022-0z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-filter\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 139,\n          \"paths\": [\n            \"M896 0h-768c-70.601 0.227-127.773 57.399-128 127.978l-0 0.022v768c0.227 70.601 57.399 127.773 127.978 128l0.022 0h768c70.601-0.227 127.773-57.399 128-127.978l0-0.022v-768c-0.227-70.601-57.399-127.773-127.978-128l-0.022-0zM896 895.8h-256v-383.8l192-192h-640l192 192v384h-256v-767.8h768z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-filter-outline\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 142,\n          \"paths\": [\n            \"M768 128c-0.080-70.66-57.34-127.92-127.993-128l-256.007-0c-70.66 0.080-127.92 57.34-128 127.993l-0 0.007v128h-64v768h640v-768h-64zM384 128.12l0.12-0.12 255.88 0.12v127.88h-256z\",\n            \"M0 320v640c0.102 35.305 28.695 63.898 63.99 64l0.010 0h64v-768h-64c-35.305 0.102-63.898 28.695-64 63.99l-0 0.010z\",\n            \"M960 256h-64v768h64c35.305-0.102 63.898-28.695 64-63.99l0-0.010v-640c-0.102-35.305-28.695-63.898-63.99-64l-0.010-0z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-suitcase\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 145,\n          \"paths\": [\n            \"M704 320h-64v-64c0-141.385-114.615-256-256-256s-256 114.615-256 256v0 64h-64c-35.301 0.113-63.887 28.699-64 63.989l-0 0.011v576c0.113 35.301 28.699 63.887 63.989 64l0.011 0h640c35.301-0.113 63.887-28.699 64-63.989l0-0.011v-576c-0.113-35.301-28.699-63.887-63.989-64l-0.011-0zM256 256c0-70.692 57.308-128 128-128s128 57.308 128 128v0 64h-256zM533.4 896l-128-128-43 85-170.4-383.6 383.6 170.2-85 43 128 128z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"width\": 768,\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-cursor-locked\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 150,\n          \"paths\": [\n            \"M192 640h832l-192-320 192-320h-896c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v896h192z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-flag\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 152,\n          \"paths\": [\n            \"M209.46 608.68q-7.46-9.86-14.26-20.28c-14.737-21.984-27.741-47.184-37.759-73.847l-0.841-2.553c11.078-29.259 24.068-54.443 39.51-77.869l-0.91 1.469c23.221-34.963 50.705-64.8 82.207-89.793l0.793-0.607c57.663-45.719 130.179-75.053 209.311-79.947l1.069-0.053 114.48-140.88c-27.366-5.017-58.869-7.898-91.041-7.92l-0.019-0c-245.8 0-452.2 168-510.8 395.6 21.856 82.93 60.906 154.847 113.325 214.773l-0.525-0.613z\",\n            \"M814.76 415.080q7.52 10 14.44 20.52c14.737 21.984 27.741 47.184 37.759 73.847l0.841 2.553c-10.859 29.216-23.863 54.416-39.447 77.748l0.847-1.348c-23.221 34.963-50.705 64.8-82.207 89.793l-0.793 0.607c-57.762 45.834-130.437 75.216-209.743 80.049l-1.057 0.051-114.46 140.86c27.346 4.988 58.817 7.84 90.955 7.84 0.037 0 0.074-0 0.111-0l-0.005 0c245.8 0 452.2-168 510.8-395.6-21.856-82.93-60.906-154.847-113.325-214.773l0.525 0.613z\",\n            \"M832 0l-832 1024h192l832-1024h-192z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-eye-disabled\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 153,\n          \"paths\": [\n            \"M830 62h-830l-4 702c0 106.6 87.4 194 194 194h640c106.6 0 194-87.4 194-194v-508c0-106.8-87.4-194-194-194zM832 446l-384 384-192-192v-256l192 192 384-384v256z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-notebook-page\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 160,\n          \"paths\": [\n            \"M768 0c-141.339 0.114-255.886 114.661-256 255.989l-0 0.011v128h-448c-35.301 0.113-63.887 28.699-64 63.989l-0 0.011v512c0.113 35.301 28.699 63.887 63.989 64l0.011 0h638c35.301-0.113 63.887-28.699 64-63.989l0-0.011v-512c-0.113-35.301-28.699-63.887-63.989-64l-0.011-0h-62v-128c0-70.692 57.308-128 128-128s128 57.308 128 128v0 128h128v-128c-0.114-141.339-114.661-255.886-255.989-256l-0.011-0z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-unlocked\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 169,\n          \"paths\": [\n            \"M1024 512c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512s512 229.23 512 512z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-circle\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 173,\n          \"paths\": [\n            \"M876.34 635.58l-49.9 49.88-19.26 19.5-26 8.7-423.040 144.2 144.2-423.28 8.84-25.78 150-149.88-85.6-149.78c-34.92-61.12-92-61.12-127 0l-422.78 739.72c-34.94 61.14-5.92 111.14 64.48 111.14h843.44c70.4 0 99.42-50 64.48-111.14z\",\n            \"M973.18 242.84c-19.32-19.3-40.66-34.62-60.16-43.16-34.42-15.12-52.38-4.54-60.1 3.16l-258.12 258.12-82.8 243.040 243-82.8 3.36-3.4 254.76-254.76c4.94-4.94 10.88-13.88 10.88-28.3 0-25.34-19.5-60.56-50.82-91.9zM631 619.82l-34.88-34.86 34.64-101.6 9.24-3.36h32v64h64v32l-3.42 9.26z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-draft\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 183,\n          \"paths\": [\n            \"M512 0c-282.78 0-512 229.22-512 512s229.22 512 512 512 512-229.22 512-512-229.22-512-512-512zM263.1 263.1c66.48-66.48 154.88-103.1 248.9-103.1 66.74 0 130.64 18.48 185.9 52.96l-484.94 484.94c-34.5-55.24-52.96-119.16-52.96-185.9 0-94.020 36.62-182.42 103.1-248.9zM760.9 760.9c-66.48 66.48-154.88 103.1-248.9 103.1-66.74 0-130.64-18.48-185.9-52.96l484.94-484.94c34.5 55.24 52.96 119.16 52.96 185.9 0 94.020-36.62 182.42-103.1 248.9z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-circle-slash\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 182,\n          \"paths\": [\n            \"M136.86 52.26c54.080-34.82 120.58-52.26 199.44-52.26 103.6 0 189.7 24.76 258.24 74.28s102.82 122.88 102.82 220.060c0 59.6-14.86 109.8-44.58 150.6-17.38 24.76-50.76 56.4-100.14 94.9l-48.68 37.82c-26.54 20.64-44.14 44.7-52.82 72.2-5.5 17.44-8.46 44.48-8.92 81.14h-186.4c2.74-77.48 10.060-131 21.94-160.58s42.5-63.62 91.88-102.12l50.060-39.2c16.46-12.38 29.72-25.9 39.78-40.58 18.28-25.2 27.42-52.96 27.42-83.22 0-34.84-10.18-66.6-30.52-95.24-20.36-28.64-57.52-42.98-111.48-42.98s-90.68 17.66-112.88 52.96c-22.18 35.32-33.26 71.98-33.26 110.040h-198.76c5.5-130.64 51.12-223.24 136.86-277.82zM251.020 825.24h205.62v198.74h-205.62v-198.74z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"width\": 697,\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-question-mark\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 179,\n          \"paths\": [\n            \"M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM768 448l-320 320-192-192v-192l192 192 320-320v192z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-status-poll-check\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 178,\n          \"paths\": [\n            \"M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM781.36 704h-538.72c-44.96 0-63.5-31.94-41.2-70.98l270-472.48c22.3-39.040 58.82-39.040 81.12 0l269.98 472.48c22.3 39.040 3.78 70.98-41.2 70.98z\",\n            \"M457.14 417.86l24.2 122.64h61.32l24.2-122.64v-163.5h-109.72v163.5z\",\n            \"M471.12 581.36h81.76v81.76h-81.76v-81.76z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-status-poll-caution\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 180,\n          \"paths\": [\n            \"M391.18 668.7c35.72 22.98 77.32 35.3 120.82 35.3 59.84 0 116.080-23.3 158.4-65.6 42.3-42.3 65.6-98.56 65.6-158.4 0-43.5-12.32-85.080-35.3-120.82l-309.52 309.52z\",\n            \"M512 256c-59.84 0-116.080 23.3-158.4 65.6-42.3 42.3-65.6 98.56-65.6 158.4 0 43.5 12.32 85.080 35.3 120.82l309.52-309.52c-35.72-22.98-77.32-35.3-120.82-35.3z\",\n            \"M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM512 800c-176.74 0-320-143.26-320-320s143.26-320 320-320 320 143.26 320 320-143.26 320-320 320z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-status-poll-circle-slash\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 181,\n          \"paths\": [\n            \"M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM579.020 832h-141.36v-136.64h141.36v136.64zM713.84 433.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-status-poll-question-mark\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 176,\n          \"paths\": [\n            \"M1000.080 334.64l-336.6 336.76-20.52 6.88-450.96 153.72 160.68-471.52 332.34-332.34c-54.040-18.2-112.28-28.14-173.020-28.14-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480 0-50.68-8.4-99.5-23.92-145.36z\",\n            \"M408.42 395.24l-2.16 6.3-111.7 327.9 334.12-113.86 4.62-4.68 350.28-350.28c6.8-6.78 14.96-19.1 14.96-38.9 0-34.86-26.82-83.28-69.88-126.38-26.54-26.54-55.9-47.6-82.7-59.34-47.34-20.8-72.020-6.24-82.64 4.36l-354.9 354.88zM470.56 421.42h44v88h88v44l-4.7 12.72-139.68 47.54-47.94-47.94 47.6-139.72 12.72-4.6z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-status-poll-edit\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 186,\n          \"paths\": [\n            \"M832 0h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM681.38 365.14c0.68-20.46-2.22-37.7-8.7-51.68-6.5-13.98-15.7-25.4-27.64-34.28-11.94-8.86-26.1-15.18-42.48-18.94-16.38-3.74-33.78-5.62-52.2-5.62-15.020 0-30.2 1.54-45.54 4.6s-29.16 8.18-41.44 15.36-22.18 16.56-29.68 28.14c-7.52 11.62-11.26 25.94-11.26 42.98s6.66 32.6 19.96 44.52c13.3 11.94 29.34 21.84 48.1 29.68 18.76 7.86 38.020 14 57.82 18.42 19.78 4.44 35.82 8.020 48.1 10.74 28.66 7.52 54.92 16.22 78.8 26.1 23.88 9.9 44.52 22.68 61.92 38.38s30.86 34.8 40.42 57.32c9.54 22.52 14.32 50.16 14.32 82.9 0 43.68-9.040 80.86-27.12 111.56s-41.28 55.62-69.6 74.7c-28.32 19.1-60.22 32.92-95.7 41.44s-70.62 12.8-105.42 12.8c-102.34 0-178.6-20.8-228.74-62.44-50.16-41.6-75.22-107.1-75.22-196.5h152.5c-1.38 25.94 1.7 47.58 9.22 64.98 7.5 17.4 18.42 31.22 32.74 41.44 14.32 10.24 31.38 17.58 51.18 22 19.78 4.44 41.28 6.66 64.48 6.66 16.38 0 32.74-2.040 49.12-6.14s31.22-10.24 44.52-18.42c13.3-8.18 24.22-18.76 32.76-31.72 8.52-12.94 12.8-28.66 12.8-47.080s-5.46-32.24-16.38-43.5c-10.92-11.26-25.080-20.98-42.48-29.16s-37.2-15.36-59.36-21.5c-22.18-6.14-44.52-12.62-67.040-19.44-23.2-6.82-45.72-15-67.54-24.56-21.84-9.54-41.44-21.82-58.84-36.84-17.4-15-31.38-33.42-41.96-55.26-10.58-21.82-15.86-48.44-15.86-79.82 0-40.94 8.52-75.74 25.58-104.4 17.040-28.66 39.22-52.020 66.52-70.1 27.28-18.080 58.16-31.38 92.62-39.92 34.44-8.52 69.42-12.8 104.9-12.8 37.52 0 72.82 4.26 105.92 12.8 33.080 8.54 62.080 22.36 87 41.44 24.9 19.1 44.68 43.5 59.36 73.18 14.66 29.68 22 65.68 22 107.98h-152.5z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-stale\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 105,\n          \"paths\": [\n            \"M1024 512l-448 512v-1024z\",\n            \"M448 0l-448 512 448 512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrows-right-left\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 106,\n          \"paths\": [\n            \"M512 0l512 448h-1024z\",\n            \"M0 576l512 448 512-448z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrows-up-down\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 56,\n          \"paths\": [\n            \"M832 752c0 44-36 80-80 80h-480c-44 0-80-36-80-80v-480c0-44 36-80 80-80h480c44 0 80 36 80 80v480z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-bullet\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 133,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM640 448h-256v-192h256v192zM384 512h256v192h-256v-192zM320 704h-256v-192h256v192zM320 256v192h-256v-192h256zM128 960c-17 0-33-6.6-45.2-18.8s-18.8-28.2-18.8-45.2v-128h256v192h-192zM384 960v-192h256v192h-256zM960 896c0 17-6.6 33-18.8 45.2s-28.2 18.8-45.2 18.8h-192v-192h256v128zM960 704h-256v-192h256v192zM960 448h-256v-192h256v192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-calendar\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 83,\n          \"paths\": [\n            \"M958.4 65.6c-43.8-43.8-101-65.6-158.4-65.6s-114.6 21.8-158.4 65.6l-128 128c-74 74-85.4 187-34 273l-12.8 12.8c-35.4-20.8-75-31.4-114.8-31.4-57.4 0-114.6 21.8-158.4 65.6l-128 128c-87.4 87.4-87.4 229.4 0 316.8 43.8 43.8 101 65.6 158.4 65.6s114.6-21.8 158.4-65.6l128-128c74-74 85.4-187 34-273l12.8-12.8c35.2 21 75 31.6 114.6 31.6 57.4 0 114.6-21.8 158.4-65.6l128-128c87.6-87.6 87.6-229.6 0.2-317zM419.8 739.8l-128 128c-18 18.2-42.2 28.2-67.8 28.2s-49.8-10-67.8-28.2c-37.4-37.4-37.4-98.4 0-135.8l128-128c18.2-18.2 42.2-28.2 67.8-28.2 5.6 0 11.2 0.6 16.8 1.4l-55.6 55.6c-10.4 10.4-16.2 24.2-16.2 38.8s5.8 28.6 16.2 38.8c10.4 10.4 24.2 16.2 38.8 16.2s28.6-5.8 38.8-16.2l55.6-55.6c5.4 30.4-3.6 62.2-26.6 85zM867.8 291.8l-128 128c-18 18.2-42.2 28.2-67.8 28.2-5.6 0-11.2-0.6-16.8-1.4l55.6-55.6c10.4-10.4 16.2-24.2 16.2-38.8s-5.8-28.6-16.2-38.8c-10.4-10.4-24.2-16.2-38.8-16.2s-28.6 5.8-38.8 16.2l-55.6 55.6c-5.2-29.8 3.6-61.6 26.6-84.6l128-128c18-18.4 42.2-28.4 67.8-28.4s49.8 10 67.8 28.2c37.6 37.4 37.6 98.2 0 135.6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-chain-links\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 51,\n          \"paths\": [\n            \"M832 576v255.66l-0.34 0.34-639.66-0.34v-255.66h-192v256c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-256h-192z\",\n            \"M512 640l448-448h-256v-192h-384v192h-256l448 448z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-download\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 91,\n          \"paths\": [\n            \"M640 256v-128c0-70.4-57.6-128-128-128h-384c-70.4 0-128 57.6-128 128v384c0 70.4 57.6 128 128 128h128v-139.6c0-134.8 109.6-244.4 244.4-244.4h139.6z\",\n            \"M896 384h-384c-70.4 0-128 57.6-128 128v384c0 70.4 57.6 128 128 128h384c70.4 0 128-57.6 128-128v-384c0-70.4-57.6-128-128-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-duplicate\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 78,\n          \"paths\": [\n            \"M896 192h-320c-16.4-16.4-96.8-96.8-109.2-109.2l-37.4-37.4c-25-25-74.2-45.4-109.4-45.4h-256c-35.2 0-64 28.8-64 64v384c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v-128c0-70.4-57.6-128-128-128z\",\n            \"M896 448h-768c-70.4 0-128 57.6-128 128v320c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-320c0-70.4-57.6-128-128-128zM704 800h-128v128h-128v-128h-128v-128h128v-128h128v128h128v128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-folder-new-v2.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 124,\n          \"paths\": [\n            \"M191.656 832c0.118 0.1 0.244 0.224 0.344 0.344v191.656h192v-192c0-105.6-86.4-192-192-192h-192v192h191.656z\",\n            \"M192 191.656c-0.1 0.118-0.224 0.244-0.344 0.344h-191.656v192h192c105.6 0 192-86.4 192-192v-192h-192v191.656z\",\n            \"M832 384h192v-192h-191.656c-0.118-0.1-0.244-0.226-0.344-0.344v-191.656h-192v192c0 105.6 86.4 192 192 192z\",\n            \"M832 832.344c0.1-0.118 0.224-0.244 0.344-0.344h191.656v-192h-192c-105.6 0-192 86.4-192 192v192h192v-191.656z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-fullscreen-collapse\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 123,\n          \"paths\": [\n            \"M192.344 832c-0.118-0.1-0.244-0.224-0.344-0.344v-191.656h-192v192c0 105.6 86.4 192 192 192h192v-192h-191.656z\",\n            \"M192 192.344c0.1-0.118 0.224-0.244 0.344-0.344h191.656v-192h-192c-105.6 0-192 86.4-192 192v192h192v-191.656z\",\n            \"M832 0h-192v192h191.656c0.118 0.1 0.244 0.226 0.344 0.344v191.656h192v-192c0-105.6-86.4-192-192-192z\",\n            \"M832 831.656c-0.1 0.118-0.224 0.244-0.344 0.344h-191.656v192h192c105.6 0 192-86.4 192-192v-192h-192v191.656z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-fullscreen-expand\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 104,\n          \"paths\": [\n            \"M1024 384l-512-384-512 384 512 384z\",\n            \"M512 896l-426.666-320-85.334 64 512 384 512-384-85.334-64z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-layers\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 102,\n          \"paths\": [\n            \"M64 576c-35.346 0-64-28.654-64-64s28.654-64 64-64h896c35.346 0 64 28.654 64 64s-28.654 64-64 64h-896z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-line-horz\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 20,\n          \"paths\": [\n            \"M1024 896l-256.8-256.8c42.4-66.6 65-144 64.8-223.2 0-229.8-186.2-416-416-416s-416 186.2-416 416 186.2 416 416 416c79 0.2 156.4-22.4 223.2-64.8l256.8 256.8 128-128zM212.4 619.6c-112.4-112.4-112.4-294.8 0-407.2s294.8-112.4 407.2 0 112.4 294.8 0 407.2c-54 54-127.2 84.4-203.6 84.4-76.4 0.2-149.8-30.2-203.6-84.4z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-magnify-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 21,\n          \"paths\": [\n            \"M1024 896l-256.86-256.86c40.681-62.963 64.861-139.898 64.861-222.481 0-0.232-0-0.464-0.001-0.696l0 0.036c0-229.76-186.24-416-416-416s-416 186.24-416 416 186.24 416 416 416c0.196 0 0.427 0.001 0.659 0.001 82.583 0 159.518-24.18 224.112-65.846l-1.631 0.985 256.86 256.86zM212.36 619.64c-52.114-52.117-84.346-124.114-84.346-203.64 0-159.058 128.942-288 288-288s288 128.942 288 288c0 159.058-128.942 288-288 288-0.005 0-0.010-0-0.014-0l0.001 0c-0.242 0.001-0.529 0.001-0.815 0.001-79.271 0-151.010-32.251-202.811-84.348l-0.013-0.014z\",\n            \"M224 352h384v128h-384v-128z\",\n            \"M352 224h128v384h-128v-384z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-magnify-in-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 19,\n          \"paths\": [\n            \"M767.2 639.2c42.4-66.6 65-144 64.8-223.2 0-229.8-186.2-416-416-416s-416 186.2-416 416 186.2 416 416 416c79 0.2 156.4-22.4 223.2-64.8l256.8 256.8 128-128-256.8-256.8zM619.6 619.6c-54 54-127.2 84.4-203.6 84.4-76.4 0.2-149.8-30.2-203.6-84.4-112.4-112.4-112.4-294.8 0-407.2s294.8-112.4 407.2 0c112.4 112.4 112.4 294.8 0 407.2z\",\n            \"M224 352h384v128h-384v-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-magnify-out-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 17,\n          \"paths\": [\n            \"M0 128h1024v128h-1024v-128z\",\n            \"M0 448h1024v128h-1024v-128z\",\n            \"M0 768h1024v128h-1024v-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-menu-v2.2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 89,\n          \"paths\": [\n            \"M293.4 512l218.6-218.6 256 256v-421.4c0-70.4-57.6-128-128-128h-512c-70.4 0-128 57.6-128 128v512c0 70.4 57.6 128 128 128h421.4l-256-256z\",\n            \"M1024 448h-128v320l-384-384-128 128 384 384h-320v128h576z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-move\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 127,\n          \"paths\": [\n            \"M448 0v128h320l-384 384 128 128 384-384v320h128v-576z\",\n            \"M576 674.274v157.382c-0.1 0.118-0.226 0.244-0.344 0.344h-383.312c-0.118-0.1-0.244-0.226-0.344-0.344v-383.312c0.1-0.118 0.226-0.244 0.344-0.344h157.382l192-192h-349.726c-105.6 0-192 86.4-192 192v384c0 105.6 86.4 192 192 192h384c105.6 0 192-86.4 192-192v-349.726l-192 192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-new-window\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 63,\n          \"paths\": [\n            \"M544 224v224c0 88.4-71.6 160-160 160s-160-71.6-160-160v-97.2l-197.4 196.4c-50 50-12.4 215.2 112.4 340s290 162.4 340 112.4l417-423.6-352-352z\",\n            \"M896 1024c70.6 0 128-57.4 128-128 0-108.6-128-192-128-192s-128 83.4-128 192c0 70.6 57.4 128 128 128z\",\n            \"M384 512c-35.4 0-64-28.6-64-64v-384c0-35.4 28.6-64 64-64s64 28.6 64 64v384c0 35.4-28.6 64-64 64z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-paint-bucket-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 117,\n          \"paths\": [\n            \"M922.344 101.68c-38.612-38.596-81.306-69.232-120.304-86.324-68.848-30.25-104.77-9.078-120.194 6.344l-516.228 516.216-3.136 9.152-162.482 476.932 485.998-165.612 6.73-6.806 509.502-509.506c9.882-9.866 21.768-27.77 21.768-56.578 0.002-50.71-38.996-121.148-101.654-183.818zM237.982 855.66l-69.73-69.728 69.25-203.228 18.498-6.704h64v128h128v64l-6.846 18.506-203.172 69.154z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pencil\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 72,\n          \"paths\": [\n            \"M922.4 101.6c-38.6-38.6-81.4-69.2-120.4-86.2-68.8-30.2-104.8-9-120.2 6.4l-516.2 516.2-3.2 9.2-162.4 476.8 486-165.6 516.2-516.4c9.8-9.8 21.8-27.8 21.8-56.6 0-50.6-39-121-101.6-183.8zM238 855.6l-69.8-69.6 69.2-203.2 18.4-6.8h64v128h128v64l-6.8 18.6-203 69z\",\n            \"M0 0v512l128-128v-256h256l128-128z\",\n            \"M1024 1024v-512l-128 128v256h-256l-128 128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pencil-edit-in-place\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 90,\n          \"paths\": [\n            \"M1024 512l-1024 512v-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-play\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 88,\n          \"paths\": [\n            \"M126-2h256v1024h-256v-1024z\",\n            \"M638-2h256v1024h-256v-1024z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pause\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 13,\n          \"paths\": [\n            \"M255.8 704c0.2 0 0.2 0 0 0l0.2-128c0-70.6 57.4-128 128-128h255.8c0 0 0 0 0.2-0.2v-127.8c0-70.6 57.4-128 128-128h143.6c-93.8-117-238-192-399.6-192-282.8 0-512 229.2-512 512 0 68 13.2 132.8 37.2 192h218.6z\",\n            \"M768.2 320c-0.2 0-0.2 0 0 0l-0.2 128c0 70.6-57.4 128-128 128h-255.8c0 0 0 0-0.2 0.2v127.8c0 70.6-57.4 128-128 128h-143.6c93.8 117 238 192 399.6 192 282.8 0 512-229.2 512-512 0-68-13.2-132.8-37.2-192h-218.6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plot-resource\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 80,\n          \"paths\": [\n            \"M766 1024l-256-512 256-512h-256l-256 512 256 512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pointer-left\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 81,\n          \"paths\": [\n            \"M254 0l256 512-256 512h256l256-512-256-512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pointer-right\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 37,\n          \"paths\": [\n            \"M1024 460.8v-460.8l-175.8 175.8c-85.2-69.6-190.8-107.6-302-107.6-127.6 0-247.6 49.8-338 140s-140 210.4-140 338 49.8 247.6 140 338 210.4 140 338 140 247.6-49.8 338-140c74-74 120.8-167.8 135-269.6h-138.6c-32 155.4-169.8 272.8-334.6 272.8-188.2 0-341.4-153.2-341.4-341.4s153.4-341.2 341.6-341.2c76.8 0 147.6 25.4 204.8 68.2l-187.8 187.8h460.8z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-refresh-v1.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 71,\n          \"paths\": [\n            \"M192.2 576c-0.2 0-0.2 0 0 0l-0.2 448h640v-447.8c0 0 0 0-0.2-0.2h-639.6z\",\n            \"M978.8 210.8l-165.4-165.4c-25-25-74.2-45.4-109.4-45.4h-576c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128v-448c0-35.2 28.8-64 64-64h640c35.2 0 64 28.8 64 64v448c70.4 0 128-57.6 128-128v-576c0-35.2-20.4-84.4-45.2-109.2zM704 256c0 35.2-28.8 64-64 64h-448c-35.2 0-64-28.8-64-64v-192h320v192h128v-192h128v192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-save-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 70,\n          \"paths\": [\n            \"M978.8 338.8l-64-64c24.8 24.8 45.2 74 45.2 109.2v448c0 70.4-57.6 128-128 128h-640c-18.8 0-36.6-4.2-52.6-11.4 20.2 44.4 65 75.4 116.6 75.4h640c70.4 0 128-57.6 128-128v-448c0-35.2-20.4-84.4-45.2-109.2z\",\n            \"M704 896v-319.8c0 0 0 0-0.2-0.2h-511.6l-0.2 320h512z\",\n            \"M192 512h512c35.2 0 64 28.8 64 64v320c70.4 0 128-57.6 128-128v-448c0-35.2-20.4-84.4-45.2-109.2l-165.4-165.4c-25-25-74.2-45.4-109.4-45.4h-448c-70.4 0-128 57.6-128 128v640c0 70.4 57.6 128 128 128v-320c0-35.2 28.8-64 64-64zM128 64h192v192h128v-192h128v192c0 35.2-28.8 64-64 64h-320c-35.2 0-64-28.8-64-64v-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-save-as\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 68,\n          \"paths\": [\n            \"M1024 512c-1.8-7.2-3.4-14.4-5.2-21.8-20.2-86.2-53.4-209.4-98.4-307.2-22.4-49-45.4-86.6-70.2-115.2-48.6-56-98.4-67.8-131.8-67.8-33.2 0-83.2 11.8-131.8 67.8-24.6 28.6-47.6 66.2-70 115.2-44.8 97.8-78.2 221-98.4 307.2-21.8 93-46.6 175.4-72 238.4-16.4 40.6-30.4 66.4-40.8 82.8-10.4-16.2-24.4-42.2-40.8-82.8-23.2-58-46.2-132.4-66.6-216.6h-198c1.8 7.2 3.4 14.4 5.2 21.8 20.2 86.2 53.4 209.4 98.4 307.2 22.4 49 45.4 86.6 70.2 115.2 48.6 56 98.6 67.8 131.8 67.8s83.2-11.8 131.8-67.8c24.8-28.6 47.6-66.2 70.2-115.2 44.8-97.8 78.2-221 98.4-307.2 21.8-93 46.6-175.4 72-238.4 16.4-40.6 30.4-66.4 40.8-82.8 10.4 16.2 24.4 42.2 40.8 82.8 23.4 57.8 46.4 132.4 66.8 216.4h197.6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-sine\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 5,\n          \"paths\": [\n            \"M800 1024h224l-384-1024h-256l-384 1024h224l84-224h408zM380 608l132-352 132 352z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-font\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 87,\n          \"paths\": [\n            \"M448 382c0 35.2-28.8 64-64 64h-320c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h320c35.2 0 64 28.8 64 64v320z\",\n            \"M1024 382c0 35.2-28.8 64-64 64h-320c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h320c35.2 0 64 28.8 64 64v320z\",\n            \"M448 958c0 35.2-28.8 64-64 64h-320c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h320c35.2 0 64 28.8 64 64v320z\",\n            \"M1024 958c0 35.2-28.8 64-64 64h-320c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h320c35.2 0 64 28.8 64 64v320z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-thumbs-strip\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 99,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM128 128h320v768h-320v-768zM896 896h-320v-768h320v768z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-two-parts-both\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 98,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM896 896h-320v-768h320v768z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-two-parts-one-only\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 40,\n          \"paths\": [\n            \"M795.2 164.8c-79.8-65.2-178.8-100.8-283.2-100.8-119.6 0-232.2 46.6-316.8 131.2-69.4 69.4-113.2 157.4-126.6 252.8h130c29.6-145.8 158.8-256 313.4-256 72 0 138.4 23.8 192 64l-176 176h432v-432l-164.8 164.8z\",\n            \"M512 832c-72 0-138.4-23.8-192-64l176-176h-432v432l164.8-164.8c79.8 65.2 178.8 100.8 283.2 100.8 119.6 0 232.2-46.6 316.8-131.2 69.4-69.4 113.2-157.4 126.6-252.8h-130c-29.6 145.8-158.8 256-313.4 256z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-resync\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 36,\n          \"paths\": [\n            \"M460.8 460.8l-187.8-187.8c57.2-42.8 128-68.2 204.8-68.2 188.2 0 341.6 153.2 341.6 341.4s-153.2 341.2-341.4 341.2c-165 0-302.8-117.6-334.6-273h-138.4c14.2 101.8 61 195.6 135 269.6 90.2 90.2 210.4 140 338 140s247.6-49.8 338-140 140-210.4 140-338-49.8-247.6-140-338-210.4-140-338-140c-111.4 0-217 38-302 107.6l-176-175.6v460.8h460.8z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-reset\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 65,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM832 704l-128 128-192-192-192 192-128-128 192-192-192-192 128-128 192 192 192-192 128 128-192 192 192 192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-x-in-circle\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 38,\n          \"paths\": [\n            \"M253.414 318.061l-155.172-116.384c-50.233 66.209-85.127 146.713-97.91 234.39l-0.333 2.781 191.919 27.434c8.145-56.552 29.998-106.879 62.068-149.006l-0.573 0.784z\",\n            \"M191.98 557.717l-191.919 27.434c13.115 90.459 48.009 170.963 99.174 238.453l-0.931-1.281 155.111-116.384c-31.476-41.347-53.309-91.675-61.231-146.504l-0.204-1.719z\",\n            \"M466.283 191.98l-27.434-191.919c-90.459 13.115-170.963 48.009-238.453 99.174l1.281-0.931 116.384 155.111c41.347-31.476 91.675-53.309 146.504-61.231l1.719-0.204z\",\n            \"M822.323 98.242c-66.209-50.233-146.713-85.127-234.39-97.91l-2.781-0.333-27.434 191.919c56.552 8.145 106.879 29.998 149.006 62.068l-0.784-0.573z\",\n            \"M832.020 466.283l191.919-27.434c-13.115-90.459-48.009-170.963-99.174-238.453l0.931 1.281-155.111 116.384c31.476 41.347 53.309 91.675 61.231 146.504l0.204 1.719z\",\n            \"M201.677 925.758c66.209 50.233 146.713 85.127 234.39 97.91l2.781 0.333 27.434-191.919c-56.552-8.145-106.879-29.998-149.006-62.068l0.784 0.573z\",\n            \"M770.586 705.939l155.131 116.343c50.233-66.209 85.127-146.713 97.91-234.39l0.333-2.781-191.919-27.434c-8.125 56.564-29.966 106.906-62.028 149.049l0.574-0.786z\",\n            \"M557.717 832.020l27.434 191.919c90.459-13.115 170.963-48.009 238.453-99.174l-1.281 0.931-116.384-155.111c-41.347 31.476-91.675 53.309-146.504 61.231l-1.719 0.204z\",\n            \"M770.586 512c0 142.813-115.773 258.586-258.586 258.586s-258.586-115.773-258.586-258.586c0-142.813 115.773-258.586 258.586-258.586s258.586 115.773 258.586 258.586z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-brightness\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 39,\n          \"paths\": [\n            \"M512 0c-282.78 0-512 229.24-512 512s229.22 512 512 512 512-229.24 512-512-229.22-512-512-512zM783.52 783.52c-69.111 69.481-164.785 112.481-270.502 112.481-0.358 0-0.716-0-1.074-0.001l0.055 0v-768c212.070 0.010 383.982 171.929 383.982 384 0 106.034-42.977 202.031-112.462 271.52l0-0z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-contrast\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 35,\n          \"paths\": [\n            \"M960 0c0 0 0 0 0 0h-320v128h165.4l-210.6 210.8c-25 25-25 65.6 0 90.6 12.4 12.4 28.8 18.8 45.2 18.8s32.8-6.2 45.2-18.8l210.8-210.8v165.4h128v-384h-64z\",\n            \"M896 805.4l-210.8-210.6c-25-25-65.6-25-90.6 0s-25 65.6 0 90.6l210.8 210.6h-165.4v128h384v-384h-128v165.4z\",\n            \"M218.6 128h165.4v-128h-320c0 0 0 0 0 0h-64v384h128v-165.4l210.8 210.8c12.4 12.4 28.8 18.8 45.2 18.8s32.8-6.2 45.2-18.8c25-25 25-65.6 0-90.6l-210.6-210.8z\",\n            \"M338.8 594.8l-210.8 210.6v-165.4h-128v384h384v-128h-165.4l210.8-210.8c25-25 25-65.6 0-90.6-25.2-24.8-65.6-24.8-90.6 0.2z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-expand\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 33,\n          \"paths\": [\n            \"M0 64h1024v128h-1024v-128z\",\n            \"M0 320h1024v128h-1024v-128z\",\n            \"M0 576h1024v128h-1024v-128z\",\n            \"M0 832h1024v128h-1024v-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-list-view\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 28,\n          \"paths\": [\n            \"M382 830h448v-448h-448v448zM510 510h192v192h-192v-192z\",\n            \"M-2 574h320v64h-320v-64z\",\n            \"M894 574h128v64h-128v-64z\",\n            \"M574-2h64v320h-64v-320z\",\n            \"M574 894h64v128h-64v-128z\",\n            \"M574 574h64v64h-64v-64z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grid-snap-to\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 29,\n          \"paths\": [\n            \"M768 576h192v64h-192v-64z\",\n            \"M256 576h192v64h-192v-64z\",\n            \"M0 576h192v64h-192v-64z\",\n            \"M640 512h-64v64h-64v64h64v64h64v-64h64v-64h-64z\",\n            \"M576 256h64v192h-64v-192z\",\n            \"M576 0h64v192h-64v-192z\",\n            \"M576 768h64v192h-64v-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grid-snap-no\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 26,\n          \"paths\": [\n            \"M0 64v896h1024v-896h-1024zM896 832h-768v-640h768v640z\",\n            \"M192 256h384v128h-384v-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-frame-show\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 25,\n          \"paths\": [\n            \"M128 190h420l104-128h-652v802.4l128-157.4z\",\n            \"M896 830h-420l-104 128h652v-802.4l-128 157.4z\",\n            \"M832-2l-832 1024h192l832-1024z\",\n            \"M392 382l104-128h-304v128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-frame-hide\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 23,\n          \"paths\": [\n            \"M832 192.4v639.4c0 0.2-0.2 0.2-0.4 0.4h-319.6v192h320c105.6 0 192-86.4 192-192v-640.2c0-105.6-86.4-192-192-192h-320v192h319.6c0.2 0 0.4 0.2 0.4 0.4z\",\n            \"M192 704v192l384-384-384-384v192h-192v384z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-import\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 24,\n          \"paths\": [\n            \"M192 831.66v-639.32l0.34-0.34h319.66v-192h-320c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h320v-192h-319.66z\",\n            \"M1024 512l-384-384v192h-192v384h192v192l384-384z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-export\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 4,\n          \"paths\": [\n            \"M1226.4 320h-176l-76.22 203.24 77 205.34 87.22-232.58 90.74 242h-174.44l49.5 132h174.44l57.76 154h154l-264-704z\",\n            \"M384 0l-384 1024h224l84-224h408l84 224h224l-384-1024zM380 608l132-352 132 352z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-font-size-alt1\"\n          ],\n          \"width\": 1504,\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 141,\n          \"paths\": [\n            \"M632 312l-120 120-120-120-80 80 120 120-120 120 80 80 120-120 120 120 80-80-120-120 120-120-80-80z\",\n            \"M512 0c-282.76 0-512 86-512 192v640c0 106 229.24 192 512 192s512-86 512-192v-640c0-106-229.24-192-512-192zM512 832c-176.731 0-320-143.269-320-320s143.269-320 320-320c176.731 0 320 143.269 320 320v0c0 176.731-143.269 320-320 320v0z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-clear-data\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 149,\n          \"paths\": [\n            \"M576 64c-247.4 0-448 200.6-448 448h-128l192 192 192-192h-128c0-85.4 33.2-165.8 93.8-226.2 60.4-60.6 140.8-93.8 226.2-93.8s165.8 33.2 226.2 93.8c60.6 60.4 93.8 140.8 93.8 226.2s-33.2 165.8-93.8 226.2c-60.4 60.6-140.8 93.8-226.2 93.8s-165.8-33.2-226.2-93.8l-90.6 90.6c81 81 193 131.2 316.8 131.2 247.4 0 448-200.6 448-448s-200.6-448-448-448z\",\n            \"M576 272c-26.6 0-48 21.4-48 48v211.8l142 142c9.4 9.4 21.6 14 34 14s24.6-4.6 34-14c18.8-18.8 18.8-49.2 0-67.8l-114-114v-172c0-26.6-21.4-48-48-48z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-history\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 158,\n          \"paths\": [\n            \"M643.427 825.261c-81.955-0.697-148.179-67.065-148.642-149.010l-0-0.044v-395.828l296.871 247.393v-197.914l-395.828-329.857-395.828 328.62v197.502l296.871-246.156v396.241c0 190.905 155.239 346.556 346.144 346.968l412.321 0.825 0.412-197.914z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"width\": 1056,\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-arrow-up-to-parent\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 159,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM783.6 783.6c-54.634 54.8-125.77 93.12-205.322 106.874l-2.278 0.326v-250.8h-128v250.8c-161.302-28.062-286.738-153.497-314.468-312.5l-0.332-2.3h250.8v-128h-250.8c28.062-161.302 153.497-286.738 312.5-314.468l2.3-0.332v250.8h128v-250.8c161.302 28.062 286.738 153.497 314.468 312.5l0.332 2.3h-250.8v128h250.8c-14.080 81.83-52.4 152.966-107.191 207.591l-0.009 0.009z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-crosshair-in-circle\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 161,\n          \"paths\": [\n            \"M512 384c70.692 0 128 57.308 128 128s-57.308 128-128 128c-70.692 0-128-57.308-128-128v0c0.114-70.647 57.353-127.886 127.989-128l0.011-0zM512 256c-141.385 0-256 114.615-256 256s114.615 256 256 256c141.385 0 256-114.615 256-256v0c-0.114-141.339-114.661-255.886-255.989-256l-0.011-0z\",\n            \"M512 128c211.87 0.128 383.575 171.912 383.575 383.8 0 211.967-171.833 383.8-383.8 383.8s-383.8-171.833-383.8-383.8c0-105.99 42.963-201.945 112.425-271.4l-0 0c69.21-69.437 164.944-112.401 270.713-112.401 0.312 0 0.624 0 0.936 0.001l-0.048-0zM512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-target\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 163,\n          \"paths\": [\n            \"M45.2 658.8h229.6l-274.8 274.6 90.6 90.6 274.6-274.8v229.6h128v-448h-448v128z\",\n            \"M1024 90.6l-90.6-90.6-274.6 274.8v-229.6h-128v448h448v-128h-229.6l274.8-274.6z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-items-collapse\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 162,\n          \"paths\": [\n            \"M448 896h-229.4l274.6-274.8-90.4-90.4-274.8 274.6v-229.4h-128v448h448v-128z\",\n            \"M530.8 402.8l90.4 90.4 274.8-274.6v229.4h128v-448h-448v128h229.4l-274.6 274.8z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-items-expand\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 164,\n          \"paths\": [\n            \"M256 512c0 70.692-57.308 128-128 128s-128-57.308-128-128c0-70.692 57.308-128 128-128s128 57.308 128 128z\",\n            \"M640 512c0 70.692-57.308 128-128 128s-128-57.308-128-128c0-70.692 57.308-128 128-128s128 57.308 128 128z\",\n            \"M1024 512c0 70.692-57.308 128-128 128s-128-57.308-128-128c0-70.692 57.308-128 128-128s128 57.308 128 128z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-3-dots\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 165,\n          \"paths\": [\n            \"M1024 384v-128h-256v-256h-128v256h-256v-256h-128v256h-256v128h256v256h-256v128h256v256h128v-256h256v256h128v-256h256v-128h-256v-256zM640 640h-256v-256h256z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grid-on\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 166,\n          \"paths\": [\n            \"M256 551.4l128-157.6v-9.8h8l104-128h-112v-256h-128v256h-256v128h256v167.4z\",\n            \"M184 640h-184v128h80l104-128z\",\n            \"M768 472.6l-128 157.6v9.8h-8l-104 128h112v256h128v-256h256v-128h-256v-167.4z\",\n            \"M840 384h184v-128h-80l-104 128z\",\n            \"M832 0l-832 1024h192l832-1024h-192z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-grid-off\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 167,\n          \"paths\": [\n            \"M896 256h-128l-128-256h-256l-128 256h-128c-70.601 0.227-127.773 57.399-128 127.978l-0 0.022v512c0.227 70.601 57.399 127.773 127.978 128l0.022 0h768c70.601-0.227 127.773-57.399 128-127.978l0-0.022v-512c-0.227-70.601-57.399-127.773-127.978-128l-0.022-0zM512 864c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256v0c0 141.385-114.615 256-256 256v0z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-camera\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 168,\n          \"paths\": [\n            \"M896 320v448c-0.215 70.606-57.394 127.785-127.979 128l-0.021 0h-576c0.215 70.606 57.394 127.785 127.979 128l0.021 0h576c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-448c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0z\",\n            \"M832 704v-448c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0h-192l-101.5-82.74c-24.88-24.9-74.040-45.26-109.24-45.26h-237.26c-35.305 0.102-63.898 28.695-64 63.99l-0 0.010v640c0.215 70.606 57.394 127.785 127.979 128l0.021 0h576c70.606-0.215 127.785-57.394 128-127.979l0-0.021zM128 644v-516l256 260z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-folders-collapse\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 188,\n          \"paths\": [\n            \"M832.4 128.6c22.8 0 38 11.8 45 19 7 7 19 22.4 19 45v640c0 22.8-11.8 38-19 45-7 7-22.4 19-45 19h-640c-22.8 0-38-11.8-45-19-7-7-19-22.4-19-45v-640c0-22.8 11.8-38 19-45 7-7 22.4-19 45-19h640zM832.4 0.6h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192v0z\",\n            \"M256.4 320.6h512v128h-512v-128z\",\n            \"M384.4 576.6h384v128h-384v-128z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-multiline\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 187,\n          \"paths\": [\n            \"M832.4 0.6h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM896.4 448.6h-512v128h512v256c0 22.8-11.8 38-19 45-7 7-22.4 19-45 19h-640c-22.8 0-38-11.8-45-19-7-7-19-22.4-19-45v-640c0-22.8 11.8-38 19-45 7-7 22.4-19 45-19h640c22.8 0 38 11.8 45 19 7 7 19 22.4 19 45v256z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-singleline\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 97,\n          \"paths\": [\n            \"M576 64h-256l320 320h-290.256c-44.264-76.516-126.99-128-221.744-128h-128v512h128c94.754 0 177.48-51.484 221.744-128h290.256l-320 320h256l448-448-448-448z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-activity\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 16,\n          \"paths\": [\n            \"M512 0c-214.8 0-398.8 132.4-474.8 320h90.8c56.8 0 108 24.8 143 64h241l-192-192h256l320 320-320 320h-256l192-192h-241c-35 39.2-86.2 64-143 64h-90.8c76 187.6 259.8 320 474.8 320 282.8 0 512-229.2 512-512s-229.2-512-512-512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-activity-mode\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 125,\n          \"paths\": [\n            \"M192 0c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h64v-1024h-64z\",\n            \"M384 0h256v1024h-256v-1024z\",\n            \"M832 0h-64v704h256v-512c0-105.6-86.4-192-192-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-autoflow-tabular\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 3,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM782 690c-12.8 22.2-36.6 36-62.4 36-12.6 0-25-3.4-36-9.6l-222-128.2c-0.8-0.4-1.6-1-2.4-1.4l-0.8-0.6-1.8-1.2-2.4-2-1.8-1.4-0.6-0.6c-0.8-0.6-1.4-1.2-2.2-1.8v0c-5-4.6-9.4-10-13-15.8-0.2-0.4-0.6-1-0.8-1.4s-0.6-1-0.8-1.4c-3.2-6-5.8-12.4-7.2-19.2v-0.2c-0.2-1-0.4-1.8-0.6-2.8 0-0.2 0-0.6-0.2-0.8-0.2-0.6-0.2-1.4-0.2-2.2s-0.2-1-0.2-1.6 0-1-0.2-1.6-0.2-1.6-0.2-2.2c0-0.4 0-0.6 0-1 0-1 0-1.8 0-2.8 0 0 0-0.2 0-0.4v-363.8c0-39.8 32.2-72 72-72s72 32.2 72 72v322.4l185.8 107.2c34.2 20 45.8 64 26 98.4z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-clock-v1.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 132,\n          \"paths\": [\n            \"M1024 192c0 106.039-229.23 192-512 192s-512-85.961-512-192c0-106.039 229.23-192 512-192s512 85.961 512 192z\",\n            \"M512 512c-282.77 0-512-85.962-512-192v512c0 106.038 229.23 192 512 192s512-85.962 512-192v-512c0 106.038-229.23 192-512 192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-database\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 131,\n          \"paths\": [\n            \"M683.52 819.286c-50.782 28.456-109.284 44.714-171.52 44.714-194.094 0-352-157.906-352-352s157.906-352 352-352 352 157.906 352 352c0 62.236-16.258 120.738-44.714 171.52l191.692 191.692c8.516-13.89 13.022-28.354 13.022-43.212v-640c0-106.038-229.23-192-512-192s-512 85.962-512 192v640c0 106.038 229.23 192 512 192 126.11 0 241.548-17.108 330.776-45.46l-159.256-159.254z\",\n            \"M352 512c0 88.224 71.776 160 160 160s160-71.776 160-160-71.776-160-160-160-160 71.776-160 160z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-database-query\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 57,\n          \"paths\": [\n            \"M896 192h-320c-16.4-16.4-96.8-96.8-109.2-109.2l-37.4-37.4c-25-25-74.2-45.4-109.4-45.4h-256c-35.2 0-64 28.8-64 64v384c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v-128c0-70.4-57.6-128-128-128z\",\n            \"M896 448h-768c-70.4 0-128 57.6-128 128v320c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-320c0-70.4-57.6-128-128-128zM320 896h-128v-320h128v320zM576 896h-128v-320h128v320zM832 896h-128v-320h128v320z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-dataset\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 67,\n          \"paths\": [\n            \"M1024 192c0 106.039-229.23 192-512 192s-512-85.961-512-192c0-106.039 229.23-192 512-192s512 85.961 512 192z\",\n            \"M512 512c-282.8 0-512-86-512-192v512c0 106 229.2 192 512 192s512-86 512-192v-512c0 106-229.2 192-512 192zM896 575v256c-36.6 15.6-79.8 28.8-128 39.4v-256c48.2-10.6 91.4-23.8 128-39.4zM256 614.4v256c-48.2-10.4-91.4-23.8-128-39.4v-256c36.6 15.6 79.8 28.8 128 39.4zM384 890v-256c41 4 83.8 6 128 6s87-2.2 128-6v256c-41 4-83.8 6-128 6s-87-2.2-128-6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-datatable\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 126,\n          \"paths\": [\n            \"M832 640c105.6 0 192-86.4 192-192v-256c0-105.6-86.4-192-192-192v320l-128-64-128 64v-320h-384c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-192c0 105.6-86.4 192-192 192h-640v-192h640z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-dictionary\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 77,\n          \"paths\": [\n            \"M896 192h-320c-16.4-16.4-96.8-96.8-109.2-109.2l-37.4-37.4c-25-25-74.2-45.4-109.4-45.4h-256c-35.2 0-64 28.8-64 64v384c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v-128c0-70.4-57.6-128-128-128z\",\n            \"M896 448h-768c-70.4 0-128 57.6-128 128v320c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-320c0-70.4-57.6-128-128-128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-folder-v2.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 100,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM896 896h-768v-768h768v768z\",\n            \"M320 256l-128 128v448h640v-320l-128-128-128 128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-image\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 130,\n          \"paths\": [\n            \"M448 0h-256c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h256v-1024z\",\n            \"M832 0h-256v577.664h448v-385.664c0-105.6-86.4-192-192-192z\",\n            \"M576 1024h256c105.6 0 192-86.4 192-192v-129.664h-448v321.664z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-layout\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 108,\n          \"paths\": [\n            \"M512 1024l512-320v-384l-512.020-320-511.98 320v384l512 320zM512 192l358.4 224-358.4 224-358.4-224 358.4-224z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-object\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 76,\n          \"paths\": [\n            \"M511.98 0l-511.98 320v384l512 320 512-320v-384l-512.020-320zM586.22 896h-141.36v-136.64h141.36v136.64zM721.040 497.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-object-unknown\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 15,\n          \"paths\": [\n            \"M512 0l-512 320v512c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-512l-512-320zM512 192l358.4 224-358.4 224-358.4-224 358.4-224z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-packet\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 74,\n          \"paths\": [\n            \"M704 512c-105.6 0-192-86.4-192-192v-320h-320c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-320h-320z\",\n            \"M768 384h256l-384-384v256c0 70.4 57.6 128 128 128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-page\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 44,\n          \"paths\": [\n            \"M830 0h-636c-106.7 0-194 87.3-194 194v406.82c14.18 18.64 25.66 28.34 32 30.84 14.28-5.62 54.44-47.54 92.96-146 42.46-108.38 116.32-237.66 227.040-237.66 52.4 0 101.42 29.16 145.7 86.68 37.34 48.5 64.84 108.92 81.34 151.080 38.52 98.38 78.68 140.3 92.96 146 14.28-5.62 54.44-47.54 92.96-146 42.46-108.48 116.32-237.76 227.040-237.76 11.355 0.003 22.389 1.366 32.952 3.936l-0.952-0.196v-57.74c0-106.7-87.3-194-194-194z\",\n            \"M992 392.34c-14.28 5.62-54.44 47.52-92.96 146-42.46 108.38-116.32 237.66-227.040 237.66-52.4 0-101.42-29.16-145.7-86.68-37.34-48.5-64.84-108.92-81.34-151.080-38.52-98.38-78.68-140.3-92.96-146-14.28 5.62-54.44 47.52-92.96 146-42.46 108.48-116.32 237.76-227.040 237.76-11.355-0.003-22.389-1.367-32.952-3.936l0.952 0.196v57.74c0 106.7 87.3 194 194 194h636c106.7 0 194-87.3 194-194v-406.82c-14.18-18.64-25.66-28.34-32-30.84z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plot-overlay\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 42,\n          \"paths\": [\n            \"M89.6 312c24.98 0 48.96-26.52 85.52-70.18 45.42-54.28 102-121.82 196-121.82 44.64 0 86.62 15.46 124.8 46 28.68 22.9 51.16 50.42 72.92 77.060 38.42 46.94 59.16 68.94 83.96 68.94h371.2v-118c0-106.7-87.3-194-194-194h-636c-106.7 0-194 87.3-194 194v118h89.6z\",\n            \"M529.5 410.4c-28.24-22.64-50.52-50-72-76.28-35.5-43.48-58.76-70.12-86.3-70.12-25.060 0-49.080 26.54-85.66 70.24-45.4 54.24-102 121.76-196 121.76h-89.54v112h371.2c44 0 85.54 15.34 123.3 45.6 28.24 22.64 50.52 50 72 76.28 35.5 43.48 58.76 70.12 86.3 70.12 25.060 0 49.080-26.54 85.66-70.24 45.4-54.24 102-121.76 196-121.76h89.54v-112h-371.2c-44.060 0-85.54-15.34-123.3-45.6z\",\n            \"M934.4 712c-24.98 0-48.96 26.52-85.52 70.18-45.42 54.28-102 121.82-196 121.82-44.64 0-86.62-15.46-124.8-46-28.68-22.9-51.16-50.42-72.92-77.060-38.42-46.94-59.16-68.94-83.96-68.94h-371.2v118c0 106.7 87.3 194 194 194h636c106.7 0 194-87.3 194-194v-118h-89.6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plot-stacked\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 14,\n          \"paths\": [\n            \"M635.6 524.4c6.6 4.2 13.2 8.6 19.2 13.6l120.4 96.4c29.6 23.8 83.8 23.8 113.4 0l135.2-108c0.2-4.8 0.2-9.4 0.2-14.2 0-52.2-7.8-102.4-22.2-149.8l-154.8 123.6c-58.2 46.6-140.2 59.2-211.4 38.4z\",\n            \"M248.6 634.2l120.4-96.4c58-46.4 140-59.2 211.2-38.4-6.6-4.2-13.2-8.6-19.2-13.6l-120.4-96.4c-29.6-23.8-83.8-23.8-113.4 0l-120.2 96.6c-40 32-91.4 48-143 48-21.6 0-43-2.8-63.8-8.4 0 0.6 0 1.2 0 1.6 5 3.4 10 6.8 14.6 10.6l120.4 96.4c29.8 23.8 83.8 23.8 113.4 0z\",\n            \"M120.6 378.2l120.4-96.4c80.2-64.2 205.6-64.2 285.8 0l120.4 96.4c29.6 23.8 83.8 23.8 113.4 0l181-144.8c-91.2-140.4-249.6-233.4-429.6-233.4-238.6 0-439.2 163.2-496 384.2 30.8 17.6 77.8 15.6 104.6-6z\",\n            \"M689 742l-120.4-96.4c-29.6-23.8-83.8-23.8-113.4 0l-120.2 96.4c-40 32-91.4 48-143 48-47.8 0-95.4-13.8-134.2-41.4 85.6 163.6 256.8 275.4 454.2 275.4s368.6-111.8 454.2-275.4c-80.4 57.4-199.8 55.2-277.2-6.6z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-session-v2.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 9,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM640 448h-256v-192h256v192zM384 512h256v192h-256v-192zM320 704h-256v-192h256v192zM320 256v192h-256v-192h256zM128 960c-17 0-33-6.6-45.2-18.8s-18.8-28.2-18.8-45.2v-128h256v192h-192zM384 960v-192h256v192h-256zM960 896c0 17-6.6 33-18.8 45.2s-28.2 18.8-45.2 18.8h-192v-192h256v128zM960 704h-256v-192h256v192zM960 448h-256v-192h256v192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabular\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 11,\n          \"paths\": [\n            \"M896 0h-768c-70.6 0.2-127.8 57.4-128 128v768c0.2 70.6 57.4 127.8 128 128h768c70.6-0.2 127.8-57.4 128-128v-768c-0.2-70.6-57.4-127.8-128-128zM64 256h256v192h-256v-192zM64 512h256v192h-256v-192zM128 960c-35.2-0.2-63.8-28.8-64-64v-128h256v192h-192zM384 960v-192h256v192h-256zM960 896c-0.2 35.2-28.8 63.8-64 64h-192v-192h256v128zM960 512v192h-576v-192h64v-64h-64v-192h576v192h-64v64h64z\",\n            \"M782.4 547.4l-110.4-55.2v-172.2c0-17.6-14.4-32-32-32s-32 14.4-32 32v211.8l145.6 72.8c15.8 8 35 1.6 43-14.4 8-15.6 1.6-35-14.2-42.8v0z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabular-lad\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 12,\n          \"paths\": [\n            \"M128 768v-576c-70.6 0.2-127.8 57.4-128 128v576c0.2 70.6 57.4 127.8 128 128h576c70.6-0.2 127.8-57.4 128-128h-576c-70.6-0.2-127.8-57.4-128-128z\",\n            \"M896 0h-576c-70.6 0.2-127.8 57.4-128 128v576c0.2 70.6 57.4 127.8 128 128h576c70.6-0.2 127.8-57.4 128-128v-576c-0.2-70.6-57.4-127.8-128-128zM256 192h192v128h-192v-128zM256 384h192v192h-192v-192zM320 768c-35.2-0.2-63.8-28.8-64-64v-64h192v128h-128zM512 768v-128h192v128h-192zM960 704c-0.2 35.2-28.8 63.8-64 64h-128v-128h192v64zM960 576h-448v-384h448v384z\",\n            \"M832 480c17.6 0 32-14.4 32-32 0-13.8-8.8-26-21.8-30.4l-74.2-24.6v-105c0-17.6-14.4-32-32-32s-32 14.4-32 32v151l117.8 39.2c3.4 1.2 6.8 1.8 10.2 1.8z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabular-lad-set\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 54,\n          \"paths\": [\n            \"M896 0h-768c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v768c0.215 70.606 57.394 127.785 127.979 128l0.021 0h768c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-768c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0zM448 292l25.060 25.32c7.916 7.922 18.856 12.822 30.94 12.822s23.023-4.9 30.94-12.822l0-0 75.5-76.3c29.97-30.338 71.571-49.128 117.56-49.128s87.59 18.79 117.544 49.112l0.016 0.016 50.44 50.98v152.2c-24.111-8.83-44.678-22.255-61.542-39.342l-0.018-0.018-75.5-76.3c-7.916-7.922-18.856-12.822-30.94-12.822s-23.023 4.9-30.94 12.822l-0 0-75.5 76.3c-29.971 30.343-71.575 49.137-117.568 49.137-20.084 0-39.331-3.584-57.137-10.146l1.145 0.369v-152.2zM320 960h-192c-35.26-0.214-63.786-28.74-64-63.98l-0-0.020v-128h256v192zM320 704h-256v-192h256v192zM320 448h-256v-192h256v192zM640 960h-256v-192h256v192zM448 636.62v-174.5c1.88 1.74 3.74 3.5 5.56 5.34l75.5 76.3c7.916 7.922 18.856 12.822 30.94 12.822s23.023-4.9 30.94-12.822l0-0 75.5-76.3c29.966-30.333 71.56-49.119 117.542-49.119 43.28 0 82.673 16.643 112.128 43.879l-0.11-0.1v174.5c-1.88-1.74-3.74-3.5-5.56-5.34l-75.5-76.3c-7.916-7.922-18.856-12.822-30.94-12.822s-23.023 4.9-30.94 12.822l-0 0-75.5 76.3c-29.966 30.333-71.56 49.119-117.542 49.119-43.28 0-82.673-16.643-112.128-43.879l0.11 0.1zM960 896c-0.214 35.26-28.74 63.786-63.98 64l-0.020 0h-192v-192h256v128z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabular-realtime-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 66,\n          \"paths\": [\n            \"M896 0h-768c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v768c0.215 70.606 57.394 127.785 127.979 128l0.021 0h768c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-768c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0zM768 256v192h-192v-192zM576 512h192v192h-192zM512 704h-192v-192h192zM512 256v192h-192v-192zM64 256h192v192h-192zM64 512h192v192h-192zM128 960c-35.255-0.225-63.775-28.745-64-63.978l-0-0.022v-128h192v192zM320 960v-192h192v192zM704 960h-128v-192h192v192zM941.14 941.14c-11.511 11.644-27.483 18.856-45.139 18.86l-64.001 0v-64h128c-0.004 17.657-7.216 33.629-18.854 45.134l-0.006 0.006zM960 768h-128v-512h128z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabular-scrolling\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 43,\n          \"paths\": [\n            \"M32 631.66c14.28-5.62 54.44-47.54 92.96-146 42.46-108.38 116.32-237.66 227.040-237.66 52.4 0 101.42 29.16 145.7 86.68 37.34 48.5 64.84 108.92 81.34 151.080 38.52 98.38 78.68 140.3 92.96 146 14.28-5.62 54.44-47.54 92.96-146 37.4-95.5 99.14-207.14 188.94-232.46-90.462-152.598-254.314-253.3-441.686-253.3-0.075 0-0.15 0-0.225 0l0.011-0c-282.76 0-512 229.24-512 512-0 0.032-0 0.070-0 0.108 0 35.719 3.641 70.587 10.572 104.254l-0.572-3.323c9.54 10.78 17.22 16.74 22 18.62z\",\n            \"M992 392.34c-14.28 5.62-54.44 47.52-92.96 146-42.46 108.38-116.32 237.66-227.040 237.66-52.4 0-101.42-29.16-145.7-86.68-37.34-48.5-64.84-108.92-81.34-151.080-38.52-98.38-78.68-140.3-92.96-146-14.28 5.62-54.44 47.52-92.96 146-37.4 95.5-99.14 207.14-188.94 232.46 90.462 152.598 254.314 253.3 441.686 253.3 0.075 0 0.15-0 0.225-0l-0.011 0c282.76 0 512-229.24 512-512 0-0.032 0-0.070 0-0.108 0-35.719-3.641-70.587-10.572-104.254l0.572 3.323c-9.54-10.78-17.22-16.74-22-18.62z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-telemetry-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 10,\n          \"paths\": [\n            \"M832 0h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM128 320v-128h256v128zM256 448h384v128h-384zM896 832h-448v-128h448zM896 576h-128v-128h128zM896 320h-384v-128h384z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-timeline\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 41,\n          \"paths\": [\n            \"M640 146.6v-82.58c0-35.346-28.654-64-64-64v0h-128c-35.346 0-64 28.654-64 64v0 82.58c-185.040 55.080-320 226.48-320 429.42 0 247.42 200.58 448 448 448s448-200.58 448-448c0-202.96-135-374.4-320-429.42zM532 596.020l-263.76 211c-57.105-59.935-92.24-141.25-92.24-230.772 0-0.080 0-0.16 0-0.24l-0 0.012c0-185.28 150.72-336 336-336 6.72 0 13.38 0.22 20 0.62v355.38z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-timer-v1.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 55,\n          \"paths\": [\n            \"M454.36 476.64l86.3-86.3c9.088-8.965 21.577-14.502 35.36-14.502s26.272 5.537 35.366 14.507l-0.006-0.006 86.3 86.3c19.328 19.358 42.832 34.541 69.047 44.082l1.313 0.418v-172.14l-57.64-57.64c-34.408-34.33-81.9-55.558-134.35-55.558s-99.943 21.228-134.354 55.562l0.004-0.004-86.3 86.3c-9.088 8.965-21.577 14.502-35.36 14.502s-26.272-5.537-35.366-14.507l0.006 0.006-28.68-28.66v172.14c19.045 7.022 41.040 11.084 63.984 11.084 52.463 0 99.966-21.239 134.379-55.587l-0.003 0.003z\",\n            \"M505.64 547.36l-86.3 86.3c-9.088 8.965-21.577 14.502-35.36 14.502s-26.272-5.537-35.366-14.507l0.006 0.006-86.3-86.3c-2-2-4.2-4-6.36-6v197.36c33.664 30.721 78.65 49.537 128.031 49.537 52.44 0 99.923-21.22 134.333-55.541l-0.004 0.004 86.3-86.3c9.088-8.965 21.577-14.502 35.36-14.502s26.272 5.537 35.366 14.507l-0.006-0.006 86.3 86.3c2 2 4.2 4 6.36 6v-197.36c-33.664-30.721-78.65-49.537-128.031-49.537-52.44 0-99.923 21.22-134.333 55.541l0.004-0.004z\",\n            \"M832 0h-128v192h127.66l0.34 0.34v639.32l-0.34 0.34h-127.66v192h128c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192z\",\n            \"M320 832h-127.66l-0.34-0.34v-639.32l0.34-0.34h127.66v-192h-128c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h128v-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-topic-v2.5\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 45,\n          \"paths\": [\n            \"M0 384h128v256h-128v-256z\",\n            \"M128 128.22l0.22-0.22h191.78v-128h-192c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v192h128v-191.78z\",\n            \"M128 895.78v-191.78h-128v192c0.215 70.606 57.394 127.785 127.979 128l0.021 0h192v-128h-191.78z\",\n            \"M384 0h256v128h-256v-128z\",\n            \"M896 895.78l-0.22 0.22h-191.78v128h192c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-192h-128v191.78z\",\n            \"M896 0h-192v128h191.78l0.22 0.22v191.78h128v-192c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0z\",\n            \"M896 384h128v256h-128v-256z\",\n            \"M384 896h256v128h-256v-128z\",\n            \"M256 256h512v512h-512v-512z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-box-with-dashed-lines-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 32,\n          \"paths\": [\n            \"M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM847.8 610.4l-82.6 143.2-189.6-131.6 19.2 230h-165.4l19.2-230-189.6 131.6-82.6-143.2 208.6-98.4-208.8-98.4 82.6-143.2 189.6 131.6-19.2-230h165.4l-19.2 230 189.6-131.6 82.6 143.2-208.6 98.4 208.8 98.4z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-summary-widget\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 30,\n          \"paths\": [\n            \"M896 110.8c0-79.8-55.4-127.4-123-105.4l-773 250.6h896v-145.2z\",\n            \"M896 320h-896v576c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-448c0-70.4-57.6-128-128-128zM832 832h-384v-320h384v320z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-notebook\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 0,\n          \"paths\": [\n            \"M0 896c0.227 70.601 57.399 127.773 127.978 128l0.022 0h768c70.601-0.227 127.773-57.399 128-127.978l0-0.022v-608h-512l-50.2-225.6c-7.6-34.2-42.6-62.4-77.8-62.4h-256c-70.601 0.227-127.773 57.399-128 127.978l-0 0.022zM832 768h-640v-256h640z\",\n            \"M480 0c35.2 0 70.2 28.2 77.8 62.4l36 161.6h430.2v-96c-0.227-70.601-57.399-127.773-127.978-128l-0.022-0z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-tabs-view\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 1,\n          \"paths\": [\n            \"M0 832c0 105.6 86.4 192 192 192h64v-576h-256z\",\n            \"M0 192v128h256v-320h-64c-105.6 0-192 86.4-192 192z\",\n            \"M768 1024h64c105.6 0 192-86.4 192-192v-128h-256z\",\n            \"M384 0h256v1024h-256v-1024z\",\n            \"M832 0h-64v576h256v-384c0-105.6-86.4-192-192-192z\"\n          ],\n          \"attrs\": [],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-flexible-layout\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": []\n          }\n        },\n        {\n          \"id\": 144,\n          \"paths\": [\n            \"M152 473.8c10.8-4.2 40.8-35.6 69.8-109.4 31.8-81.4 87.2-178.4 170.2-178.4 39.4 0 76 21.8 109.2 65 28 36.4 48.8 81.6 61 113.4 29 73.8 59 105.2 69.8 109.4 10.8-4.2 40.8-35.6 69.8-109.4s74.2-155.4 141.6-174.4c-67.89-114.467-190.82-190-331.391-190-0.003 0-0.007 0-0.010 0l0.001-0c-212 0-384 172-384 384 0.017 26.829 2.71 53.018 7.827 78.329l-0.427-2.529c7.2 8 13 12.6 16.6 14z\",\n            \"M884.6 477c7.235-27.919 11.392-59.972 11.4-92.995l0-0.005c-0.017-26.829-2.71-53.018-7.827-78.329l0.427 2.529c-7.2-8-13-12.6-16.6-14-10.8 4.2-40.8 35.6-69.8 109.4-21.8 55.8-54.6 119-100 153.2z\",\n            \"M512 640l135-59c-4.485 0.614-9.689 0.977-14.972 1l-0.028 0c-39.4 0-76-21.8-109.2-65-28-36.4-48.8-81.6-61-113.4-29-73.8-59-105.2-69.8-109.4-10.8 4.2-40.8 35.6-69.8 109.4-16.4 42.2-39.2 88.4-68.8 123.2z\",\n            \"M1024 480l-512 224-512-224v320l512 224 512-224v-320z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-generator-sine\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 143,\n          \"paths\": [\n            \"M320 192h384v64h-384v-64z\",\n            \"M320 448h384v64h-384v-64z\",\n            \"M320 320h320v64h-320v-64z\",\n            \"M256 128.2h512v399.8l128-56v-344c-0.227-70.601-57.399-127.773-127.978-128l-0.022-0h-512c-70.601 0.227-127.773 57.399-128 127.978l-0 0.022v344l128 56z\",\n            \"M658.2 576h-292.4l146.2 64 146.2-64z\",\n            \"M512 704l-512-224v320l512 224 512-224v-320l-512 224z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {},\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-generator-event\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {},\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 138,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512 0 226.4 147 418.4 350.6 486l257.4-486v503c236.8-45 416-253 416-503 0-282.8-229.2-512-512-512zM754.8 527.8c-58.967-68.597-145.842-111.772-242.8-111.772s-183.833 43.176-242.445 111.35l-0.355 0.422-146-125c8.6-10 17.4-19.6 26.8-28.8 92.628-92.679 220.619-150.006 362-150.006s269.372 57.326 361.997 150.003l0.003 0.003c9.4 9.2 18.2 18.8 26.8 28.8z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-gauge-v2\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 148,\n          \"paths\": [\n            \"M768 704h-512l102.4-179.2-358.4 51.2v254c0 106.6 87.4 194 194 194h636c106.8 0 194-87.4 194-194v-62l-325.8-186.2z\",\n            \"M830 0h-636c-106.6 0-194 87.2-194 194v318l400-60.2 112-195.8 109.8 192h402.2v-254c-0.227-107.052-86.948-193.773-193.978-194l-0.022-0z\",\n            \"M1024 640v-64l-384-64 384 128z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-spectra\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 147,\n          \"paths\": [\n            \"M512 256l109.8 192h398.2c-31.4-252.6-247-448-508-448-282.8 0-512 229.2-512 512l400-60.2z\",\n            \"M768 704h-512l102.4-179.2-354.4 50.6c31.2 252.8 246.8 448.6 508 448.6 201.6 0 376-116.6 459.6-286l-273.4-156.2z\",\n            \"M640 512l384 128v-64l-384-64z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-telemetry-spectra\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 146,\n          \"paths\": [\n            \"M370.2 459.4c9.326 8.53 19.666 16.261 30.729 22.914l0.871 0.486c-11.077-19.209-17.664-42.221-17.8-66.76l-0-0.040c0-39.6 17.8-77.6 50.2-107.4 37-34 87.4-52.6 141.8-52.6 40.2 0 78.2 10.2 110.2 29.2-8.918-15.653-19.693-29.040-32.268-40.482l-0.132-0.118c-37-34-87.4-52.6-141.8-52.6s-104.8 18.6-141.8 52.6c-32.4 29.8-50.2 67.8-50.2 107.4s17.8 77.6 50.2 107.4z\",\n            \"M885.4 269.6c-40.6-154.6-192.4-269.6-373.4-269.6s-332.8 115-373.4 269.6c-86 80-138.6 187.8-138.6 306.4 0 247.4 229.2 448 512 448s512-200.6 512-448c0-118.6-52.6-226.4-138.6-306.4zM512 128c141.2 0 256 100.4 256 224s-114.8 224-256 224-256-100.4-256-224 114.8-224 256-224zM512 832c-175.4 0-318.4-127.8-320-285.4 68.8 94.8 186.4 157.4 320 157.4s251.2-62.6 320-157.4c-1.6 157.6-144.6 285.4-320 285.4z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-pushbutton\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 151,\n          \"paths\": [\n            \"M512 0c-282.76 0-512 229.24-512 512s229.24 512 512 512 512-229.24 512-512-229.24-512-512-512zM512 768l-384-256 384-256 384 256z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-conditional\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 154,\n          \"paths\": [\n            \"M832 0h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM512 768l-384-256 384-256 384 256z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-condition-widget\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 155,\n          \"paths\": [\n            \"M535.6 530.6c-8.4 1.6-17.2 3-26.2 4s-18.2 2.4-27.2 4c-10.196 1.861-18.808 4.010-27.21 6.633l1.61-0.433c-8.609 2.674-16.105 6.348-22.89 10.987l0.29-0.187c-6.693 4.517-12.283 10.107-16.663 16.585l-0.137 0.215c-4.6 6.8-7.4 15.6-8.8 26s-0.4 18.4 2.4 25.2c2.746 6.688 7.224 12.195 12.881 16.122l0.119 0.078c5.967 4.053 13.057 6.94 20.704 8.161l0.296 0.039c7.592 1.527 16.319 2.4 25.25 2.4 0.123 0 0.246-0 0.369-0l-0.019 0c22.2 0 39.6-3.6 52.6-11s23.2-16.2 30.2-26.4c6.273-8.873 11.271-19.191 14.426-30.285l0.174-0.715c1.853-6.809 3.601-15.41 4.855-24.169l0.145-1.231 5.2-41.6c-5.4 4.217-11.723 7.564-18.583 9.689l-0.417 0.111c-6.489 2.241-14.362 4.255-22.444 5.662l-0.956 0.138z\",\n            \"M1024 384v-192h-152l24-192h-192l-24 192h-256l24-192h-192l-24 192h-232v192h208l-32 256h-176v192h152l-24 192h192l24-192h256l-24 192h192l24-192h232v-192h-208l32-256zM702.8 411.8l-26.4 211.8c-2.231 15.809-3.537 34.122-3.6 52.727l-0 0.073c0 16.8 2.2 29.4 6.4 37.8h-113.4c-1.342-5.556-2.338-12.122-2.781-18.84l-0.019-0.36c-0.261-3.524-0.409-7.634-0.409-11.778 0-2.962 0.076-5.907 0.226-8.832l-0.017 0.41c-18.663 17.401-41.395 30.694-66.597 38.289l-1.203 0.311c-22.627 6.956-48.639 10.974-75.586 11l-0.014 0c-0.764 0.011-1.666 0.018-2.569 0.018-18.098 0-35.598-2.563-52.156-7.345l1.325 0.328c-15.991-4.512-29.851-12.090-41.545-22.122l0.145 0.122c-11.233-9.982-19.792-22.733-24.624-37.192l-0.176-0.608c-5.2-15.2-6.4-33.4-3.8-54.4s9.4-42.2 19.4-57.2c9.524-14.399 21.535-26.346 35.532-35.512l0.468-0.288c13.387-8.662 28.922-15.533 45.512-19.765l1.088-0.235c13.436-3.792 30.801-7.554 48.47-10.41l2.93-0.39c17-2.6 33.8-4.6 50.4-6.2 16.628-1.527 31.69-4.070 46.349-7.643l-2.149 0.443c13-3 23.6-7.6 31.6-13.6s12.6-15 13.6-26.4 0.8-21.8-2.4-28.8c-2.849-6.902-7.542-12.56-13.468-16.517l-0.132-0.083c-6.217-4.011-13.604-6.78-21.543-7.774l-0.257-0.026c-7.897-1.277-17-2.007-26.274-2.007-0.537 0-1.073 0.002-1.609 0.007l0.082-0.001c-22 0-40 4.6-53.8 14.2s-23 25.2-28 47.2h-111.8c4.8-26.2 14.2-48 27.8-65.4 13.475-16.978 29.89-30.968 48.574-41.377l0.826-0.423c18.192-10.038 39.297-17.806 61.619-22.175l1.381-0.225c20.488-4.162 44.053-6.563 68.171-6.6l0.029-0c21.8 0.005 43.239 1.532 64.222 4.479l-2.422-0.279c20.641 2.809 39.324 8.783 56.401 17.461l-1.001-0.461c15.909 8.108 28.858 20.031 37.967 34.601l0.233 0.399c9 15 12.2 34.8 9 59.6z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-alphanumeric\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 156,\n          \"paths\": [\n            \"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM783.6 783.6c-69.581 69.675-165.757 112.776-272 112.776-212.298 0-384.4-172.102-384.4-384.4s172.102-384.4 384.4-384.4c212.298 0 384.4 172.102 384.4 384.4 0 0.008-0 0.017-0 0.025l0-0.001c0.001 0.264 0.001 0.575 0.001 0.887 0 105.769-42.964 201.503-112.391 270.703l-0.010 0.010z\",\n            \"M704 384l-128 128-192-192-192 192c0 176.731 143.269 320 320 320s320-143.269 320-320v0z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-image-telemetry\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 170,\n          \"paths\": [\n            \"M78 395.44c14-41.44 37.48-100.8 69.2-148.36 38.62-57.78 82.38-87.080 130.14-87.080s91.5 29.3 130 87.080c31.72 47.56 55.14 106.92 69.2 148.36 30.88 90.96 63.12 134.98 78 146.54 14.94-11.56 47.2-55.58 78-146.54 14-41.44 37.48-100.8 69.22-148.36q27.8-41.7 59.12-63.5c-75.7-111.377-201.81-183.58-344.783-183.58-0.034 0-0.068 0-0.103 0l0.006-0c-229.76 0-416 186.24-416 416-0 0.071-0 0.156-0 0.24 0 39.119 5.396 76.977 15.484 112.871l-0.704-2.931c16.78-21.74 40.4-63.34 63.22-130.74z\",\n            \"M754 436.56c-14 41.44-37.48 100.8-69.2 148.36-38.56 57.78-82.32 87.080-130 87.080s-91.5-29.3-130-87.080c-31.72-47.56-55.14-106.92-69.2-148.36-30.88-90.96-63.14-134.98-78-146.54-14.94 11.56-47.2 55.58-78 146.54-14.38 41.44-37.8 100.8-69.6 148.36q-27.8 41.7-59.12 63.5c75.7 111.378 201.81 183.58 344.783 183.58 0.119 0 0.237-0 0.356-0l-0.019 0c229.76 0 416-186.24 416-416 0-0.071 0-0.156 0-0.24 0-39.119-5.396-76.977-15.484-112.871l0.704 2.931c-16.78 21.74-40.4 63.34-63.22 130.74z\",\n            \"M921.56 334.62c4.098 24.449 6.44 52.617 6.44 81.332 0 0.017-0 0.034-0 0.051l0-0.003c0 0.095 0 0.208 0 0.32 0 282.593-229.087 511.68-511.68 511.68-0.113 0-0.225-0-0.338-0l0.018 0c-0.014 0-0.031 0-0.048 0-28.716 0-56.884-2.342-84.325-6.845l2.993 0.405c72.483 63.623 168.109 102.44 272.802 102.44 0.203 0 0.406-0 0.61-0l-0.031 0c229.76 0 416-186.24 416-416 0-0.172 0-0.375 0-0.578 0-104.692-38.817-200.319-102.844-273.271l0.404 0.47z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-telemetry-aggregate\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 172,\n          \"paths\": [\n            \"M832 0h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM267.64 896h-139.64v-448h139.64zM477.1 896h-139.64v-768h139.64zM686.54 896h-139.64v-320h139.64zM896 896h-139.64v-640h139.64z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-bar-graph\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 171,\n          \"paths\": [\n            \"M896 65.4l-128 62.6v896l128-62.6c70.4-34.42 128-120.2 128-190.6v-640c0-70.4-57.6-99.82-128-65.4z\",\n            \"M320 912l387.2 96.8v-896l-387.2-96.8v896z\",\n            \"M259.2 0.8l-3.2-0.8-128 62.6c-70.4 34.42-128 120.2-128 190.6v640c0 70.4 57.6 99.82 128 65.4l128-62.6 3.2 0.8z\"\n          ],\n          \"attrs\": [\n            {},\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-map\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 174,\n          \"paths\": [\n            \"M256 192v-64c0.215-70.606 57.394-127.785 127.979-128l0.021-0h256c70.606 0.215 127.785 57.394 128 127.979l0 0.021v64z\",\n            \"M832 128v128h-640v-128c-105.6 0-192 86.4-192 192v512c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-512c0-105.6-86.4-192-192-192zM128 576v-128h256v128zM640 832h-384v-128h384zM896 832h-128v-128h128zM896 576h-384v-128h384z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plan\"\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 175,\n          \"paths\": [\n            \"M896 0h-768c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v768c0.215 70.606 57.394 127.785 127.979 128l0.021 0h768c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-768c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0zM426.94 533.46c-8.054 15.864-24.249 26.545-42.938 26.545-7.823 0-15.209-1.871-21.734-5.191l0.273 0.126-154.54-77.28v-221.66c0-26.51 21.49-48 48-48s48 21.49 48 48v0 162.34l101.46 50.72c15.864 8.054 26.545 24.249 26.545 42.938 0 7.823-1.871 15.209-5.191 21.734l0.126-0.273zM896 896h-320v-128h320zM896 704h-320v-128h320zM896 512h-320v-128h320zM896 320h-320v-128h320z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-timelist\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 185,\n          \"paths\": [\n            \"M192 0c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM128 352c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96v0c-53.019 0-96-42.981-96-96v0zM288 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 640c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 320c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM800 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0z\"\n          ],\n          \"attrs\": [\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-plot-scatter\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {}\n            ]\n          }\n        },\n        {\n          \"id\": 184,\n          \"paths\": [\n            \"M896 110.72c0-79.9-55.38-127.32-123.080-105.36l-772.92 250.64h896v-145.28z\",\n            \"M896 320h-896v576c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-448c0-70.4-57.6-128-128-128zM256 832h-128v-128h128v128zM256 640h-128v-128h128v128zM896 832h-512v-128h512v128zM896 640h-512v-128h512v128z\"\n          ],\n          \"attrs\": [\n            {},\n            {}\n          ],\n          \"isMulticolor\": false,\n          \"isMulticolor2\": false,\n          \"grid\": 16,\n          \"tags\": [\n            \"icon-notebook-restricted\"\n          ],\n          \"colorPermutations\": {\n            \"12552552551\": [\n              {},\n              {}\n            ]\n          }\n        }\n      ],\n      \"invisible\": false,\n      \"colorThemes\": [\n        [\n          [\n            0,\n            0,\n            0,\n            1\n          ],\n          [\n            255,\n            255,\n            255,\n            1\n          ]\n        ]\n      ],\n      \"colorThemeIdx\": 0\n    }\n  ],\n  \"preferences\": {\n    \"showGlyphs\": true,\n    \"showCodes\": true,\n    \"showQuickUse\": true,\n    \"showQuickUse2\": true,\n    \"showSVGs\": true,\n    \"fontPref\": {\n      \"prefix\": \"openmct-symbols-\",\n      \"metadata\": {\n        \"fontFamily\": \"Open-MCT-Symbols-16px\",\n        \"majorVersion\": 5,\n        \"minorVersion\": 1,\n        \"designer\": \"Charles Hacskaylo\",\n        \"description\": \"Change to 5% baseline height\"\n      },\n      \"metrics\": {\n        \"emSize\": 1024,\n        \"baseline\": 10,\n        \"whitespace\": 0\n      },\n      \"embed\": false,\n      \"noie8\": true,\n      \"ie7\": false,\n      \"resetPoint\": 59904,\n      \"showSelector\": true,\n      \"showVersion\": true,\n      \"autoHost\": false,\n      \"selector\": \"\",\n      \"classSelector\": \".icon\",\n      \"showMetrics\": true,\n      \"showMetadata\": true\n    },\n    \"imagePref\": {\n      \"prefix\": \"icon-\",\n      \"png\": true,\n      \"useClassSelector\": true,\n      \"color\": 0,\n      \"bgColor\": 16777215\n    },\n    \"historySize\": 50,\n    \"gridSize\": 16,\n    \"quickUsageToken\": {\n      \"OpenMCTSymbols\": \"ZTA5ZDc2NTE1MTc5NWM5Njk2NGE1MmQ0NTNiYjI1MmIjMSMxNTY1Mzc3OTA4IyMj\"\n    }\n  },\n  \"uid\": -1\n}"
  },
  {
    "path": "src/styles/notebook.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/*********************************************** NOTEBOOK */\n@mixin searchHighlight {\n  background: rgba($colorBtnSelectedBg, 0.4);\n  color: $colorBtnSelectedFg;\n  font-weight: bold;\n}\n\n.c-notebook {\n  $headerFontSize: 1.3em;\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 auto;\n  overflow: hidden;\n  height: 100%;\n\n  /****************************** CONTENT */\n  &__body {\n    // Holds __nav and __page-view\n    display: flex;\n    flex: 1 1 auto;\n    overflow: hidden;\n  }\n\n  &__nav {\n    flex: 0 0 auto;\n\n    * {\n      overflow: hidden;\n    }\n  }\n\n  .c-sidebar {\n    .c-sidebar__pane {\n      flex-basis: 50%;\n    }\n  }\n\n  body.mobile & {\n    .c-list-button,\n    &-snapshot-menubutton {\n      display: none;\n    }\n  }\n\n  /****************************** CONTENT */\n  &__contents {\n    width: 70%;\n  }\n\n  &__page-view {\n    // Holds __header, __drag-area and __entries\n    display: flex;\n    flex: 1 1 auto;\n    flex-direction: column;\n    width: 100%;\n\n    > * {\n      flex: 0 0 auto;\n\n      + * {\n        margin-top: $interiorMargin;\n      }\n    }\n  }\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__head,\n  &__controls,\n  &__drag-area,\n  &__entries {\n    display: flex;\n    flex-wrap: nowrap;\n  }\n\n  &__head,\n  &__drag-area,\n  &__controls {\n    flex: 0 0 auto;\n  }\n\n  &__head {\n    [class*='__'] + [class*='__'] {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  &__search {\n    flex: 1 1 auto;\n  }\n\n  &__page-locked-message,\n  &__drag-area {\n    border-radius: $controlCr;\n    padding: 10px;\n\n    &:before {\n      margin-right: 7px !important;\n    }\n  }\n\n  &__drag-area {\n    background: rgba($colorKey, 0.02);\n    border: 1px dashed rgba($colorKey, 0.7);\n    color: $colorKey;\n    cursor: pointer;\n    justify-content: center;\n\n    [class*='__label'] {\n      font-style: italic;\n      @include ellipsize();\n    }\n\n    &:hover {\n      background: rgba($colorBodyFg, 0.2);\n    }\n\n    &.drag-active,\n    &.is-active {\n      // Not currently working\n      border-color: $colorKey;\n    }\n\n    body.mobile & {\n      display: none;\n    }\n  }\n\n  /***** PAGE VIEW */\n  &__page-view {\n    &__header {\n      display: flex;\n      flex-wrap: wrap; // Allows wrapping in mobile portrait and narrow placements\n      line-height: 220%;\n\n      > * {\n        flex: 0 0 auto;\n      }\n    }\n\n    &__path {\n      flex: 1 1 auto;\n      margin: 0 $interiorMargin;\n      overflow: hidden;\n      white-space: nowrap;\n      font-size: $headerFontSize;\n\n      > * {\n        // Section\n        flex: 0 0 auto;\n\n        + * {\n          // Page\n          display: inline;\n          flex: 1 1 auto;\n          @include ellipsize();\n        }\n      }\n    }\n  }\n\n  &__entries {\n    flex-direction: column;\n    flex: 1 1 auto;\n    overflow-x: hidden;\n    overflow-y: scroll;\n\n    @include desktop() {\n      padding-right: $interiorMarginSm; // Scrollbar kickoff\n    }\n\n    [class*='__entry'] + [class*='__entry'] {\n      margin-top: $interiorMarginSm;\n    }\n\n    .commit-button {\n      @include cButton();\n      position: absolute;\n      right: 5px;\n      bottom: 5px;\n    }\n  }\n\n  /***** SEARCH RESULTS */\n  &__search-results {\n    display: flex;\n    flex: 1 1 auto;\n    flex-direction: column;\n    overflow-y: auto;\n\n    > * + * {\n      margin-top: 5px;\n    }\n\n    &__header {\n      font-size: $headerFontSize;\n      flex: 0 0 auto;\n    }\n\n    .c-notebook__entries {\n      flex: 1 1 auto;\n    }\n\n    .c-ne {\n      flex-direction: column;\n\n      > * + * {\n        margin-top: $interiorMargin;\n      }\n    }\n  }\n\n  /***** RESTRICTED NOTEBOOK */\n  &__page-locked-message {\n    background: rgba($colorAlert, 0.2);\n    display: flex;\n    padding: 5px;\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n\n    [class*='icon'] {\n      flex: 0 0 auto;\n    }\n\n    [class*='__message'] {\n      flex: 1 1 auto;\n    }\n  }\n\n  &__commit-entries-control {\n    display: flex;\n    justify-content: flex-end;\n  }\n}\n\n.is-notebook-default,\n.is-status--notebook-default {\n  &:after {\n    color: $colorFilter;\n    font-family: symbolsfont;\n    font-size: 0.9em;\n  }\n\n  &.c-list__item:after {\n    content: $glyph-icon-notebook-page;\n    flex: 1 0 auto;\n    text-align: right;\n  }\n}\n\n/****************************** ENTRIES */\n.c-ne {\n  // A Notebook entry\n  $p: $interiorMarginSm;\n  @include discreteItem();\n  cursor: pointer;\n  display: flex;\n  padding: $interiorMarginSm $interiorMarginSm $interiorMarginSm $interiorMargin;\n\n  @include hover() {\n    background: $colorDiscreteItemBgHov;\n  }\n\n  &.is-selected {\n    background: rgba($colorKey, 0.1);\n\n    .c-ne__text {\n      .c-notebook--page-unlocked & {\n        cursor: text;\n\n        @include hover() {\n          &:not(.locked) {\n            background: $colorInputBgHov;\n          }\n        }\n      }\n    }\n  }\n\n  &__text,\n  &__local-controls {\n    padding-top: $p;\n    padding-bottom: $p;\n  }\n\n  &__creator,\n  &__embed__time {\n    opacity: 0.9;\n  }\n\n  &__time-and-creator-and-delete,\n  &__text,\n  &__input {\n    padding: $p;\n  }\n\n  &__time-and-creator-and-delete {\n    display: flex;\n    align-items: center;\n\n    > * + * {\n      float: right;\n      margin-left: auto;\n    }\n  }\n\n  &__time-and-creator {\n    color: $colorA;\n  }\n\n  &__creator [class*='icon'] {\n    font-size: 0.95em;\n  }\n\n  &__time-and-content {\n    display: block;\n    flex: 1 1 auto;\n    overflow: visible;\n\n    > * + * {\n      margin-top: $interiorMarginSm;\n    }\n  }\n\n  &__time {\n    * {\n      white-space: nowrap;\n    }\n  }\n\n  &__content {\n    flex-direction: column;\n    flex: 1 1 auto;\n    margin-right: $interiorMarginSm;\n    margin-top: $interiorMargin;\n\n    > [class*='__'] + [class*='__'] {\n      margin-top: $interiorMarginSm;\n    }\n  }\n\n  &__text,\n  &__input {\n    color: $colorBodyFgEm !important;\n  }\n\n  &__text {\n    min-height: 22px; // Needed in Firefox when field is blank\n    white-space: normal;\n\n    .search-highlight {\n      @include searchHighlight();\n    }\n\n    // Resets and styling for markdown\n    h1,\n    h2,\n    h3,\n    h4,\n    h5 {\n      margin: unset !important;\n      padding: unset !important;\n    }\n\n    a {\n      text-decoration: underline;\n    }\n\n    em {\n      font-style: italic;\n    }\n\n    > * {\n      &:not(:first-child) {\n        margin-top: $interiorMarginSm;\n      }\n    }\n\n    p,\n    blockquote,\n    pre {\n      margin: 0;\n\n      &:not(:first-child) {\n        margin-top: $interiorMarginLg;\n      }\n    }\n\n    blockquote {\n      $m: 16px;\n      margin-left: $m;\n      margin-right: $m;\n    }\n\n    ul,\n    ol {\n      padding: 0 0 0 16px;\n    }\n\n    ul {\n      list-style: square;\n    }\n\n    ol {\n      list-style: decimal;\n    }\n\n    li {\n      list-style-type: inherit;\n    }\n\n    table {\n      width: auto;\n    }\n\n    th,\n    td {\n      border: 1px solid rgba($colorBodyFg, 0.7);\n    }\n\n    th {\n      background: rgba($colorBodyFg, 0.2);\n    }\n  }\n\n  &__input {\n    // Textarea\n    //@include inlineInput;\n    @include reactive-input();\n    padding-left: $p;\n    padding-right: $p;\n    overflow: unset;\n    margin-bottom: $interiorMargin;\n    min-height: 5rem;\n    resize: vertical;\n    width: 100%;\n  }\n\n  &__save-button {\n    display: flex;\n    justify-content: flex-end;\n\n    .c-button {\n      $lrP: 15px;\n      padding-left: $lrP;\n      padding-right: $lrP;\n    }\n  }\n\n  &__section-and-page {\n    // Shown when c-ne within search results\n    background: rgba($colorBodyFg, 0.1); //$colorInteriorBorder;\n    border-radius: $controlCr;\n    display: inline-flex;\n    align-items: center;\n    align-self: flex-start;\n    padding: $interiorMargin;\n\n    .search-highlight {\n      @include searchHighlight();\n    }\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n\n    [class*='icon'] {\n      font-size: 0.8em;\n      opacity: 0.7;\n    }\n  }\n\n  &__remove {\n    float: right;\n  }\n}\n\n/****************************** EMBEDS */\n@mixin snapThumb() {\n  // LEGACY: TODO: refactor when .snap-thumb in New Entry dialog is refactored\n  $d: 40px;\n  border: 1px solid $colorInteriorBorder;\n  cursor: pointer;\n  width: $d;\n  height: $d;\n  border-radius: 5px;\n  overflow: hidden;\n\n  img {\n    height: 100%;\n    width: 100%;\n  }\n}\n\n.snap-thumb {\n  // LEGACY,\n  @include snapThumb();\n}\n\n.c-ne__embeds-wrapper {\n  max-height: 75px;\n  padding-left: $interiorMargin;\n  padding-top: $interiorMargin;\n  display: flex;\n}\n\n.c-ne__embed {\n  @include discreteItemInnerElem();\n  display: inline-flex;\n  flex: 0 0 auto;\n  padding: $interiorMargin;\n  border: 1px solid $colorInteriorBorderNotebook;\n\n  &__info {\n    display: flex;\n    flex-direction: column;\n    margin-left: $interiorMargin;\n\n    a {\n      color: $colorKey;\n    }\n  }\n\n  &__name,\n  &__link {\n    // Holds __link and __context-available\n    display: flex;\n    align-items: center;\n  }\n\n  &__link {\n    flex: 1 1 auto;\n\n    &:before {\n      display: block;\n      font-size: 1em;\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &__context-available {\n    font-size: 0.7em;\n    margin-left: $interiorMarginSm;\n  }\n\n  &__snap-thumb {\n    @include snapThumb();\n  }\n\n  &__actions {\n    margin: $interiorMarginSm;\n  }\n\n  &__actions-menu {\n    width: 55vh;\n    max-width: 500px;\n    height: 130px;\n    z-index: 70;\n\n    [class*='__icon'] {\n      filter: $colorKeyFilter;\n      margin: 0%;\n      height: 4vh;\n    }\n\n    [class*='__item-description'] {\n      min-width: 200px;\n    }\n  }\n}\n\n/****************************** SNAPSHOTTING */\n// LEGACY: TODO: refactor these names\n.t-contents,\n.snap-annotation {\n  overflow: hidden;\n}\n\n.s-status-taking-snapshot,\n.overlay.snapshot {\n  // Handle overflow-y issues with tables and html2canvas\n  background: $colorBodyBg; // Prevent html2canvas from using white background\n  color: $colorBodyFg;\n  padding: $interiorMarginSm !important; // Prevents items from going right to the edge of the image\n\n  .l-sticky-headers .l-tabular-body {\n    overflow: auto;\n  }\n\n  .l-browse-bar {\n    display: none; // Suppress browse-bar when snapshotting from view-large overlay\n    + * {\n      margin-top: 0 !important; // Remove margin from any following elements\n    }\n  }\n\n  * {\n    box-shadow: none !important; // Prevent html2canvas problems with box-shadow\n  }\n}\n\n.c-notebook-snapshot {\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__header {\n    flex: 0 0 auto;\n  }\n\n  &__image {\n    background-size: contain;\n    background-repeat: no-repeat;\n    background-position: center center;\n    flex: 1 1 auto;\n  }\n}\n\n/****************************** SNAPSHOT CONTAINER */\n.c-snapshots-h {\n  // Is hidden when the parent div l-shell__drawer is collapsed, so no worries about padding, etc.\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  padding: $interiorMargin $interiorMarginLg;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  .l-browse-bar {\n    flex: 0 0 auto;\n  }\n\n  .c-snapshots-h__title {\n    display: flex;\n  }\n\n  .c-snapshots {\n    flex: 1 1 auto;\n  }\n}\n\n.c-snapshots {\n  flex-wrap: wrap;\n  overflow: auto;\n\n  .c-snapshot {\n    margin: 0 $interiorMarginSm $interiorMarginSm 0;\n  }\n\n  .hint {\n    font-size: 1.25em;\n    font-style: italic;\n    opacity: 0.7;\n    padding: $interiorMarginLg;\n    text-align: center;\n  }\n}\n\n/****************************** PAINTERRO OVERRIDES */\n.annotation-dialog .abs.editor {\n  border-radius: 0;\n}\n\n#snap-annotation {\n  $m: 0; //$interiorMargin;\n  display: flex;\n  flex-direction: column;\n  position: absolute;\n  top: $m;\n  right: 0;\n  bottom: $m;\n  left: 0; // LEGACY, deal with .editor border-radius clipping stuff\n}\n\n#snap-annotation-wrapper,\n#snap-annotation-bar {\n  position: relative;\n  top: auto;\n  right: auto;\n  bottom: auto;\n  left: auto;\n}\n\n#snap-annotation-wrapper {\n  background: rgba($colorBodyFg, 0.1);\n  border: 1px solid $colorInteriorBorder;\n  order: 2;\n  flex: 10 0 auto;\n}\n\n#snap-annotation-bar {\n  // Holds tool buttons, color selectors, etc.\n  $h: 22px;\n  $fs: 0.8rem;\n  $m: $interiorMarginSm;\n\n  display: flex;\n  align-items: center;\n  height: $h + ($m * 2) !important;\n  margin-bottom: $interiorMarginLg;\n  order: 1;\n  flex: 0 0 auto;\n  background-color: transparent !important;\n  padding: $interiorMarginSm;\n\n  > div {\n    display: contents;\n\n    > * + * {\n      margin-left: $interiorMargin !important;\n    }\n  }\n\n  .ptro-tool-controls {\n    display: flex;\n    margin-left: $interiorMarginLg !important;\n\n    > * + * {\n      margin-left: $interiorMargin !important;\n    }\n  }\n\n  .ptro-icon-btn,\n  .ptro-named-btn,\n  .ptro-color-btn,\n  .ptro-bordered-btn,\n  .ptro-tool-ctl-name,\n  .ptro-color-btn,\n  .tool-controls,\n  .ptro-input {\n    // Lot of resets for crappy CSS in Painterro\n    font-family: inherit;\n    font-size: $fs !important;\n    height: $h !important;\n    margin: 0;\n    position: relative;\n    line-height: $h !important;\n  }\n\n  .ptro-tool-ctl-name {\n    border-radius: 0;\n    background: none;\n    color: $colorBodyFg;\n    top: auto;\n    font-family: inherit;\n    padding: 0;\n  }\n\n  .ptro-color-btn {\n    width: $h !important;\n  }\n\n  .ptro-check,\n  .ptro-color-control,\n  .ptro-icon-btn,\n  .ptro-named-btn {\n    // Buttons in toolbar\n    border-radius: $smallCr;\n    box-shadow: rgba($colorBtnFg, 0.3) 0 0 0 1px;\n    color: $colorBtnFg !important;\n    padding: 1px $interiorMargin;\n\n    &:hover {\n      background: $colorBtnBgHov;\n      color: $colorBtnFgHov;\n    }\n\n    i {\n      display: contents;\n      font-size: $fs * 1.2;\n      line-height: inherit;\n    }\n  }\n\n  .ptro-color-control,\n  .ptro-icon-btn,\n  .ptro-named-btn {\n    // Buttons in toolbar\n    background-color: $colorBtnBg;\n  }\n\n  .ptro-color-active-control {\n    background: $colorBtnMajorBg !important;\n    color: $colorBtnMajorFg !important;\n  }\n\n  .ptro-info,\n  .ptro-btn-color-checkers-bar,\n  *[title='Font name'],\n  *[title='Stroke color'],\n  *[title='Stroke width'],\n  *[data-id='fontName'],\n  *[data-id='fontStrokeSize'],\n  *[data-id='stroke'] {\n    display: none;\n  }\n}\n\n/****************************** MOBILE */\nbody.mobile {\n  .c-notebook__drag-area {\n    display: none;\n  }\n\n  .c-notebook__entry {\n    [class*='local-controls'] {\n      display: none;\n      height: fit-content;\n    }\n  }\n\n  &.phone.portrait {\n    .c-notebook__head,\n    .c-notebook__entry,\n    .c-ne__time-and-content {\n      flex-direction: column;\n\n      > [class*='__'] + [class*='__'] {\n        margin-left: 0;\n        margin-top: $interiorMargin;\n      }\n    }\n\n    .c-notebook__entry {\n      [class*='text'] {\n        min-height: 0;\n        pointer-events: none;\n      }\n    }\n  }\n}\n\n/****************************** INDICATOR */\n.c-indicator.has-new-snapshot {\n  $c: $colorOk;\n  @include pulseProp(\n    $animName: flashSnapshot,\n    $dur: 500ms,\n    $iter: infinite,\n    $prop: background,\n    $valStart: rgba($c, 0.4),\n    $valEnd: rgba($c, 0)\n  );\n}\n\n/****************************** RESTRICTED NOTEBOOK / SHIFT LOG */\n.c-notebook--restricted {\n  .c-notebook__pages {\n    .c-list__item {\n      // Can display lock icon when a page is committed.\n      &:before {\n        $s: 0.8em;\n        color: $colorAlert;\n        display: block;\n        font-size: $s;\n        width: $s;\n        margin-right: $interiorMarginSm;\n      }\n\n      &:not([class*='lock']) {\n        &:before {\n          content: '';\n        }\n      }\n    }\n  }\n}\n\n.c-list__item {\n  &__name:focus {\n    text-overflow: clip;\n  }\n}\n"
  },
  {
    "path": "src/styles/plotly.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/***************** PLOTLY OVERRIDES */\n.plot-container.plotly {\n  .bglayer .bg {\n    fill: $colorPlotBg !important;\n    fill-opacity: 1 !important;\n    stroke: $colorInteriorBorder;\n    stroke-width: 1 !important;\n  }\n\n  .cartesianlayer {\n    .gridlayer {\n      .x,\n      .y {\n        path {\n          opacity: $opacityPlotHash;\n          stroke: $colorPlotHash !important;\n        }\n      }\n    }\n\n    path.xy2-y {\n      stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba\n      opacity: $opacityPlotHash !important;\n    }\n  }\n\n  .xtick,\n  .ytick,\n  [class^='g-'] text[class*='title'] {\n    // Matches <g class=\"g-*\"> <text class=\"*title\">\n    text {\n      fill: $colorPlotFg !important;\n      font-size: 12px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/vendor/normalize-min.scss",
    "content": "/*! normalize.css v1.1.2 | MIT License | git.io/normalize */\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection,\nsummary {\n  display: block;\n}\naudio,\ncanvas,\nvideo {\n  display: inline-block;\n  *display: inline;\n  *zoom: 1;\n}\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n[hidden] {\n  display: none;\n}\nhtml {\n  font-size: 100%;\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n}\nhtml,\nbutton,\ninput,\nselect,\ntextarea {\n  font-family: sans-serif;\n}\nbody {\n  margin: 0;\n}\na:focus {\n  outline: thin dotted;\n}\na:active,\na:hover {\n  outline: 0;\n}\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\nh2 {\n  font-size: 1.5em;\n  margin: 0.83em 0;\n}\nh3 {\n  font-size: 1.17em;\n  margin: 1em 0;\n}\nh4 {\n  font-size: 1em;\n  margin: 1.33em 0;\n}\nh5 {\n  font-size: 0.83em;\n  margin: 1.67em 0;\n}\nh6 {\n  font-size: 0.67em;\n  margin: 2.33em 0;\n}\nabbr[title] {\n  border-bottom: 1px dotted;\n}\nb,\nstrong {\n  font-weight: bold;\n}\nblockquote {\n  margin: 1em 40px;\n}\ndfn {\n  font-style: italic;\n}\nhr {\n  -moz-box-sizing: content-box;\n  box-sizing: content-box;\n  height: 0;\n}\nmark {\n  background: #ff0;\n  color: #000;\n}\np,\npre {\n  margin: 1em 0;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, serif;\n  _font-family: 'courier new', monospace;\n  font-size: 1em;\n}\npre {\n  white-space: pre;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\nq {\n  quotes: none;\n}\nq:before,\nq:after {\n  content: '';\n  content: none;\n}\nsmall {\n  font-size: 80%;\n}\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\nsup {\n  top: -0.5em;\n}\nsub {\n  bottom: -0.25em;\n}\ndl,\nmenu,\nol,\nul {\n  margin: 1em 0;\n}\ndd {\n  margin: 0 0 0 40px;\n}\nmenu,\nol,\nul {\n  padding: 0 0 0 40px;\n}\nnav ul,\nnav ol {\n  list-style: none;\n  list-style-image: none;\n}\nimg {\n  border: 0;\n  -ms-interpolation-mode: bicubic;\n}\nsvg:not(:root) {\n  overflow: hidden;\n}\nfigure {\n  margin: 0;\n}\nform {\n  margin: 0;\n}\nfieldset {\n  border: 1px solid silver;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n  border: 0;\n  padding: 0;\n  white-space: normal;\n  *margin-left: -7px;\n}\nbutton,\ninput,\nselect,\ntextarea {\n  font-size: 100%;\n  margin: 0;\n  vertical-align: baseline;\n  *vertical-align: middle;\n}\nbutton,\ninput {\n  line-height: normal;\n}\nbutton,\nselect {\n  text-transform: none;\n}\nbutton,\nhtml input[type='button'],\ninput[type='reset'],\ninput[type='submit'] {\n  -webkit-appearance: button;\n  cursor: pointer;\n  *overflow: visible;\n}\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\ninput[type='checkbox'],\ninput[type='radio'] {\n  box-sizing: border-box;\n  padding: 0;\n  *height: 13px;\n  *width: 13px;\n}\ninput[type='search'] {\n  -webkit-appearance: textfield;\n  -moz-box-sizing: content-box;\n  -webkit-box-sizing: content-box;\n  box-sizing: content-box;\n}\ninput[type='search']::-webkit-search-cancel-button,\ninput[type='search']::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\ntextarea {\n  overflow: auto;\n  vertical-align: top;\n}\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n"
  },
  {
    "path": "src/styles/vue-styles.scss",
    "content": "@import '../api/overlays/components/dialog-component.scss';\n@import '../api/overlays/components/overlay-component.scss';\n@import '../api/tooltips/components/tooltip-component.scss';\n@import '../plugins/condition/components/conditionals.scss';\n@import '../plugins/comps/components/comps.scss';\n@import '../plugins/conditionWidget/components/condition-widget.scss';\n@import '../plugins/condition/components/inspector/conditional-styles.scss';\n@import '../plugins/displayLayout/components/box-and-line-views';\n@import '../plugins/displayLayout/components/display-layout.scss';\n@import '../plugins/displayLayout/components/edit-marquee.scss';\n@import '../plugins/displayLayout/components/image-view.scss';\n@import '../plugins/displayLayout/components/layout-frame.scss';\n@import '../plugins/displayLayout/components/telemetry-view.scss';\n@import '../plugins/displayLayout/components/text-view.scss';\n@import '../plugins/events/components/events-view.scss';\n@import '../plugins/filters/components/filters-view.scss';\n@import '../plugins/filters/components/global-filters.scss';\n@import '../plugins/flexibleLayout/components/flexible-layout.scss';\n@import '../plugins/folderView/components/grid-view.scss';\n@import '../plugins/folderView/components/list-item.scss';\n@import '../plugins/imagery/components/imagery-view.scss';\n@import '../plugins/imagery/components/Compass/compass.scss';\n@import '../plugins/telemetryTable/components/table-row.scss';\n@import '../plugins/telemetryTable/components/table-footer-indicator.scss';\n@import '../plugins/tabs/components/tabs.scss';\n@import '../plugins/telemetryTable/components/table.scss';\n@import '../plugins/timeConductor/conductor.scss';\n@import '../plugins/timeConductor/conductor-axis.scss';\n@import '../plugins/timeConductor/conductor-mode-icon.scss';\n@import '../plugins/timeConductor/date-picker.scss';\n@import '../plugins/timeline/timeline.scss';\n@import '../plugins/timelist/timelist.scss';\n@import '../plugins/plan/plan';\n@import '../plugins/viewDatumAction/components/metadata-list.scss';\n@import '../ui/components/object-frame.scss';\n@import '../ui/components/object-label.scss';\n@import '../ui/components/progress-bar.scss';\n@import '../ui/components/search.scss';\n@import '../ui/components/swim-lane/swimlane.scss';\n@import '../plugins/inspectorViews/annotations/tags/tags.scss';\n@import '../ui/components/toggle-switch.scss';\n@import '../ui/components/timesystem-axis.scss';\n@import '../ui/components/List/list-view.scss';\n@import '../plugins/inspectorViews/elements/elements.scss';\n@import '../ui/inspector/inspector.scss';\n@import '../plugins/inspectorViews/properties/location.scss';\n@import '../ui/layout/app-logo.scss';\n@import '../ui/layout/create-button.scss';\n@import '../ui/layout/layout.scss';\n@import '../ui/layout/mct-tree.scss';\n@import '../ui/layout/search/search.scss';\n@import '../ui/layout/pane.scss';\n@import '../ui/layout/recent-objects.scss';\n@import '../ui/layout/status-bar/indicators.scss';\n@import '../ui/layout/status-bar/notification-banner.scss';\n@import '../ui/preview/preview.scss';\n@import '../ui/toolbar/components/toolbar-checkbox.scss';\n@import './notebook.scss';\n@import '../plugins/notebook/components/sidebar.scss';\n@import '../plugins/gauge/gauge.scss';\n@import '../plugins/faultManagement/fault-manager.scss';\n@import '../plugins/operatorStatus/operator-status.scss';\n@import '../plugins/userIndicator/user-indicator.scss';\n@import '../plugins/inspectorDataVisualization/inspector-data-visualization.scss';\n\n#splash-screen {\n  display: none;\n}\n"
  },
  {
    "path": "src/tools/url.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Module defining url handling.\n */\n\n/**\n * Convert the current URL parameters to an array of strings.\n * @param {import('../../openmct').OpenMCT} openmct\n * @returns {Array<string>} newTabParams\n */\nexport function paramsToArray(openmct, customUrlParams = {}) {\n  let urlParams = openmct.router.getParams();\n\n  // Merge the custom URL parameters with the current URL parameters.\n  Object.entries(customUrlParams).forEach((param) => {\n    const [key, value] = param;\n    urlParams[key] = value;\n  });\n\n  if (urlParams['tc.mode'] === 'fixed') {\n    delete urlParams['tc.startDelta'];\n    delete urlParams['tc.endDelta'];\n  } else if (urlParams['tc.mode'] === 'local') {\n    delete urlParams['tc.startBound'];\n    delete urlParams['tc.endBound'];\n  }\n\n  return Object.entries(urlParams).map(([key, value]) => `${key}=${value}`);\n}\n\nexport function identifierToString(openmct, objectPath) {\n  return '#/browse/' + openmct.objects.getRelativePath(objectPath);\n}\n\n/**\n * @param {import('../../openmct').OpenMCT} openmct\n * @param {Array<import('../api/objects/ObjectAPI').DomainObject>} objectPath\n * @param {any} customUrlParams\n * @returns {string} url\n */\nexport function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {\n  let url = identifierToString(openmct, objectPath);\n\n  let urlParams = paramsToArray(openmct, customUrlParams);\n  if (urlParams.length) {\n    url += '?' + urlParams.join('&');\n  }\n\n  return url;\n}\n"
  },
  {
    "path": "src/tools/urlSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from '../utils/testing.js';\nimport { identifierToString, objectPathToUrl, paramsToArray } from './url.js';\n\ndescribe('the url tool', function () {\n  let openmct;\n  let mockObjectPath;\n\n  beforeEach((done) => {\n    mockObjectPath = [\n      {\n        name: 'mock folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-folder',\n          namespace: ''\n        }\n      },\n      {\n        name: 'mock parent folder',\n        type: 'fake-folder',\n        identifier: {\n          key: 'mock-parent-folder',\n          namespace: ''\n        }\n      }\n    ];\n    openmct = createOpenMct();\n    openmct.on('start', () => {\n      openmct.router.setPath('/browse/mine?testParam1=testValue1');\n      done();\n    });\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('paramsToArray', () => {\n    it('exists', () => {\n      expect(paramsToArray).toBeDefined();\n    });\n    it('can construct an array properly from query parameters', () => {\n      const arrayOfParams = paramsToArray(openmct);\n      expect(arrayOfParams.length).toBeDefined();\n      expect(arrayOfParams.length).toBeGreaterThan(0);\n    });\n  });\n\n  describe('identifierToString', () => {\n    it('exists', () => {\n      expect(identifierToString).toBeDefined();\n    });\n    it('can construct a String properly from a path', () => {\n      const constructedString = identifierToString(openmct, mockObjectPath);\n      expect(constructedString).toEqual('#/browse/mock-parent-folder/mock-folder');\n    });\n  });\n\n  describe('objectPathToUrl', () => {\n    it('exists', () => {\n      expect(objectPathToUrl).toBeDefined();\n    });\n    it('can construct URL properly from a path', () => {\n      const constructedURL = objectPathToUrl(openmct, mockObjectPath);\n      expect(constructedURL).toContain('#/browse/mock-parent-folder/mock-folder');\n    });\n    it('can take params to set a custom url', () => {\n      const customParams = {\n        'tc.startBound': 1669911059,\n        'tc.endBound': 1669911082,\n        'tc.mode': 'fixed'\n      };\n      const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);\n      expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082');\n      expect(constructedURL).toContain('tc.mode=fixed');\n    });\n  });\n});\n"
  },
  {
    "path": "src/ui/color/Color.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A representation of a color that allows conversions between different\n * formats.\n *\n * @constructor\n */\n/**\n * A representation of a color that allows conversions between different\n * formats.\n *\n * @constructor\n */\nfunction Color(integerArray) {\n  this.integerArray = integerArray;\n}\n\nColor.fromHexString = function (hexString) {\n  if (!/#([0-9a-fA-F]{2}){2}/.test(hexString)) {\n    throw new Error(\n      'Invalid input \"' + hexString + '\". Hex string must be in CSS format e.g. #00FF00'\n    );\n  }\n\n  return new Color([\n    parseInt(hexString.slice(1, 3), 16),\n    parseInt(hexString.slice(3, 5), 16),\n    parseInt(hexString.slice(5, 7), 16)\n  ]);\n};\n\n/**\n * Return color as a three element array of RGB values, where each value\n * is a integer in the range of 0-255.\n *\n * @return {number[]} the color, as integer RGB values\n */\nColor.prototype.asIntegerArray = function () {\n  return this.integerArray.map(function (c) {\n    return c;\n  });\n};\n\n/**\n * Return color as a string using #-prefixed six-digit RGB hex notation\n * (e.g. #FF0000).  See http://www.w3.org/TR/css3-color/#rgb-color.\n *\n * @return {string} the color, as a style-friendly string\n */\n\nColor.prototype.asHexString = function () {\n  return (\n    '#' +\n    this.integerArray\n      .map(function (c) {\n        return (c < 16 ? '0' : '') + c.toString(16);\n      })\n      .join('')\n  );\n};\n\n/**\n * Return color as a RGBA float array.\n *\n * This format is present specifically to support use with\n * WebGL, which expects colors of that form.\n *\n * @return {number[]} the color, as floating-point RGBA values\n */\nColor.prototype.asRGBAArray = function () {\n  return this.integerArray\n    .map(function (c) {\n      return c / 255.0;\n    })\n    .concat([1]);\n};\n\nColor.prototype.equalTo = function (otherColor) {\n  return this.asHexString() === otherColor.asHexString();\n};\n\nexport default Color;\n"
  },
  {
    "path": "src/ui/color/ColorHelper.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const COLOR_PALETTE = [\n  [0x43, 0xb0, 0xff],\n  [0xf0, 0x60, 0x00],\n  [0x00, 0x70, 0x40],\n  [0xfb, 0x49, 0x49],\n  [0xc8, 0x00, 0xcf],\n  [0x55, 0x77, 0xf2],\n  [0xff, 0xa6, 0x3d],\n  [0x05, 0xa3, 0x00],\n  [0xf0, 0x00, 0x6c],\n  [0xac, 0x54, 0xae],\n  [0x23, 0xa9, 0xdb],\n  [0xc7, 0xbe, 0x52],\n  [0x5a, 0xbd, 0x56],\n  [0xad, 0x50, 0x72],\n  [0x94, 0x25, 0xea],\n  [0x21, 0x87, 0x82],\n  [0x8f, 0x6e, 0x47],\n  [0xf0, 0x59, 0xcb],\n  [0x34, 0xb6, 0x7d],\n  [0x7f, 0x52, 0xff],\n  [0x46, 0xc7, 0xc0],\n  [0xa1, 0x8c, 0x1c],\n  [0x95, 0xb1, 0x26],\n  [0xff, 0x84, 0x9e],\n  [0xb7, 0x79, 0xe7],\n  [0x8c, 0xc9, 0xfd],\n  [0xdb, 0xaa, 0x6e],\n  [0x93, 0xb5, 0x77],\n  [0xff, 0xbc, 0xda],\n  [0xd3, 0xb6, 0xde]\n];\n\nexport function isDefaultColor(color) {\n  const a = color.asIntegerArray();\n\n  return COLOR_PALETTE.some(function (b) {\n    return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];\n  });\n}\n"
  },
  {
    "path": "src/ui/color/ColorPalette.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A color palette stores a set of colors and allows for different\n * methods of color allocation.\n *\n * @constructor\n */\nimport Color from './Color.js';\nimport { COLOR_PALETTE, isDefaultColor } from './ColorHelper.js';\n\n/**\n * A color palette stores a set of colors and allows for different\n * methods of color allocation.\n *\n * @constructor\n */\nfunction ColorPalette() {\n  const allColors = (this.allColors = COLOR_PALETTE.map(function (color) {\n    return new Color(color);\n  }));\n  this.colorGroups = [[], [], []];\n  for (let i = 0; i < allColors.length; i++) {\n    this.colorGroups[i % 3].push(allColors[i]);\n  }\n\n  this.reset();\n}\n\n/**\n *\n */\nColorPalette.prototype.groups = function () {\n  return this.colorGroups;\n};\n\nColorPalette.prototype.reset = function () {\n  this.availableColors = this.allColors.slice();\n};\n\nColorPalette.prototype.remove = function (color) {\n  this.availableColors = this.availableColors.filter(function (c) {\n    return !c.equalTo(color);\n  });\n};\n\nColorPalette.prototype.return = function (color) {\n  if (isDefaultColor(color)) {\n    this.availableColors.unshift(color);\n  }\n};\n\nColorPalette.prototype.getByHexString = function (hexString) {\n  const color = Color.fromHexString(hexString);\n\n  return color;\n};\n\n/**\n * @returns {Color} the next unused color in the palette.  If all colors\n * have been allocated, it will wrap around.\n */\nColorPalette.prototype.getNextColor = function () {\n  if (!this.availableColors.length) {\n    console.warn('Color Palette empty, reusing colors!');\n    this.reset();\n  }\n\n  return this.availableColors.shift();\n};\n\nexport default ColorPalette;\n"
  },
  {
    "path": "src/ui/color/ColorSwatch.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"grid-row grid-row--pad-label-for-button\">\n    <template v-if=\"canEdit\">\n      <div class=\"grid-cell label\" :aria-label=\"editTitle\" :title=\"editTitle\">{{ shortLabel }}</div>\n      <div class=\"grid-cell value\">\n        <div class=\"c-click-swatch c-click-swatch--menu\" @click=\"toggleSwatch()\">\n          <span class=\"c-color-swatch\" :style=\"{ background: currentColor }\"> </span>\n        </div>\n        <div class=\"c-palette c-palette--color\">\n          <div v-show=\"swatchActive\" class=\"c-palette__items\">\n            <div v-for=\"group in colorPaletteGroups\" :key=\"group.id\" class=\"u-contents\">\n              <div\n                v-for=\"color in group\"\n                :key=\"color.id\"\n                class=\"c-palette__item\"\n                :class=\"{ selected: currentColor === color.hexString }\"\n                :style=\"{ background: color.hexString }\"\n                @click=\"setColor(color)\"\n              ></div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </template>\n    <template v-else>\n      <div class=\"grid-cell label\" :aria-label=\"viewTitle\" :title=\"viewTitle\">{{ shortLabel }}</div>\n      <div class=\"grid-cell value\">\n        <span\n          class=\"c-color-swatch\"\n          :style=\"{\n            background: currentColor\n          }\"\n        >\n        </span>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport ColorPalette from './ColorPalette.js';\n\nexport default {\n  inject: ['openmct', 'domainObject'],\n  props: {\n    currentColor: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    editTitle: {\n      type: String,\n      default() {\n        return 'Set the color.';\n      }\n    },\n    viewTitle: {\n      type: String,\n      default() {\n        return 'The current color.';\n      }\n    },\n    shortLabel: {\n      type: String,\n      default() {\n        return 'Color';\n      }\n    }\n  },\n  emits: ['color-set'],\n  data() {\n    return {\n      swatchActive: false,\n      colorPaletteGroups: [],\n      isEditing: this.openmct.editor.isEditing()\n    };\n  },\n  computed: {\n    canEdit() {\n      return this.isEditing && !this.domainObject.locked;\n    }\n  },\n  mounted() {\n    this.colorPalette = new ColorPalette();\n    this.openmct.editor.on('isEditing', this.setEditState);\n    this.initialize();\n  },\n  beforeUnmount() {\n    this.openmct.editor.off('isEditing', this.setEditState);\n  },\n  methods: {\n    initialize() {\n      const colorPaletteGroups = this.colorPalette.groups();\n      colorPaletteGroups.forEach((group, index) => {\n        let groupId = [];\n        group.forEach((color) => {\n          color.hexString = color.asHexString();\n          color.id = `${color.hexString}-${index}`;\n          groupId.push(color.id);\n        });\n        group.id = groupId.join('-');\n      });\n      this.colorPaletteGroups = colorPaletteGroups;\n    },\n    setEditState(isEditing) {\n      this.isEditing = isEditing;\n    },\n    setColor(chosenColor) {\n      this.$emit('color-set', chosenColor);\n    },\n    toggleSwatch() {\n      this.swatchActive = !this.swatchActive;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/COMPONENTS.md",
    "content": "# Components\n\nComponents in this folder are intended for reuse in other parts of the \napplication.  In order for components to be reused, they must not depend on \nparent styling, and they should have minimum internal state.\n"
  },
  {
    "path": "src/ui/components/ContextMenuDropDown.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"root\"\n    class=\"c-so-view__context-actions c-disclosure-button\"\n    @click=\"showContextMenu\"\n  ></div>\n</template>\n\n<script>\nimport contextMenu from '../mixins/context-menu-gesture.js';\n\nexport default {\n  mixins: [contextMenu],\n  props: {\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/List/ListHeader.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <th\n    v-if=\"isSortable\"\n    class=\"is-sortable\"\n    :class=\"{\n      'is-sorting': currentSort === property,\n      asc: direction,\n      desc: !direction\n    }\"\n    @click=\"sort(property, direction)\"\n  >\n    {{ title }}\n  </th>\n  <th v-else>\n    {{ title }}\n  </th>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    property: {\n      type: String,\n      required: true\n    },\n    currentSort: {\n      type: String,\n      required: true\n    },\n    title: {\n      type: String,\n      required: true\n    },\n    direction: {\n      type: Boolean,\n      required: true\n    },\n    isSortable: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    }\n  },\n  emits: ['sort'],\n  methods: {\n    sort(property, direction) {\n      this.$emit('sort', {\n        property,\n        direction\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/List/ListItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <tr class=\"c-list-item js-list-item\" :class=\"item.cssClass || ''\">\n    <td\n      v-for=\"itemValue in formattedItemValues\"\n      :key=\"itemValue.key\"\n      class=\"c-list-item__value js-list-item__value\"\n      :class=\"['--' + itemValue.key]\"\n      :aria-label=\"itemValue.text\"\n      :title=\"itemValue.text\"\n    >\n      {{ itemValue.text }}\n    </td>\n  </tr>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    item: {\n      type: Object,\n      required: true\n    },\n    itemProperties: {\n      type: Array,\n      required: true\n    }\n  },\n  computed: {\n    formattedItemValues() {\n      let values = [];\n      this.itemProperties.forEach((property) => {\n        let value = _.get(this.item, property.key);\n        if (property.format) {\n          value = property.format(value, this.item, property.key, this.openmct);\n        }\n\n        values.push({\n          text: value,\n          key: property.key\n        });\n      });\n\n      return values;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/List/ListView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-table c-table--sortable c-list-view c-list-view--sticky-header\">\n    <table class=\"c-table__body js-table__body\">\n      <thead class=\"c-table__header\">\n        <tr>\n          <ListHeader\n            v-for=\"headerItem in headerItems\"\n            :key=\"headerItem.property\"\n            :direction=\"sortBy === headerItem.property ? ascending : headerItem.defaultDirection\"\n            :is-sortable=\"headerItem.isSortable\"\n            :aria-label=\"headerItem.name\"\n            :title=\"headerItem.name\"\n            :property=\"headerItem.property\"\n            :current-sort=\"sortBy\"\n            @sort=\"sort\"\n          />\n        </tr>\n      </thead>\n      <tbody>\n        <ListItem\n          v-for=\"item in sortedItems\"\n          :key=\"item.key\"\n          :item=\"item\"\n          :item-properties=\"itemProperties\"\n          @click.stop=\"itemSelected(item, $event)\"\n        />\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport ListHeader from './ListHeader.vue';\nimport ListItem from './ListItem.vue';\n\nexport default {\n  components: {\n    ListItem,\n    ListHeader\n  },\n  inject: ['domainObject', 'openmct'],\n  props: {\n    headerItems: {\n      type: Array,\n      required: true\n    },\n    items: {\n      type: Array,\n      required: true\n    },\n    defaultSort: {\n      type: Object,\n      default() {\n        return {\n          property: '',\n          defaultDirection: true\n        };\n      }\n    },\n    storageKey: {\n      type: String,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  emits: ['item-selection-changed'],\n  data() {\n    let sortBy = this.defaultSort.property;\n    let ascending = this.defaultSort.defaultDirection;\n    if (this.storageKey) {\n      let persistedSortOrder = window.localStorage.getItem(this.storageKey);\n\n      if (persistedSortOrder) {\n        let parsed = JSON.parse(persistedSortOrder);\n\n        sortBy = parsed.sortBy;\n        ascending = parsed.ascending;\n      }\n    }\n\n    return {\n      sortBy,\n      ascending\n    };\n  },\n  computed: {\n    sortedItems() {\n      let sortedItems = _.sortBy(this.items, this.sortBy);\n      if (!this.ascending) {\n        sortedItems = sortedItems.reverse();\n      }\n\n      return sortedItems;\n    },\n    itemProperties() {\n      return this.headerItems.map((headerItem) => {\n        return {\n          key: headerItem.property,\n          format: headerItem.format\n        };\n      });\n    }\n  },\n  watch: {\n    defaultSort: {\n      handler() {\n        this.setSort();\n      },\n      deep: true\n    }\n  },\n  methods: {\n    setSort() {\n      this.sortBy = this.defaultSort.property;\n      this.ascending = this.defaultSort.defaultDirection;\n    },\n    sort(data) {\n      const property = data.property;\n      const direction = data.direction;\n\n      if (this.sortBy === property) {\n        this.ascending = !this.ascending;\n      } else {\n        this.sortBy = property;\n        this.ascending = direction;\n      }\n\n      if (this.storageKey) {\n        window.localStorage.setItem(\n          this.storageKey,\n          JSON.stringify({\n            sortBy: this.sortBy,\n            ascending: this.ascending\n          })\n        );\n      }\n    },\n    itemSelected(item, event) {\n      this.$emit('item-selection-changed', item, event.currentTarget);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/List/list-view.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2025, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/******************************* LIST VIEW */\n.c-list-view {\n  tbody tr {\n    background: $colorListItemBg;\n  }\n\n  td {\n    $p: $interiorMargin;\n    @include ellipsize();\n    line-height: 120%; // Needed for icon alignment\n    max-width: 0;\n    padding-top: $p;\n    padding-bottom: $p;\n    width: 25%;\n  }\n\n  &--selectable {\n    body.desktop & {\n      tbody tr {\n        cursor: pointer;\n\n        &:hover {\n          background: $colorListItemBgHov;\n        }\n      }\n    }\n  }\n\n  &--sticky-header {\n    thead tr {\n      position: -webkit-sticky;\n      position: sticky;\n      top: 0;\n      z-index: 2;\n    }\n  }\n\n  .is-object-type-folder & {\n    tbody { font-size: 1.1em; }\n  }\n}\n"
  },
  {
    "path": "src/ui/components/ObjectFrame.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"soView\"\n    class=\"c-so-view js-notebook-snapshot-item-wrapper\"\n    :class=\"[\n      statusClass,\n      widthClass,\n      'c-so-view--' + domainObject.type,\n      {\n        'c-so-view--no-frame': !hasFrame,\n        'has-complex-content': complexContent\n      }\n    ]\"\n    :aria-label=\"ariaLabel\"\n  >\n    <div class=\"c-so-view__header\">\n      <div class=\"c-object-label\" :class=\"[statusClass]\">\n        <div class=\"c-object-label__type-icon\" :class=\"cssClass\">\n          <span\n            class=\"is-status__indicator\"\n            :aria-label=\"`This item is ${status}`\"\n            :title=\"`This item is ${status}`\"\n          ></span>\n        </div>\n        <div\n          ref=\"objectName\"\n          class=\"c-object-label__name\"\n          aria-label=\"object name\"\n          :title=\"domainObject && domainObject.name\"\n          @mouseover.ctrl=\"showToolTip\"\n          @mouseleave=\"hideToolTip\"\n        >\n          {{ domainObject && domainObject.name }}\n        </div>\n      </div>\n\n      <div\n        class=\"c-so-view__frame-controls\"\n        :class=\"{\n          'c-so-view__frame-controls--no-frame': !hasFrame,\n          'has-complex-content': complexContent\n        }\"\n        :aria-label=\"`${ariaLabel} Controls`\"\n      >\n        <div v-if=\"supportsIndependentTime\" class=\"c-conductor-holder--compact\">\n          <IndependentTimeConductor :domain-object=\"domainObject\" :object-path=\"objectPath\" />\n        </div>\n        <NotebookMenuSwitcher\n          v-if=\"notebookEnabled\"\n          :domain-object=\"domainObject\"\n          :object-path=\"objectPath\"\n          class=\"c-notebook-snapshot-menubutton\"\n        />\n        <div v-if=\"statusBarItems.length > 0\" class=\"c-so-view__frame-controls__btns\">\n          <button\n            v-for=\"(item, index) in statusBarItems\"\n            :key=\"index\"\n            class=\"c-icon-button\"\n            :class=\"item.cssClass\"\n            :title=\"item.name\"\n            :aria-label=\"item.name\"\n            @click=\"item.onItemClicked\"\n          >\n            <span class=\"c-icon-button__label\">{{ item.name }}</span>\n          </button>\n        </div>\n        <button\n          class=\"c-icon-button icon-3-dots c-so-view__frame-controls__more\"\n          aria-label=\"View menu items\"\n          title=\"View menu items\"\n          @click.prevent.stop=\"showMenuItems($event)\"\n        ></button>\n      </div>\n    </div>\n\n    <ObjectView\n      ref=\"objectView\"\n      class=\"c-so-view__object-view js-object-view js-notebook-snapshot-item\"\n      :show-edit-view=\"showEditView\"\n      :object-path=\"objectPath\"\n      :layout-font-size=\"layoutFontSize\"\n      :layout-font=\"layoutFont\"\n      @change-action-collection=\"setActionCollection\"\n    />\n  </div>\n</template>\n\n<script>\nimport NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';\nimport IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';\n\nimport tooltipHelpers from '../../api/tooltips/tooltipMixins.js';\nimport { SupportedViewTypes } from '../../utils/constants.js';\nimport ObjectView from './ObjectView.vue';\n\nconst SIMPLE_CONTENT_TYPES = ['clock', 'timer', 'summary-widget', 'hyperlink', 'conditionWidget'];\nconst CSS_WIDTH_LESS_STR = '--width-less-than-';\n\nexport default {\n  components: {\n    ObjectView,\n    NotebookMenuSwitcher,\n    IndependentTimeConductor\n  },\n  mixins: [tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    },\n    hasFrame: Boolean,\n    showEditView: {\n      type: Boolean,\n      default: true\n    },\n    layoutFontSize: {\n      type: String,\n      default: ''\n    },\n    layoutFont: {\n      type: String,\n      default: ''\n    }\n  },\n  data() {\n    let objectType = this.openmct.types.get(this.domainObject.type);\n\n    let cssClass =\n      objectType && objectType.definition ? objectType.definition.cssClass : 'icon-object-unknown';\n\n    let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type);\n\n    return {\n      cssClass,\n      widthClass: '',\n      complexContent,\n      notebookEnabled: this.openmct.types.get('notebook'),\n      statusBarItems: [],\n      status: '',\n      supportsIndependentTime: false\n    };\n  },\n  computed: {\n    ariaLabel() {\n      return `${this.domainObject.name} Frame`;\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    }\n  },\n  mounted() {\n    this.status = this.openmct.status.get(this.domainObject.identifier);\n    this.removeStatusListener = this.openmct.status.observe(\n      this.domainObject.identifier,\n      this.setStatus\n    );\n    const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0];\n    if (provider) {\n      this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath);\n    }\n\n    if (this.$refs.soView) {\n      this.soViewResizeObserver = new ResizeObserver(this.resizeSoView);\n      this.soViewResizeObserver.observe(this.$refs.soView);\n    }\n\n    const viewKey = this.$refs.objectView?.viewKey;\n    this.supportsIndependentTime = this.domainObject && SupportedViewTypes.includes(viewKey);\n  },\n  beforeUnmount() {\n    this.removeStatusListener();\n\n    if (this.actionCollection) {\n      this.unlistenToActionCollection();\n    }\n\n    if (this.soViewResizeObserver) {\n      this.soViewResizeObserver.disconnect();\n    }\n  },\n  methods: {\n    getSelectionContext() {\n      return this.$refs.objectView.getSelectionContext();\n    },\n    setActionCollection(actionCollection) {\n      if (this.actionCollection) {\n        this.unlistenToActionCollection();\n      }\n\n      this.actionCollection = actionCollection;\n      this.actionCollection.on('update', this.updateActionItems);\n      this.updateActionItems();\n    },\n    unlistenToActionCollection() {\n      this.actionCollection.off('update', this.updateActionItems);\n      delete this.actionCollection;\n    },\n    updateActionItems() {\n      const statusBarItems = this.actionCollection.getStatusBarActions();\n      this.statusBarItems = this.openmct.menus.actionsToMenuItems(\n        statusBarItems,\n        this.actionCollection.objectPath,\n        this.actionCollection.view\n      );\n      this.menuActionItems = this.actionCollection.getVisibleActions();\n    },\n    showMenuItems(event) {\n      const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);\n      if (sortedActions.length) {\n        const menuItems = this.openmct.menus.actionsToMenuItems(\n          sortedActions,\n          this.actionCollection.objectPath,\n          this.actionCollection.view\n        );\n        this.openmct.menus.showMenu(event.x, event.y, menuItems);\n      }\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    resizeSoView() {\n      let cW = this.$refs.soView.offsetWidth;\n      let widths = [220, 600];\n      let wClass = '';\n\n      for (let width of widths) {\n        if (cW < width) {\n          wClass = wClass.concat(' ', CSS_WIDTH_LESS_STR, width);\n        }\n      }\n\n      this.widthClass = wClass.trimStart();\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ObjectLabel.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <a\n    ref=\"root\"\n    class=\"c-tree__item__label c-object-label\"\n    :class=\"[statusClass]\"\n    draggable=\"true\"\n    :aria-label=\"ariaLabel\"\n    @dragstart=\"dragStart\"\n    @click=\"navigateOrPreview\"\n  >\n    <div class=\"c-tree__item__type-icon c-object-label__type-icon\" :class=\"typeClass\">\n      <span\n        class=\"is-status__indicator\"\n        :aria-label=\"`This item is ${status}`\"\n        :title=\"`This item is ${status}`\"\n      ></span>\n    </div>\n    <div\n      ref=\"objectLabel\"\n      class=\"c-tree__item__name c-object-label__name\"\n      @mouseover.ctrl=\"showToolTip\"\n      @mouseleave=\"hideToolTip\"\n    >\n      {{ domainObject.name }}\n    </div>\n  </a>\n</template>\n\n<script>\nimport { inject } from 'vue';\n\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport tooltipHelpers from '../../api/tooltips/tooltipMixins.js';\nimport { useIsEditing } from '../../ui/composables/edit.js';\nimport ContextMenuGesture from '../mixins/context-menu-gesture.js';\nimport ObjectLink from '../mixins/object-link.js';\n\nexport default {\n  mixins: [ObjectLink, ContextMenuGesture, tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    },\n    navigateToPath: {\n      type: String,\n      default: undefined\n    },\n    readOnly: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    }\n  },\n  setup() {\n    const openmct = inject('openmct');\n    const { isEditing } = useIsEditing(openmct);\n    return {\n      isEditing\n    };\n  },\n  data() {\n    return {\n      status: ''\n    };\n  },\n  computed: {\n    typeClass() {\n      let type = this.openmct.types.get(this.domainObject.type);\n      if (!type) {\n        return 'icon-object-unknown';\n      }\n\n      return type.definition.cssClass;\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    },\n    ariaLabel() {\n      return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${\n        this.domainObject.type\n      } Object`;\n    }\n  },\n  mounted() {\n    this.removeStatusListener = this.openmct.status.observe(\n      this.domainObject.identifier,\n      this.setStatus\n    );\n    this.status = this.openmct.status.get(this.domainObject.identifier);\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n  },\n  unmounted() {\n    this.removeStatusListener();\n  },\n  methods: {\n    navigateOrPreview(event) {\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        this.preview();\n      } else {\n        this.openmct.router.navigate(this.objectLink);\n      }\n    },\n    preview() {\n      if (this.previewAction.appliesTo(this.objectPath)) {\n        this.previewAction.invoke(this.objectPath);\n      }\n    },\n    dragStart(event) {\n      let navigatedObject = this.openmct.router.path[0];\n      let serializedPath = JSON.stringify(this.objectPath);\n      let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n      /*\n       * Cannot inspect data transfer objects on dragover/dragenter so impossible to determine composability at\n       * that point. If dragged object can be composed by navigated object, then indicate with presence of\n       * 'composable-domain-object' in data transfer\n       */\n      if (this.openmct.composition.checkPolicy(navigatedObject, this.domainObject)) {\n        event.dataTransfer.setData(\n          'openmct/composable-domain-object',\n          JSON.stringify(this.domainObject)\n        );\n      }\n\n      // serialize domain object anyway, because some views can drag-and-drop objects without composition\n      // (eg. notebook.)\n      event.dataTransfer.setData('openmct/domain-object-path', serializedPath);\n      event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'objectLabel');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ObjectPath.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <ul\n    v-if=\"orderedPath.length\"\n    class=\"c-location\"\n    :aria-label=\"`${domainObject.name}`\"\n    role=\"navigation\"\n  >\n    <li v-for=\"pathObject in orderedPath\" :key=\"pathObject.key\" class=\"c-location__item\">\n      <ObjectLabel\n        :domain-object=\"pathObject.domainObject\"\n        :object-path=\"pathObject.objectPath\"\n        :read-only=\"readOnly\"\n        :navigate-to-path=\"navigateToPath(pathObject.objectPath)\"\n      />\n    </li>\n  </ul>\n</template>\n\n<script>\nimport ObjectLabel from './ObjectLabel.vue';\n\nexport default {\n  components: {\n    ObjectLabel\n  },\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    readOnly: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    },\n    showObjectItself: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    },\n    objectPath: {\n      type: Array,\n      default() {\n        return null;\n      }\n    }\n  },\n  data() {\n    return {\n      orderedPath: []\n    };\n  },\n  async mounted() {\n    this.abortController = new AbortController();\n    this.nameChangeListeners = {};\n    const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n    if (keyString && this.keyString !== keyString) {\n      this.keyString = keyString;\n      this.originalPath = [];\n\n      let rawPath = null;\n      if (this.objectPath === null) {\n        try {\n          rawPath = await this.openmct.objects.getOriginalPath(\n            keyString,\n            [],\n            this.abortController.signal\n          );\n        } catch (error) {\n          // aborting the search is ok, everything else should be thrown\n          if (error.name !== 'AbortError') {\n            throw error;\n          }\n        }\n      } else {\n        rawPath = this.objectPath;\n      }\n\n      const pathWithDomainObject = rawPath.map((domainObject, index, pathArray) => {\n        let key = this.openmct.objects.makeKeyString(domainObject.identifier);\n        const objectPath = pathArray.slice(index);\n\n        return {\n          domainObject,\n          key,\n          objectPath\n        };\n      });\n      if (this.showObjectItself) {\n        // remove ROOT only\n        this.orderedPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();\n      } else {\n        // remove ROOT and object itself from path\n        this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();\n      }\n      this.orderedPath.forEach((pathObject) => {\n        this.addNameListenerFor(pathObject.domainObject);\n      });\n    }\n  },\n  unmounted() {\n    if (this.abortController) {\n      this.abortController.abort();\n    }\n    Object.values(this.nameChangeListeners).forEach((unlisten) => {\n      unlisten();\n    });\n  },\n  methods: {\n    /**\n     * Generate the hash url for the given object path, removing the '/ROOT' prefix if present.\n     * @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath\n     */\n    navigateToPath(objectPath) {\n      /** @type {string} */\n      const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;\n\n      return path.replace('ROOT/', '');\n    },\n    updateObjectPathName(keyString, newName) {\n      this.orderedPath = this.orderedPath.map((pathObject) => {\n        if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {\n          return {\n            ...pathObject,\n            domainObject: { ...pathObject.domainObject, name: newName }\n          };\n        }\n        return pathObject;\n      });\n    },\n    removeNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString]();\n        delete this.nameChangeListeners[keyString];\n      }\n    },\n    addNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (!this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString] = this.openmct.objects.observe(\n          domainObject,\n          'name',\n          this.updateObjectPathName.bind(this, keyString)\n        );\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ObjectPathString.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    v-if=\"orderedPath.length\"\n    class=\"c-object-path-string\"\n    :aria-label=\"`${domainObject.name} Object Path`\"\n    role=\"navigation\"\n  >\n    {{ orderedPathStr }}\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    readOnly: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    },\n    showObjectItself: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    },\n    objectPath: {\n      type: Array,\n      default() {\n        return null;\n      }\n    }\n  },\n  data() {\n    return {\n      orderedPath: [],\n      orderedPathStr: ''\n    };\n  },\n  async mounted() {\n    this.abortController = new AbortController();\n    this.nameChangeListeners = {};\n    const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n\n    if (keyString && this.keyString !== keyString) {\n      this.keyString = keyString;\n      this.originalPath = [];\n\n      let rawPath = null;\n      if (this.objectPath === null) {\n        try {\n          rawPath = await this.openmct.objects.getOriginalPath(\n            keyString,\n            [],\n            this.abortController.signal\n          );\n        } catch (error) {\n          // aborting the search is ok, everything else should be thrown\n          if (error.name !== 'AbortError') {\n            throw error;\n          }\n        }\n      } else {\n        rawPath = this.objectPath;\n      }\n\n      const pathWithDomainObject = rawPath.map((domainObject, index, pathArray) => {\n        let key = this.openmct.objects.makeKeyString(domainObject.identifier);\n        const objectPath = pathArray.slice(index);\n\n        return {\n          domainObject,\n          key,\n          objectPath\n        };\n      });\n      if (this.showObjectItself) {\n        // remove ROOT only\n        this.orderedPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();\n      } else {\n        // remove ROOT and object itself from path\n        this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();\n      }\n\n      this.orderedPath.forEach((pathObject) => {\n        this.orderedPathStr = this.orderedPathStr.concat('/').concat(pathObject.domainObject.name);\n      });\n    }\n  },\n  unmounted() {\n    if (this.abortController) {\n      this.abortController.abort();\n    }\n    Object.values(this.nameChangeListeners).forEach((unlisten) => {\n      unlisten();\n    });\n  },\n  methods: {\n    /**\n     * Generate the hash url for the given object path, removing the '/ROOT' prefix if present.\n     * @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath\n     */\n    navigateToPath(objectPath) {\n      /** @type {string} */\n      const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;\n\n      return path.replace('ROOT/', '');\n    },\n    updateObjectPathName(keyString, newName) {\n      this.orderedPath = this.orderedPath.map((pathObject) => {\n        if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {\n          return {\n            ...pathObject,\n            domainObject: { ...pathObject.domainObject, name: newName }\n          };\n        }\n        return pathObject;\n      });\n    },\n    removeNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString]();\n        delete this.nameChangeListeners[keyString];\n      }\n    },\n    addNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (!this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString] = this.openmct.objects.observe(\n          domainObject,\n          'name',\n          this.updateObjectPathName.bind(this, keyString)\n        );\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ObjectView.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div>\n    <div\n      ref=\"objectViewWrapper\"\n      role=\"region\"\n      :aria-label=\"ariaLabel\"\n      class=\"c-object-view\"\n      :class=\"viewClasses\"\n    ></div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { toRaw } from 'vue';\n\nimport StyleRuleManager from '@/plugins/condition/StyleRuleManager';\nimport { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';\nimport stalenessMixin from '@/ui/mixins/staleness-mixin';\n\nimport { objectEquals } from '../../api/objects/object-utils.js';\nimport VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';\n\nexport default {\n  mixins: [stalenessMixin],\n  inject: ['openmct'],\n  props: {\n    showEditView: Boolean,\n    defaultObject: {\n      type: Object,\n      default: undefined\n    },\n    objectPath: {\n      type: Array,\n      default: () => {\n        return [];\n      }\n    },\n    layoutFontSize: {\n      type: String,\n      default: ''\n    },\n    layoutFont: {\n      type: String,\n      default: ''\n    },\n    objectViewKey: {\n      type: String,\n      default: ''\n    }\n  },\n  emits: ['change-action-collection'],\n  data() {\n    return {\n      domainObject: this.defaultObject\n    };\n  },\n  computed: {\n    ariaLabel() {\n      return this.domainObject ? `${this.domainObject.name} Object View` : 'Object View';\n    },\n    path() {\n      return this.domainObject && (this.currentObjectPath || this.objectPath);\n    },\n    objectFontStyle() {\n      return this.domainObject?.configuration?.fontStyle;\n    },\n    fontSize() {\n      return this.objectFontStyle ? this.objectFontStyle.fontSize : this.layoutFontSize;\n    },\n    font() {\n      return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;\n    },\n    viewClasses() {\n      let classes;\n\n      if (this.domainObject) {\n        classes = `is-object-type-${this.domainObject.type} ${this.isStale ? 'is-stale' : ''}`;\n      }\n\n      return classes;\n    }\n  },\n  beforeUnmount() {\n    this.clear();\n    if (this.releaseEditModeHandler) {\n      this.releaseEditModeHandler();\n    }\n\n    if (this.stopListeningStyles) {\n      this.stopListeningStyles();\n    }\n\n    if (this.stopListeningFontStyles) {\n      this.stopListeningFontStyles();\n    }\n\n    if (this.styleRuleManager) {\n      this.styleRuleManager.destroy();\n      delete this.styleRuleManager;\n    }\n\n    if (this.actionCollection) {\n      this.actionCollection.destroy();\n      delete this.actionCollection;\n    }\n    if (this.visibilityObserver) {\n      this.visibilityObserver.destroy();\n    }\n    this.$refs.objectViewWrapper.removeEventListener('dragover', this.onDragOver, {\n      capture: true\n    });\n    this.$refs.objectViewWrapper.removeEventListener('drop', this.editIfEditable, {\n      capture: true\n    });\n    this.$refs.objectViewWrapper.removeEventListener('drop', this.addObjectToParent);\n  },\n  created() {\n    this.debounceUpdateView = _.debounce(this.updateView, 10);\n  },\n  mounted() {\n    this.visibilityObserver = new VisibilityObserver(\n      this.$refs.objectViewWrapper,\n      this.openmct.element\n    );\n    this.updateView();\n    this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {\n      capture: true\n    });\n    this.$refs.objectViewWrapper.addEventListener('drop', this.editIfEditable, {\n      capture: true\n    });\n    this.$refs.objectViewWrapper.addEventListener('drop', this.addObjectToParent);\n    if (this.domainObject) {\n      //This is to apply styles to subobjects in a layout\n      this.initObjectStyles();\n      this.triggerStalenessSubscribe(this.domainObject);\n    }\n    this.setupClockChangedEvent((domainObject) => {\n      this.triggerUnsubscribeFromStaleness(domainObject);\n      this.subscribeToStaleness(domainObject);\n    });\n  },\n  methods: {\n    clear() {\n      if (this.currentView) {\n        this.currentView.destroy();\n\n        if (this.$refs.objectViewWrapper) {\n          this.$refs.objectViewWrapper.innerHTML = '';\n        }\n\n        if (this.releaseEditModeHandler) {\n          this.releaseEditModeHandler();\n          delete this.releaseEditModeHandler;\n        }\n\n        if (this.styleRuleManager) {\n          this.styleRuleManager.destroy();\n          delete this.styleRuleManager;\n        }\n      }\n\n      delete this.viewContainer;\n      delete this.currentView;\n\n      if (this.removeSelectable) {\n        this.removeSelectable();\n        delete this.removeSelectable;\n      }\n\n      if (this.composition) {\n        this.composition._destroy();\n      }\n\n      this.triggerUnsubscribeFromStaleness(this.domainObject);\n\n      this.openmct.objectViews.off('clearData', this.clearData);\n      this.openmct.objectViews.off('reload', this.reload);\n      if (this.contextActionEvent) {\n        this.openmct.objectViews.off(this.contextActionEvent, this.performContextAction);\n      }\n    },\n    getStyleReceiver() {\n      let styleReceiver;\n\n      if (this.$refs.objectViewWrapper !== undefined) {\n        styleReceiver =\n          this.$refs.objectViewWrapper.querySelector('.js-style-receiver') ||\n          this.$refs.objectViewWrapper.querySelector(':first-child');\n\n        if (styleReceiver === null) {\n          styleReceiver = undefined;\n        }\n      }\n\n      return styleReceiver;\n    },\n    invokeEditModeHandler(editMode) {\n      let edit;\n\n      if (this.domainObject.locked) {\n        edit = false;\n      } else {\n        edit = editMode;\n      }\n\n      this.currentView.onEditModeChange(edit);\n    },\n    toggleEditView(editMode) {\n      this.clear();\n      this.updateView(true);\n    },\n    reload(domainObjectToReload) {\n      if (objectEquals(domainObjectToReload, this.domainObject)) {\n        this.updateView(true);\n        this.initObjectStyles();\n        this.triggerStalenessSubscribe(this.domainObject);\n      }\n    },\n    triggerStalenessSubscribe(object) {\n      if (this.openmct.telemetry.isTelemetryObject(object)) {\n        this.subscribeToStaleness(object);\n      }\n    },\n    updateStyle(styleObj) {\n      let elemToStyle = this.getStyleReceiver();\n\n      if (!styleObj || elemToStyle === undefined) {\n        return;\n      }\n\n      let keys = Object.keys(styleObj);\n\n      keys.forEach((key) => {\n        if (elemToStyle) {\n          if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) {\n            if (elemToStyle.style[key]) {\n              if (key === 'background-color') {\n                elemToStyle.style[key] = 'transparent';\n              } else {\n                elemToStyle.style[key] = '';\n              }\n            }\n          } else {\n            if (\n              !styleObj.isStyleInvisible &&\n              elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)\n            ) {\n              elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);\n            } else if (\n              styleObj.isStyleInvisible &&\n              !elemToStyle.classList.contains(styleObj.isStyleInvisible)\n            ) {\n              elemToStyle.classList.add(styleObj.isStyleInvisible);\n            }\n\n            elemToStyle.style[key] = styleObj[key];\n          }\n        }\n      });\n    },\n    updateView(immediatelySelect) {\n      this.clear();\n      if (!this.domainObject) {\n        return;\n      }\n\n      this.composition = this.openmct.composition.get(this.domainObject);\n\n      if (this.composition) {\n        this.loadComposition();\n      }\n\n      this.viewContainer = this.$refs.objectViewWrapper;\n      let provider = this.getViewProvider();\n      if (!provider) {\n        return;\n      }\n\n      let objectPath = this.currentObjectPath || this.objectPath;\n\n      if (provider.edit && this.showEditView) {\n        if (this.openmct.editor.isEditing()) {\n          this.currentView = provider.edit(toRaw(this.domainObject), true, objectPath);\n        } else {\n          this.currentView = provider.view(toRaw(this.domainObject), objectPath);\n        }\n\n        this.openmct.editor.on('isEditing', this.toggleEditView);\n        this.releaseEditModeHandler = () =>\n          this.openmct.editor.off('isEditing', this.toggleEditView);\n      } else {\n        this.currentView = provider.view(toRaw(this.domainObject), objectPath);\n\n        if (this.currentView.onEditModeChange) {\n          this.openmct.editor.on('isEditing', this.invokeEditModeHandler);\n          this.releaseEditModeHandler = () =>\n            this.openmct.editor.off('isEditing', this.invokeEditModeHandler);\n        }\n      }\n\n      this.currentView.show(this.viewContainer, this.openmct.editor.isEditing(), {\n        renderWhenVisible: this.visibilityObserver.renderWhenVisible\n      });\n\n      if (immediatelySelect) {\n        this.removeSelectable = this.openmct.selection.selectable(\n          this.$refs.objectViewWrapper,\n          this.getSelectionContext(),\n          true\n        );\n      }\n\n      this.contextActionEvent = `contextAction:${this.openmct.objects.makeKeyString(\n        this.domainObject.identifier\n      )}`;\n      this.openmct.objectViews.on('clearData', this.clearData);\n      this.openmct.objectViews.on('reload', this.reload);\n      this.openmct.objectViews.on(this.contextActionEvent, this.performContextAction);\n\n      this.$nextTick(() => {\n        this.updateStyle(this.styleRuleManager?.currentStyle);\n        this.setFontSize(this.fontSize);\n        this.setFont(this.font);\n        this.getActionCollection();\n      });\n    },\n    getActionCollection() {\n      if (this.actionCollection) {\n        this.actionCollection.destroy();\n      }\n\n      this.actionCollection = this.openmct.actions.getActionsCollection(\n        this.currentObjectPath || this.objectPath,\n        this.currentView\n      );\n      this.$emit('change-action-collection', this.actionCollection);\n    },\n    show(object, viewKey, immediatelySelect, currentObjectPath) {\n      this.updateStyle();\n\n      if (this.domainObject) {\n        this.triggerUnsubscribeFromStaleness(this.domainObject);\n      }\n\n      if (this.removeSelectable) {\n        this.removeSelectable();\n        delete this.removeSelectable;\n      }\n\n      if (this.composition) {\n        this.composition._destroy();\n      }\n\n      this.domainObject = object;\n\n      if (currentObjectPath) {\n        this.currentObjectPath = currentObjectPath;\n      }\n\n      this.viewKey = viewKey;\n\n      this.updateView(immediatelySelect);\n\n      this.triggerStalenessSubscribe(this.domainObject);\n      this.initObjectStyles();\n    },\n    initObjectStyles() {\n      if (!this.styleRuleManager) {\n        this.styleRuleManager = new StyleRuleManager(\n          this.domainObject.configuration?.objectStyles,\n          this.openmct,\n          this.updateStyle.bind(this),\n          true\n        );\n      } else {\n        this.styleRuleManager.updateObjectStyleConfig(\n          this.domainObject.configuration?.objectStyles\n        );\n      }\n\n      if (this.stopListeningStyles) {\n        this.stopListeningStyles();\n      }\n\n      this.stopListeningStyles = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.objectStyles',\n        (newObjectStyle) => {\n          //Updating styles in the inspector view will trigger this so that the changes are reflected immediately\n          this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);\n        }\n      );\n\n      this.stopListeningFontStyles = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.fontStyle',\n        (newFontStyle) => {\n          this.setFontSize(newFontStyle.fontSize);\n          this.setFont(newFontStyle.font);\n        }\n      );\n    },\n    loadComposition() {\n      return this.composition.load();\n    },\n    getSelectionContext() {\n      if (this.currentView && this.currentView.getSelectionContext) {\n        return this.currentView.getSelectionContext();\n      } else {\n        return { item: this.domainObject };\n      }\n    },\n    onDragOver(event) {\n      if (this.hasComposableDomainObject(event)) {\n        if (this.isEditingAllowed()) {\n          event.preventDefault();\n        } else {\n          event.stopPropagation();\n        }\n      }\n    },\n    addObjectToParent(event) {\n      if (this.hasComposableDomainObject(event) && this.composition) {\n        let composableDomainObject = this.getComposableDomainObject(event);\n        this.loadComposition().then(() => {\n          this.composition.add(composableDomainObject);\n        });\n\n        event.preventDefault();\n        event.stopPropagation();\n      }\n    },\n    getViewKey() {\n      let viewKey = this.viewKey;\n      if (this.objectViewKey) {\n        viewKey = this.objectViewKey;\n      }\n\n      return viewKey;\n    },\n    getViewProvider() {\n      let provider = this.openmct.objectViews.getByProviderKey(this.getViewKey());\n\n      if (!provider) {\n        let objectPath = this.currentObjectPath || this.objectPath;\n        provider = this.openmct.objectViews.get(this.domainObject, objectPath)[0];\n        if (!provider) {\n          return;\n        }\n      }\n\n      return provider;\n    },\n    editIfEditable(event) {\n      let objectPath = this.currentObjectPath || this.objectPath;\n      let provider = this.getViewProvider();\n      if (\n        provider &&\n        provider.canEdit &&\n        provider.canEdit(toRaw(this.domainObject), objectPath) &&\n        this.isEditingAllowed() &&\n        !this.openmct.editor.isEditing()\n      ) {\n        this.openmct.editor.edit();\n      }\n    },\n    hasComposableDomainObject(event) {\n      return event.dataTransfer.types.includes('openmct/composable-domain-object');\n    },\n    getComposableDomainObject(event) {\n      let serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object');\n\n      return JSON.parse(serializedDomainObject);\n    },\n    clearData(domainObject) {\n      if (domainObject) {\n        let clearKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n        let currentObjectKeyString = this.openmct.objects.makeKeyString(\n          this.domainObject.identifier\n        );\n\n        if (clearKeyString === currentObjectKeyString) {\n          if (this.currentView.onClearData) {\n            this.currentView.onClearData();\n          }\n        }\n      } else {\n        if (this.currentView.onClearData) {\n          this.currentView.onClearData();\n        }\n      }\n    },\n    performContextAction(...args) {\n      if (this?.currentView?.contextAction) {\n        this.currentView.contextAction(...args);\n      }\n    },\n    isEditingAllowed() {\n      let browseObject = this.openmct.layout.$refs.browseObject.domainObject;\n      let objectPath = this.currentObjectPath || this.objectPath;\n      let parentObject = objectPath[1];\n\n      return [browseObject, parentObject, this.domainObject].every(\n        (object) => object && !object.locked\n      );\n    },\n    setFontSize(newSize) {\n      let elemToStyle = this.getStyleReceiver();\n\n      if (elemToStyle !== undefined) {\n        elemToStyle.dataset.fontSize = newSize;\n      }\n    },\n    setFont(newFont) {\n      let elemToStyle = this.getStyleReceiver();\n\n      if (elemToStyle !== undefined) {\n        elemToStyle.dataset.font = newFont;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ProgressBar.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-progress-bar\">\n    <div\n      class=\"c-progress-bar__bar\"\n      :class=\"{ '--indeterminate': !progressPerc }\"\n      :style=\"styleBarWidth\"\n      role=\"progressbar\"\n      :aria-valuenow=\"progressPerc\"\n      aria-valuemin=\"0\"\n      aria-valuemax=\"100\"\n    ></div>\n    <div v-if=\"progressText !== ''\" class=\"c-progress-bar__text\">\n      <span v-if=\"progressPerc > 0\">{{ progressPerc }}% complete.</span>\n      {{ progressText }}\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    progressPerc: {\n      type: Number,\n      default: 0\n    },\n    progressText: {\n      type: String,\n      default: ''\n    }\n  },\n  computed: {\n    styleBarWidth() {\n      return this.progressPerc ? `width: ${this.progressPerc}%;` : '';\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/SearchComponent.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-search\" v-bind=\"$attrs\" :class=\"{ 'is-active': active }\">\n    <input\n      class=\"c-search__input\"\n      aria-label=\"Search Input\"\n      tabindex=\"0\"\n      type=\"search\"\n      :value=\"value\"\n      v-bind=\"$attrs\"\n      @click=\"() => $emit('click')\"\n      @input=\"($event) => $emit('input', $event.target.value)\"\n    />\n    <a v-if=\"value\" class=\"c-search__clear-input icon-x-in-circle\" @click=\"clearInput\"></a>\n    <slot></slot>\n  </div>\n</template>\n\n<script>\nexport default {\n  inheritAttrs: false,\n  props: {\n    value: {\n      type: String,\n      default: ''\n    }\n  },\n  emits: ['input', 'clear', 'click'],\n  data() {\n    return {\n      active: false\n    };\n  },\n  watch: {\n    value(inputValue) {\n      this.active = inputValue.length > 0;\n    }\n  },\n  methods: {\n    clearInput() {\n      // Clear the user's input and set 'active' to false\n      this.$emit('clear', '');\n      this.active = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/TimeSystemAxis.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"axisHolder\" class=\"c-timesystem-axis\">\n    <div v-if=\"showAheadBehind\" class=\"c-ta-abi\" :class=\"aheadOrBehindCSSClass\">\n      <div class=\"c-ta-abi__icon icon-clock\"></div>\n      <div class=\"c-ta-abi__connector\"></div>\n      <div class=\"c-ta-abi__text\">{{ formattedAheadBehindDuration }}</div>\n    </div>\n    <div ref=\"lineWrapper\" class=\"c-timesystem-axis__line-wrapper\" :style=\"lineWrapperStyle\">\n      <div\n        ref=\"nowMarker\"\n        class=\"c-timesystem-axis__mb-line\"\n        :style=\"nowMarkerStyle\"\n        aria-label=\"Now Marker\"\n      >\n        <div\n          v-if=\"showAheadBehind\"\n          ref=\"aheadBehindMarker\"\n          class=\"c-timesystem-axis__ahead-behind-line\"\n          :class=\"aheadOrBehindCSSClass\"\n          :style=\"aheadBehindMarkerStyle\"\n          aria-label=\"Ahead Behind Marker\"\n        >\n          <svg\n            class=\"c-timesystem-axis__ahead-behind-connector\"\n            viewBox=\"0 0 100 100\"\n            preserveAspectRatio=\"none\"\n          >\n            <polygon\n              class=\"c-timesystem-axis__ahead-behind-connector--ahead\"\n              points=\"0 0 100 0 100 100\"\n            ></polygon>\n            <polygon\n              class=\"c-timesystem-axis__ahead-behind-connector--behind\"\n              points=\"0 0 100 0 0 100\"\n            ></polygon>\n          </svg>\n        </div>\n      </div>\n    </div>\n    <svg class=\"c-timesystem-axis__ticks\" :width=\"svgWidth\" :height=\"svgHeight\">\n      <g class=\"axis\" :transform=\"axisTransform\"></g>\n    </svg>\n  </div>\n</template>\n\n<script>\nconst AXES_PADDING = 20;\n\nimport { axisTop } from 'd3-axis';\nimport { scaleLinear, scaleUtc } from 'd3-scale';\nimport { select } from 'd3-selection';\nimport { inject, onMounted, reactive, ref } from 'vue';\n\nimport utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';\n\nimport { getPreciseDuration } from '../../utils/duration';\nimport { useAlignment } from '../composables/alignmentContext';\nimport { useResizeObserver } from '../composables/resize';\n\nconst PADDING = 1;\nconst PIXELS_PER_TICK = 100;\nconst PIXELS_PER_TICK_WIDE = 200;\nconst TIME_AXIS_LINE_Y = 20;\nconst executionMonitorStates = [\n  {\n    key: 'nominal',\n    label: 'Nominal'\n  },\n  {\n    key: 'behind',\n    label: 'Behind by'\n  },\n  {\n    key: 'ahead',\n    label: 'Ahead by'\n  }\n];\n\nexport default {\n  inject: ['openmct', 'domainObject', 'path'],\n  props: {\n    bounds: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    timeSystem: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    contentHeight: {\n      type: Number,\n      default() {\n        return 0;\n      }\n    },\n    renderingEngine: {\n      type: String,\n      default() {\n        return 'svg';\n      }\n    },\n    aheadBehind: {\n      type: Object,\n      default() {\n        return {\n          duration: 0,\n          status: false\n        };\n      }\n    }\n  },\n  setup() {\n    const axisHolder = ref(null);\n    const { size: containerSize, startObserving } = useResizeObserver();\n    const svgWidth = ref(0);\n    const svgHeight = ref(0);\n    const axisTransform = ref(`translate(0,${TIME_AXIS_LINE_Y})`);\n    const alignmentOffset = ref(0);\n    const leftAlignmentOffset = ref(0);\n    const alignmentStyle = ref({ margin: `0 0 0 0` });\n    const nowMarkerStyle = reactive({\n      height: '0px',\n      left: '0px'\n    });\n    const aheadBehindMarkerStyle = reactive({\n      width: '0px'\n    });\n    const lineWrapperStyle = reactive({\n      height: '0px'\n    });\n    const showAheadBehind = ref(false);\n    // The aheadOrBehindCSSClass has a default value of --ahead, but it will be hidden if there is not value for ahead/behind time\n    const aheadOrBehindCSSClass = ref('--ahead');\n    const formattedAheadBehindDuration = ref('');\n\n    onMounted(() => {\n      startObserving(axisHolder.value);\n    });\n\n    const domainObject = inject('domainObject');\n    const objectPath = inject('path');\n    const openmct = inject('openmct');\n    const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);\n\n    return {\n      axisHolder,\n      containerSize,\n      alignmentData,\n      svgWidth,\n      svgHeight,\n      axisTransform,\n      alignmentOffset,\n      leftAlignmentOffset,\n      alignmentStyle,\n      nowMarkerStyle,\n      openmct,\n      aheadBehindMarkerStyle,\n      lineWrapperStyle,\n      showAheadBehind,\n      aheadOrBehindCSSClass,\n      formattedAheadBehindDuration\n    };\n  },\n  watch: {\n    alignmentData: {\n      handler() {\n        let leftOffset = 0;\n        const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;\n        if (this.alignmentData.leftWidth) {\n          leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;\n        }\n        this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;\n        this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;\n        this.alignmentOffset =\n          this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;\n        this.alignmentStyle = {\n          margin: `0 ${this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`\n        };\n        this.refresh();\n      },\n      deep: true\n    },\n    bounds(newBounds) {\n      this.setDimensions();\n      this.drawAxis(newBounds, this.timeSystem);\n      this.updateTimeAxisMarkers();\n    },\n    timeSystem(newTimeSystem) {\n      this.setDimensions();\n      this.drawAxis(this.bounds, newTimeSystem);\n      this.updateTimeAxisMarkers();\n    },\n    contentHeight() {\n      this.updateLineWrapper();\n      this.updateTimeAxisMarkers();\n    },\n    aheadBehind() {\n      this.updateAheadBehindSettings();\n      this.updateTimeAxisMarkers();\n    },\n    containerSize: {\n      handler() {\n        this.resize();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    if (this.renderingEngine === 'svg') {\n      this.useSVG = true;\n    }\n\n    this.container = select(this.axisHolder);\n    this.axisElement = this.container.select('.c-timesystem-axis__ticks').select('g.axis');\n\n    this.refresh();\n    this.resize();\n  },\n  unmounted() {\n    clearInterval(this.resizeTimer);\n  },\n  methods: {\n    resize() {\n      if (this.axisHolder.clientWidth - this.alignmentOffset !== this.width) {\n        this.refresh();\n      }\n    },\n    refresh() {\n      this.setDimensions();\n      this.drawAxis(this.bounds, this.timeSystem);\n      this.updateAheadBehindSettings();\n      this.updateLineWrapper();\n      this.updateNowMarker();\n      this.updateAheadBehindMarker();\n    },\n    updateAheadBehindSettings() {\n      this.showAheadBehind = !this.isNominal() && this.aheadBehind.duration > 0;\n      this.aheadOrBehindCSSClass = this.getAheadOrBehindCSSClass();\n      this.aheadBehindDuration = this.aheadBehind.duration * 60 * 1000;\n      this.formattedAheadBehindDuration = getPreciseDuration(this.aheadBehindDuration, {\n        excludeMilliSeconds: true,\n        useDayFormat: true\n      });\n    },\n    getAheadOrBehindCSSClass() {\n      let cssClass = '';\n      if (this.isBehind()) {\n        cssClass = '--behind';\n      } else if (this.isAhead()) {\n        cssClass = '--ahead';\n      }\n\n      return cssClass;\n    },\n    isBehind() {\n      return (\n        this.aheadBehind.status &&\n        this.aheadBehind.duration &&\n        this.aheadBehind.status === executionMonitorStates[1].key\n      );\n    },\n    isAhead() {\n      return (\n        this.aheadBehind.status &&\n        this.aheadBehind.duration &&\n        this.aheadBehind.status === executionMonitorStates[2].key\n      );\n    },\n    isNominal() {\n      return (\n        !this.aheadBehind.duration ||\n        !this.aheadBehind.status ||\n        this.aheadBehind.status === executionMonitorStates[0].key\n      );\n    },\n    updateTimeAxisMarkers() {\n      this.updateNowMarker();\n      this.updateAheadBehindMarker();\n    },\n    updateLineWrapper() {\n      const lineWrapper = this.$refs.lineWrapper;\n      if (lineWrapper) {\n        this.lineWrapperStyle.height = this.contentHeight - TIME_AXIS_LINE_Y + 'px';\n      }\n    },\n    updateNowMarker() {\n      const nowMarker = this.$refs.nowMarker;\n      if (nowMarker) {\n        nowMarker.classList.remove('hidden');\n        this.nowMarkerStyle.height = this.contentHeight - TIME_AXIS_LINE_Y + 'px';\n        const nowTimeStamp = this.openmct.time.now();\n        const now = this.xScale(nowTimeStamp);\n        this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;\n        if (now < 0 || now > this.width) {\n          nowMarker.classList.add('hidden');\n        }\n      }\n    },\n    updateAheadBehindMarker() {\n      const aheadBehindMarker = this.$refs.aheadBehindMarker;\n      if (aheadBehindMarker) {\n        aheadBehindMarker.classList.remove('hidden');\n\n        const nowTimeStamp = this.openmct.time.now();\n        const now = this.xScale(nowTimeStamp);\n\n        if (now < 0 || now > this.width || this.isNominal()) {\n          aheadBehindMarker.classList.add('hidden');\n          this.aheadBehindMarkerStyle.width = '0px';\n        } else {\n          //We need the delta - we don't care if it's ahead or behind here.\n          const relativeAheadBehindDuration = this.aheadBehindDuration + nowTimeStamp;\n          const delta = this.xScale(relativeAheadBehindDuration) - now;\n          this.aheadBehindMarkerStyle.width = delta + 'px';\n        }\n      }\n    },\n    setDimensions() {\n      this.width = this.axisHolder.clientWidth - (this.alignmentOffset ?? 0);\n      this.height = Math.round(this.axisHolder.getBoundingClientRect().height);\n\n      if (this.useSVG) {\n        this.svgWidth = this.width;\n        this.svgHeight = this.height;\n      } else {\n        this.svgHeight = 50;\n      }\n    },\n    drawAxis(bounds, timeSystem) {\n      let viewBounds = Object.create(bounds);\n\n      this.setScale(viewBounds, timeSystem);\n      this.setAxis(viewBounds);\n      this.axisElement.call(this.xAxis);\n      this.updateNowMarker();\n      this.updateAheadBehindMarker();\n    },\n    setScale(bounds, timeSystem) {\n      if (!this.width) {\n        return;\n      }\n\n      if (timeSystem === undefined) {\n        timeSystem = this.openmct.time.getTimeSystem();\n      }\n\n      if (timeSystem.isUTCBased) {\n        this.xScale = scaleUtc();\n        this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);\n      } else {\n        this.xScale = scaleLinear();\n        this.xScale.domain([bounds.start, bounds.end]);\n      }\n\n      this.xScale.range([PADDING, this.width - PADDING * 2]);\n    },\n    setAxis() {\n      this.xAxis = axisTop(this.xScale);\n      this.xAxis.tickFormat(utcMultiTimeFormat);\n\n      if (this.width > 1800) {\n        this.xAxis.ticks(this.width / PIXELS_PER_TICK_WIDE);\n      } else {\n        this.xAxis.ticks(this.width / PIXELS_PER_TICK);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ToggleSwitch.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-toggle-switch\">\n    <label class=\"c-toggle-switch__control\">\n      <input :id=\"id\" type=\"checkbox\" :checked=\"checked\" @change=\"onUserSelect($event)\" />\n      <span class=\"c-toggle-switch__slider\" role=\"switch\" :aria-label=\"name\"></span>\n    </label>\n    <div v-if=\"label && label.length\" class=\"c-toggle-switch__label\">\n      {{ label }}\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    id: {\n      type: String,\n      required: true\n    },\n    label: {\n      type: String,\n      required: false,\n      default: ''\n    },\n    name: {\n      type: String,\n      required: false,\n      default: ''\n    },\n    checked: Boolean\n  },\n  emits: ['change'],\n  methods: {\n    onUserSelect(event) {\n      this.$emit('change', event.target.checked);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/ViewControl.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <span\n    :class=\"[controlClass, { 'c-disclosure-triangle--expanded': value }, { 'is-enabled': enabled }]\"\n    tabindex=\"0\"\n    role=\"button\"\n    :aria-label=\"ariaLabelValue\"\n    :aria-expanded=\"value ? 'true' : 'false'\"\n    @click=\"handleClick\"\n    @keydown.enter=\"handleClick\"\n  ></span>\n</template>\n\n<script>\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    },\n    enabled: {\n      // Provided to allow the view-control to still occupy space without displaying a control icon.\n      // Used as such in the tree - when a node doesn't have children, set disabled to true.\n      type: Boolean,\n      default: false\n    },\n    controlClass: {\n      type: String,\n      default: 'c-disclosure-triangle'\n    },\n    domainObject: {\n      type: Object,\n      default: () => {}\n    }\n  },\n  emits: ['input'],\n  computed: {\n    ariaLabelValue() {\n      const name = this.domainObject.name ? ` ${this.domainObject.name}` : '';\n      const type = this.domainObject.type ? ` ${this.domainObject.type}` : '';\n\n      return `${this.value ? 'Collapse' : 'Expand'}${name}${type}`;\n    }\n  },\n  methods: {\n    handleClick(event) {\n      event.stopPropagation();\n      this.$emit('input', !this.value);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/object-frame.scss",
    "content": ".c-so-view {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  // &__container{\n  //   display: contents;\n  // }\n\n  /*************************** HEADER */\n  &__header {\n    flex: 0 0 auto;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: $interiorMarginSm;\n    overflow: hidden;\n    padding: 3px;\n    @include smallerControlButtons; // Make button in frame headers a bit smaller\n\n    .c-object-label {\n      font-size: 1.05em;\n      min-width: 20%;\n\n      &__type-icon {\n        opacity: $objectLabelTypeIconOpacity;\n      }\n\n      &__name {\n        color: $objectLabelNameColorFg;\n      }\n    }\n  }\n\n  &:not(.c-so-view--no-frame) {\n    border: $browseFrameBorder;\n    @include browseFrameBorder;\n    padding: $interiorMarginObjectFrameVertical $interiorMarginObjectFrameHorizontal;\n    \n    .is-editing & {\n      background: rgba($colorBodyBg, 0.8);\n      @include browseFrameBorder;\n    }\n  }\n\n\n\n\n  /*************************** FRAME CONTROLS */\n  &__frame-controls {\n    display: flex;\n    flex: 0 1 auto;\n    overflow: hidden;\n\n    &__btns,\n    &__more {\n      flex: 0 0 auto;\n    }\n\n    .--width-less-than-220 &,\n    .--width-less-than-600 & {\n      [class*='__label'] {\n        // button labels\n        display: none !important;\n      }\n    }\n  }\n\n  /*************************** HIDDEN FRAME */\n  &--no-frame {\n    > .c-so-view__header {\n      visibility: hidden;\n      pointer-events: none;\n      position: absolute;\n      top: 0;\n      right: 0;\n      bottom: auto;\n      left: 0;\n      z-index: 10;\n\n      .c-object-label {\n        visibility: hidden;\n      }\n\n      .c-so-view__frame-controls {\n        background: $frameControlsColorBg;\n        border-radius: $controlCr;\n        box-shadow: $frameControlsShdw;\n        padding: 1px;\n        pointer-events: all;\n\n        .c-icon-button {\n          color: $frameControlsColorFg;\n\n          &:hover {\n            background: rgba($frameControlsColorFg, 0.3);\n          }\n        }\n\n        &__btns {\n          display: none;\n        }\n\n        &:hover {\n          [class*='__btns'] {\n            display: block;\n          }\n        }\n\n        [class*='__label'] {\n          // button labels\n          display: none;\n        }\n      }\n    }\n\n    &.c-so-view--flexible-layout,\n    &.c-so-view--layout {\n      // For sub-layouts with hidden frames, completely hide the header to avoid overlapping buttons\n      > .c-so-view__header {\n        display: none;\n      }\n    }\n\n    /* HOVERS */\n    &:hover {\n      > .c-so-view__header {\n        visibility: visible;\n      }\n    }\n\n    &[class*='is-status'] {\n      border: $borderMissing;\n    }\n  }\n\n  /*************************** OBJECT VIEW */\n  &__object-view {\n    flex: 1 1 auto;\n    height: 0; // Chrome 73 overflow bug fix\n    overflow: auto;\n    //To accommodate independent time conductor controls\n    display: flex;\n    flex-direction: column;\n\n    .u-fills-container {\n      // Expand component types that fill a container\n      @include abs();\n    }\n  }\n\n  &.has-complex-content {\n    > .c-so-view__view-large {\n      display: block;\n    }\n  }\n\n  &.is-status--missing {\n    border: $borderMissing;\n  }\n\n  // Leave for debugging\n  //&.--width-less-than-600 { background: rgba(orange, 0.2) !important; }\n  //&.--width-less-than-220 { background: rgba(red, 0.2) !important; }\n}\n\n.l-angular-ov-wrapper {\n  // This element is the recipient for object styling; cannot be display: contents\n  overflow: hidden;\n  display: block;\n  height: 100%;\n}\n"
  },
  {
    "path": "src/ui/components/object-label.scss",
    "content": ".c-object-label {\n  // <a> tag and draggable element that holds type icon and name.\n  // Used mostly in trees and lists\n  @include ellipsize();\n  display: flex;\n  align-items: center;\n  flex: 0 1 auto;\n\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n\n  &.is-status--draft {\n    .c-object-label__type-icon {\n      &:after {\n        color: $colorStatusAlert;\n        font-family: symbolsfont;\n        content: $glyph-icon-draft;\n        margin-left: $interiorMarginSm;\n      }\n    }\n  }\n\n  &__name {\n    @include ellipsize();\n    color: $objectLabelNameColorFg;\n    display: inline;\n    padding: 1px 0;\n  }\n\n  &__type-icon {\n    // Type icon. Must be an HTML entity to allow inclusion of alias indicator.\n    display: block;\n    flex: 0 0 auto;\n    font-size: 1.1em;\n    opacity: $objectLabelTypeIconOpacity;\n  }\n\n  .is-status__indicator {\n    position: absolute;\n    right: -3px;\n    top: -3px;\n    transform: scale(0.5);\n  }\n\n  &.is-status--missing,\n  &.is-status--suspect {\n    [class*='__type-icon'] {\n      &:before,\n      &:after {\n        opacity: $opacityMissing;\n      }\n    }\n  }\n\n  &.is-status--notebook-default {\n    &:after {\n      content: $glyph-icon-notebook-page;\n      display: block;\n      margin-left: $interiorMargin;\n    }\n  }\n\n  &.is-status--current {\n    &:after {\n      content: $glyph-icon-asterisk;\n      display: block;\n      margin-left: $interiorMargin;\n      font-family: symbolsfont;\n    }\n  }\n}\n\n.c-tree .c-object-label {\n  border-radius: $controlCr;\n  padding: $interiorMarginSm 1px;\n\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n\n  &__name {\n    display: inline;\n    width: 100%;\n  }\n\n  &__type-icon {\n    color: $colorItemTreeIcon;\n    font-size: 1.25em;\n    margin-right: $interiorMarginSm;\n    opacity: 1;\n    min-width: $treeTypeIconW;\n  }\n}\n"
  },
  {
    "path": "src/ui/components/progress-bar.scss",
    "content": "/******************************************************** PROGRESS BAR */\n@keyframes progressIndeterminate {\n  0% {\n    transform:scaleX(0);\n  }\n  90% {\n    transform:scaleX(1);\n    opacity: 1;\n  }\n  100% {\n    opacity: 0;\n  }\n}\n\n.c-progress-bar {\n  background: $colorProgressBarHolder;\n  display: block;\n  min-height: $progressBarMinH;\n  overflow: hidden;\n  width: 100%;\n\n  &__bar {\n    background: $colorProgressBar;\n    transform-origin: left;\n\n    &.--indeterminate {\n      @include abs();\n      animation: progressIndeterminate 1.5s ease-in infinite;\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/components/search.scss",
    "content": "@mixin visibleRegexButton {\n  opacity: 1;\n  padding: 1px 3px;\n  min-width: 24px;\n}\n\n.c-search {\n  @include wrappedInput();\n  padding-top: 2px;\n  padding-bottom: 2px;\n\n  &:before {\n    // Mag glass icon\n    content: $glyph-icon-magnify;\n    body.mobile & { // Make search icon stand out in mobile\n      opacity: 1;\n    }\n  }\n\n  &__use-regex {\n    // Button\n    $c: $colorBodyFg;\n    background: rgba($c, 0.2);\n    border: 1px solid rgba($c, 0.3);\n    color: $c;\n    border-radius: $controlCr;\n    font-weight: bold;\n    letter-spacing: 1px;\n    font-size: 0.8em;\n    margin-left: $interiorMarginSm;\n    min-width: 0;\n    opacity: 0;\n    order: 2;\n    overflow: hidden;\n    padding: 1px 0;\n    transform-origin: left;\n    @include transition($prop: min-width, $dur: $transOutTime);\n    width: 0;\n\n    &.is-active {\n      $c: $colorBtnActiveBg;\n      @include visibleRegexButton();\n      background: rgba($c, 0.3);\n      border-color: $c;\n      color: $c;\n    }\n  }\n\n  &__clear-input {\n    display: none;\n    order: 99;\n    padding: 1px 0;\n    body.mobile & {\n      display: block;\n    }\n\n  }\n\n  &.is-active {\n    body.mobile & { // In mobile, persist the expanded search bar instead of collapsing upon clicking away\n      background-color: rgba($colorHeadFg, 0.2) !important; \n      width: 50vw !important;\n    }\n    .c-search__use-regex {\n      margin-left: 0;\n    }\n\n    &:before {\n      width: 0;\n      body.mobile & {\n        width: auto;\n      }\n    }\n\n    input[type='text'],\n    input[type='search'] {\n      margin-left: 0;\n    }\n\n    @include hover {\n      .c-search__clear-input {\n        display: block;\n      }\n    }\n  }\n\n  input[type='text'],\n  input[type='search'] {\n    margin-left: $interiorMargin;\n    order: 3;\n    text-align: left;\n  }\n\n  @include hover {\n    .c-search__use-regex {\n      @include visibleRegexButton();\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/components/swim-lane/SwimLane.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    ref=\"swimLane\"\n    :class=\"[{ 'c-swimlane': !isNested, 'u-contents': isNested }, statusClass]\"\n    :style=\"gridTemplateColumnStyle\"\n    @mouseover.ctrl=\"showToolTip\"\n    @mouseleave=\"hideToolTip\"\n  >\n    <div\n      v-if=\"hideLabel === false\"\n      class=\"c-swimlane__lane-label c-object-label\"\n      :class=\"[swimlaneClass, statusClass]\"\n      :style=\"gridRowSpan\"\n    >\n      <div v-if=\"iconClass\" class=\"c-object-label__type-icon\" :class=\"iconClass\">\n        <span\n          v-if=\"status\"\n          class=\"is-status__indicator\"\n          :aria-label=\"`This item is ${status}`\"\n          :title=\"`This item is ${status}`\"\n        ></span>\n      </div>\n      <div class=\"c-object-label__name\">\n        <slot name=\"label\"></slot>\n      </div>\n      <div class=\"c-swimlane__lane-label-button-h\">\n        <button\n          v-if=\"!hideButton\"\n          class=\"c-button\"\n          :class=\"[buttonIcon, buttonPressed ? 'is-active' : '']\"\n          :title=\"buttonTitle\"\n          :aria-label=\"buttonTitle\"\n          @click=\"pressOnButton\"\n        />\n      </div>\n      <div\n        v-if=\"canShowResizeHandle\"\n        class=\"c-swimlane__handle horizontal\"\n        :style=\"{ height: `${resizeHandleHeight}px` }\"\n        @mousedown=\"mousedown\"\n      ></div>\n    </div>\n\n    <div\n      class=\"c-swimlane__lane-object\"\n      :style=\"{ 'min-height': minHeight }\"\n      :class=\"{ 'u-contents': showUcontents }\"\n    >\n      <slot name=\"object\"></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\n\nexport default {\n  mixins: [tooltipHelpers],\n  inject: ['openmct', 'mousedown', 'swimLaneLabelWidth'],\n  props: {\n    iconClass: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    status: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    minHeight: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    showUcontents: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    isHidden: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    hideLabel: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    isNested: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    canShowResizeHandle: {\n      type: Boolean,\n      default() {\n        return false;\n      }\n    },\n    resizeHandleHeight: {\n      type: Number,\n      required: false,\n      default() {\n        return 32;\n      }\n    },\n    spanRowsCount: {\n      type: Number,\n      default() {\n        return 0;\n      }\n    },\n    domainObject: {\n      type: Object,\n      default: undefined\n    },\n    hideButton: {\n      type: Boolean,\n      default() {\n        return true;\n      }\n    },\n    buttonTitle: {\n      type: String,\n      default() {\n        return null;\n      }\n    },\n    buttonIcon: {\n      type: String,\n      default() {\n        return null;\n      }\n    },\n    buttonClickOn: {\n      type: Function,\n      default() {\n        return () => {};\n      }\n    },\n    buttonClickOff: {\n      type: Function,\n      default() {\n        return () => {};\n      }\n    }\n  },\n  data() {\n    return {\n      buttonPressed: false,\n      labelWidth: 200\n    };\n  },\n  computed: {\n    gridRowSpan() {\n      if (this.spanRowsCount) {\n        return `grid-row: span ${this.spanRowsCount}`;\n      } else {\n        return '';\n      }\n    },\n    swimlaneClass() {\n      if (!this.spanRowsCount && !this.isNested) {\n        return 'c-swimlane__lane-label --span-cols';\n      }\n      return '';\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    },\n    gridTemplateColumnStyle() {\n      if (this.isNested) {\n        return {};\n      }\n\n      const columnWidth = this.swimLaneLabelWidth / 2;\n\n      return {\n        'grid-template-columns': `${columnWidth}px ${columnWidth}px 1fr`\n      };\n    }\n  },\n  methods: {\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'swimLane');\n    },\n    pressOnButton() {\n      this.buttonPressed = !this.buttonPressed;\n      if (this.buttonPressed) {\n        this.buttonClickOn();\n      } else {\n        this.buttonClickOff();\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/components/swim-lane/swimlane.scss",
    "content": "/*****************************************************************************\n* Open MCT, Copyright (c) 2014-2024, United States Government\n* as represented by the Administrator of the National Aeronautics and Space\n* Administration. All rights reserved.\n*\n* Open MCT is licensed under the Apache License, Version 2.0 (the\n* \"License\"); you may not use this file except in compliance with the License.\n* You may obtain a copy of the License at\n* http://www.apache.org/licenses/LICENSE-2.0.\n*\n* Unless required by applicable law or agreed to in writing, software\n* distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n* License for the specific language governing permissions and limitations\n* under the License.\n*\n* Open MCT includes source code licensed under additional open source\n* licenses. See the Open Source Licenses file (LICENSES.md) included with\n* this source code distribution or the Licensing information page available\n* at runtime from the About dialog for additional information.\n*****************************************************************************/\n\n@use 'sass:math';\n\n.c-swimlane {\n  $handleSize: 1px;\n  $handleMargin: 3px;\n  $handleHitSize: $handleMargin * 2 + $handleSize;\n\n  display: grid;\n  grid-template-columns: 100px 100px 1fr;\n  grid-column-gap: 1px;\n  grid-row-gap: 1px; // Used for grid within a swimlane for Plan views\n  min-height: max-content; // Plan and Gantt views: must use max-content to prevent swimlane from collapsing\n  width: 100%;\n\n  .is-object-type-time-strip & {\n    min-height: $btnStdH;\n\n  }\n\n  &__time-axis {\n    flex: 0 0 auto;\n    height: 32px;\n    overflow: visible;\n  }\n\n  &.is-status--draft {\n    background: $colorTimeStripDraftBg;\n  }\n\n  &__lane-label {\n    background: $colorTimeStripLabelBg;\n    color: $colorBodyFg;\n    overflow: visible;\n    padding: $interiorMarginSm $interiorMargin;\n  }\n\n  &__handle {\n    $size: $handleSize;\n    $margin: $handleMargin;\n    z-index: 2;\n\n    @include resizeHandleStyle($size, $margin);\n\n    @include abs();\n    display: none; // Set to display: block in .is-editing section below\n    //z-index: 1000;\n\n    &.vertical {\n      // Vertical resizing uses c-fl-frame__resize-handle\n    }\n\n    &.horizontal {\n      // Resizes in X dimension\n      left: auto;\n      right: math.floor($handleHitSize * -0.5);\n      width: $handleHitSize;\n    }\n  }\n\n  &__lane-object {\n    background: rgba(black, 0.1);\n    height: 100%;\n\n    .c-plan {\n      display: contents;\n    }\n\n    @include smallerControlButtons;\n  }\n\n  &__lane-label-button-h {\n    // Holds swimlane button(s)\n    flex: 1 1 auto;\n    text-align: right;\n  }\n\n  .--span-cols {\n    grid-column: span 2;\n  }\n\n  // Yet more brittle special case selecting...\n  .is-object-type-plan,\n  .is-object-type-gantt-chart {\n    display: contents;\n  }\n\n  .is-editing & {\n    //grid-column-gap: $handleHitSize;\n    .c-swimlane__handle {\n      display: block;\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/components/timesystem-axis.scss",
    "content": "@use 'sass:math';\n\n.c-timesystem-axis {\n  $h: 30px;\n  height: $h;\n\n  &__ticks {\n    // Ticks SVG\n    $lineC: $colorInteriorBorder;\n    text-rendering: geometricPrecision;\n    width: 100%;\n    height: 100%;\n\n    .domain {\n      display: none;\n    }\n\n    .tick {\n      line {\n        stroke: $lineC;\n      }\n\n      text {\n        // Tick labels\n        fill: $colorBodyFg;\n        paint-order: stroke;\n        font-weight: bold;\n      }\n    }\n  }\n\n  /******************************************** LINES */\n  $mbMarkerW: 16px;\n  $mbMarkerH: math.floor(math.div($mbMarkerW, 2));\n  &__line-wrapper,\n  &__mb-line,\n  &__ahead-behind-line {\n    pointer-events: none;\n    position: absolute;\n    z-index: 10;\n\n    &.hidden {\n      display: none;\n    }\n  }\n\n  &__line-wrapper {\n    position: absolute;\n    left: 0;\n    top: 20px;  // This must be kept in parity with JS constant TIME_AXIS_LINE_Y\n    right: 0;\n    overflow: hidden;\n  }\n\n  &__mb-line {\n    $c: $colorTimeRealtimeBtnBgMajor;\n    $w: 15px;\n    $wHalf: math.round(math.div($w, 2));\n    $transform: translateX(($wHalf - 1) * -1);\n\n    border-right: 2px dashed $c;\n    opacity: 0.5;\n    pointer-events: none;\n    width: 1px;\n    z-index: 10;\n\n    &:before,\n    &:after {\n      content: '';\n      display: block;\n      position: absolute;\n      width: 0;\n      height: 0;\n      transform: $transform;\n      border-left: $mbMarkerH solid transparent;\n      border-right: $mbMarkerH solid transparent;\n      border-top: $mbMarkerH solid $c;\n    }\n\n    &:after {\n      bottom: 0;\n      transform: $transform rotate(180deg);\n    }\n  }\n\n  &__ahead-behind-line {\n    $lineOffset: 1px;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    z-index: 0;\n\n    .c-timesystem-axis__ahead-behind-connector {\n      // SVG that holds \"connector\" polygon\n      height: $mbMarkerH;\n      position: absolute;\n      width: 100%;\n      top: 0;\n    }\n\n    &.--ahead {\n      $c: $colorABAhead;\n      border-right: 2px dashed $c;\n      left: 0;\n\n      .c-timesystem-axis__ahead-behind-connector {\n        right: $lineOffset;\n        polygon {\n          fill: $c;\n        }\n        &--behind {\n          display: none;\n        }\n      }\n    }\n\n    &.--behind {\n      $c: $colorABBehind;\n      border-left: 2px dashed $c;\n      right: 0;\n\n      .c-timesystem-axis__ahead-behind-connector {\n        left: $lineOffset;\n        polygon {\n          fill: $c;\n        }\n        &--ahead {\n          display: none;\n        }\n      }\n    }\n  }\n}\n\n.c-ta-abi {\n  // .c-timesystem-axis ahead-behind-indicator\n  $m: 0;\n  $p: 3px;\n  background: rgba($colorBodyBg, 0.7);\n  display: flex;\n  flex-direction: row;\n  font-size: 0.9em;\n  align-items: center;\n  gap: $interiorMarginSm;\n  position: absolute;\n  left: $m;\n  top: $m;\n  padding: $p;\n  white-space: nowrap;\n\n  &__icon {\n    order: 1;\n\n    .--behind & {\n      order: 2;\n    }\n  }\n\n  &__connector {\n    order: 2;\n\n    .--behind & {\n      order: 1;\n    }\n    &:before {\n      $s: 4.5px;\n      content: '';\n      height: 0;\n      width: 0;\n      display: block;\n      border: $s solid transparent;\n\n      .--ahead & {\n        $c: $colorABAhead;\n        border-top-color: $c;\n        border-right-color: $c;\n      }\n\n      .--behind & {\n        $c: $colorABBehind;\n        border-top-color: $c;\n        border-left-color: $c;\n      }\n    }\n  }\n\n  &__text {\n    order: 3;\n\n    &:before {\n      content: 'AHEAD ';\n    }\n\n    .--behind & {\n      &:before {\n        content: 'BEHIND ';\n      }\n    }\n  }\n\n  &.--ahead {\n    color: $colorABAhead;\n  }\n  &.--behind {\n    color: $colorABBehind;\n  }\n}\n"
  },
  {
    "path": "src/ui/components/toggle-switch.scss",
    "content": "@use 'sass:math';\n\n@mixin toggleSwitch($d: 12px, $m: 2px, $bg: $colorBtnBg) {\n  $br: math.div($d, 1.5);\n\n  .c-toggle-switch__slider {\n    background: $bg;\n    border-radius: $br;\n    height: $d + ($m * 2);\n    width: $d * 2 + $m * 2;\n\n    &:before {\n      // Knob\n      border-radius: floor($br * 0.8);\n      box-shadow: rgba(black, 0.4) 0 0 2px;\n      height: $d;\n      width: $d;\n      top: $m;\n      left: $m;\n      right: auto;\n    }\n  }\n}\n\n.c-toggle-switch {\n  cursor: pointer;\n  display: inline-flex;\n  gap: $interiorMarginSm;\n  align-items: center;\n  vertical-align: middle;\n\n  &__control,\n  &__label {\n    flex: 0 0 auto;\n  }\n\n  &__control {\n    cursor: pointer;\n    overflow: hidden;\n    display: block;\n  }\n\n  &__slider {\n    // Sits within __switch\n    display: inline-block;\n    position: relative;\n\n    &:before {\n      // Knob\n      background: $colorBtnFg; // TODO: make discrete theme constants for these colors\n      content: '';\n      display: block;\n      position: absolute;\n      transition: transform 100ms ease-in-out;\n    }\n  }\n\n  &__label {\n    white-space: nowrap;\n  }\n\n  input {\n    opacity: 0;\n    width: 0;\n    height: 0;\n\n    &:checked {\n      + .c-toggle-switch__slider {\n        background: $colorKey; // TODO: make discrete theme constants for these colors\n        &:before {\n          transform: translateX(100%);\n        }\n      }\n    }\n  }\n\n  @include toggleSwitch();\n}\n\n.c-toggle-switch--mini {\n  @include toggleSwitch($d: 9px, $m: 0px);\n}\n"
  },
  {
    "path": "src/ui/composables/alignmentContext.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/* eslint-disable func-style */\n\nimport { reactive } from 'vue';\n\n/** @type {Map<string, Alignment>} */\nconst alignmentMap = new Map();\n/**\n * Manages alignment for multiple y axes given an object path.\n * This is a Vue composition API utility function.\n * @param {Object} targetObject - The target to attach the event listener to.\n * @param {ObjectPath} objectPath - The path of the target object.\n * @param {import('../../../openmct.js').OpenMCT} openmct - The open mct API.\n * @returns {Object} An object containing alignment data and methods to update, remove, and reset alignment.\n */\nexport function useAlignment(targetObject, objectPath, openmct) {\n  /**\n   * Get the alignment key for the given path.\n   * @returns {string|undefined} The alignment key if found, otherwise undefined.\n   */\n  const getAlignmentKeyForPath = () => {\n    const keys = Array.from(alignmentMap.keys());\n    return objectPath\n      .map((domainObject) => openmct.objects.makeKeyString(domainObject.identifier))\n      .reverse()\n      .find((keyString) => keys.includes(keyString));\n  };\n\n  // Use the furthest ancestor's alignment if it exists, otherwise, use your own\n  let alignmentKey =\n    getAlignmentKeyForPath() || openmct.objects.makeKeyString(targetObject.identifier);\n\n  if (!alignmentMap.has(alignmentKey)) {\n    alignmentMap.set(\n      alignmentKey,\n      reactive({\n        leftWidth: 0,\n        rightWidth: 0,\n        multiple: false,\n        axes: {}\n      })\n    );\n  }\n\n  /**\n   * Reset any alignment data for the given key.\n   */\n  const reset = () => {\n    const key = getAlignmentKeyForPath();\n    if (key && alignmentMap.has(key)) {\n      alignmentMap.delete(key);\n    }\n  };\n\n  /**\n   * Given the axes ids and widths, calculate the max left and right widths and whether or not multiple left axes exist.\n   */\n  const processAlignment = () => {\n    const alignment = alignmentMap.get(alignmentKey);\n    const axesKeys = Object.keys(alignment.axes);\n    const leftAxes = axesKeys.filter((axis) => axis <= 2);\n    const rightAxes = axesKeys.filter((axis) => axis > 2);\n\n    alignment.leftWidth = leftAxes.reduce((sum, axis) => sum + (alignment.axes[axis] || 0), 0);\n    alignment.rightWidth = rightAxes.reduce((sum, axis) => sum + (alignment.axes[axis] || 0), 0);\n    alignment.multiple = leftAxes.length > 1;\n  };\n\n  /**\n   * @typedef {Object} RemoveParams\n   * @property {number} yAxisId - The ID of the y-axis to remove.\n   * @property {ObjectPath} [updateObjectPath] - The path of the object to update.\n   */\n\n  /**\n   * Unregister y-axis from width calculations.\n   * @param {RemoveParams} param0 - The object containing yAxisId and updateObjectPath.\n   */\n  const remove = ({ yAxisId, updateObjectPath } = {}) => {\n    const key = getAlignmentKeyForPath();\n    if (key) {\n      const alignment = alignmentMap.get(alignmentKey);\n      if (alignment.axes[yAxisId] !== undefined) {\n        delete alignment.axes[yAxisId];\n      }\n      processAlignment();\n    }\n  };\n\n  /**\n   * @typedef {Object} UpdateParams\n   * @property {number} width - The width of the y-axis.\n   * @property {number} yAxisId - The ID of the y-axis to update.\n   * @property {ObjectPath} [updateObjectPath] - The path of the object to update.\n   */\n\n  /**\n   * Update widths of a y axis given the id and path. The path is used to determine which ancestor should hold the alignment.\n   * @param {UpdateParams} param0 - The object containing width, yAxisId, and updateObjectPath.\n   */\n  const update = ({ width, yAxisId, updateObjectPath } = {}) => {\n    const key = getAlignmentKeyForPath();\n    if (key) {\n      const alignment = alignmentMap.get(alignmentKey);\n      if (alignment.axes[yAxisId] === undefined || width > alignment.axes[yAxisId]) {\n        alignment.axes[yAxisId] = width;\n      }\n      processAlignment();\n    }\n  };\n\n  return { alignment: alignmentMap.get(alignmentKey), update, remove, reset };\n}\n\n/**\n * @typedef {import('../../api/objects/ObjectAPI.js').DomainObject[]} ObjectPath\n */\n\n/**\n * @typedef {Object} Alignment\n * @property {number} leftWidth - The total width of the left axes.\n * @property {number} rightWidth - The total width of the right axes.\n * @property {boolean} multiple - Indicates if there are multiple left axes.\n * @property {Object.<string, number>} axes - A map of axis IDs to their widths.\n */\n"
  },
  {
    "path": "src/ui/composables/edit.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { ref } from 'vue';\n\nimport { useEventEmitter } from './event.js';\n\n/**\n * Provides a reactive `isEditing` property that reflects the current editing state of the\n * application.\n * @param {import('openmct').OpenMCT} openmct the Open MCT API\n * @returns {{\n *  isEditing: import('vue').Ref<boolean>\n * }}\n */\nexport function useIsEditing(openmct) {\n  const isEditing = ref(openmct.editor.isEditing());\n\n  useEventEmitter(openmct.editor, 'isEditing', (_isEditing) => {\n    isEditing.value = _isEditing;\n  });\n\n  return { isEditing };\n}\n"
  },
  {
    "path": "src/ui/composables/event.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/* eslint-disable func-style */\n\nimport { isRef, onBeforeMount, onBeforeUnmount, onMounted, watch } from 'vue';\n\n/**\n * Registers an event listener on the specified target and automatically removes it when the\n * component is unmounted.\n * This is a Vue composition API utility function.\n * @param {EventTarget} target - The target to attach the event listener to.\n * @param {string} event - The name of the event to listen for.\n * @param {Function} handler - The callback function to execute when the event is triggered.\n */\nexport function useEventListener(target, event, handler) {\n  const addListener = (el) => {\n    if (el) {\n      el.addEventListener(event, handler);\n    }\n  };\n\n  const removeListener = (el) => {\n    if (el) {\n      el.removeEventListener(event, handler);\n    }\n  };\n\n  // If target is a reactive ref, watch it for changes\n  if (isRef(target)) {\n    watch(\n      target,\n      (newTarget, oldTarget) => {\n        if (newTarget !== oldTarget) {\n          removeListener(oldTarget);\n          addListener(newTarget);\n        }\n      },\n      { immediate: true }\n    );\n  } else {\n    // Otherwise use lifecycle hooks to add/remove listener\n    onMounted(() => addListener(target));\n    onBeforeUnmount(() => removeListener(target));\n  }\n}\n\n/**\n * Registers an event listener on the specified EventEmitter instance and automatically removes it\n * when the component is unmounted.\n * This is a Vue composition API utility function.\n * @param {import('eventemitter3').EventEmitter} emitter - The EventEmitter instance to attach the event listener to.\n * @param {string} event - The name of the event to listen for.\n * @param {Function} callback - The callback function to execute when the event is triggered.\n */\nexport function useEventEmitter(emitter, event, callback) {\n  onBeforeMount(() => emitter.on(event, callback));\n  onBeforeUnmount(() => emitter.off(event, callback));\n}\n"
  },
  {
    "path": "src/ui/composables/resize.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/* eslint-disable func-style */\nimport { onBeforeUnmount, reactive } from 'vue';\n\nimport throttle from '../../utils/throttle.js';\nimport { useEventListener } from './event.js';\n\n/**\n * A composable which provides a function to begin observing the size of the passed-in element,\n * and a reactive object containing the width and height of the observed element. The ResizeObserver\n * is automatically disconnected before the component is unmounted.\n * @returns {{size: {width: number, height: number}, startObserving: (element: HTMLElement) => void}}\n */\nexport function useResizeObserver() {\n  const size = reactive({ width: 0, height: 0 });\n  let observer;\n\n  const startObserving = (element) => {\n    if (!element) {\n      return;\n    }\n\n    observer = new ResizeObserver((entries) => {\n      if (entries[0]) {\n        const { width, height } = entries[0].contentRect;\n        size.width = width;\n        size.height = height;\n      }\n    });\n\n    observer.observe(element);\n  };\n\n  onBeforeUnmount(() => {\n    if (observer) {\n      observer.disconnect();\n    }\n  });\n\n  return { size, startObserving };\n}\n\n/**\n * A composable function which can be used to listen to and handle window resize events.\n * Throttles the resize event to prevent performance issues.\n * @param {number} [throttleMs=100] The number of milliseconds to throttle the resize event.\n * @returns {{ windowSize: { width: number, height: number } }}\n */\nexport function useWindowResize(throttleMs = 100) {\n  const windowSize = reactive({ width: window.innerWidth, height: window.innerHeight });\n\n  const handleResize = throttle(() => {\n    windowSize.width = window.innerWidth;\n    windowSize.height = window.innerHeight;\n  }, throttleMs);\n\n  useEventListener(window, 'resize', handleResize);\n\n  return { windowSize };\n}\n"
  },
  {
    "path": "src/ui/inspector/InspectorDetailsSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nconst INSPECTOR_SELECTOR_PREFIX = '.c-inspect-properties__';\n\ndescribe('the inspector', () => {\n  let appHolder;\n  let openmct;\n  let folderItem;\n  let selection;\n\n  beforeEach((done) => {\n    folderItem = {\n      name: 'folder',\n      type: 'folder',\n      createdBy: 'John Q',\n      modifiedBy: 'Public',\n      id: 'mock-folder-key',\n      identifier: {\n        namespace: '',\n        key: 'mock-folder-key'\n      },\n      notes: 'This object should have some notes',\n      created: 1592851063871\n    };\n\n    selection = [\n      {\n        context: {\n          item: folderItem\n        }\n      }\n    ];\n\n    appHolder = document.createElement('div');\n\n    openmct = createOpenMct();\n    openmct.on('start', done);\n    openmct.start(appHolder);\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('displays default details for selection', async () => {\n    openmct.selection.select(selection);\n    await nextTick();\n\n    const details = getDetails();\n    const [title, type, createdBy, modifiedBy, notes, timestamp] = details;\n\n    expect(title.name).toEqual('Title');\n    expect(title.value.toLowerCase()).toEqual(folderItem.name);\n\n    expect(type.name).toEqual('Type');\n    expect(type.value.toLowerCase()).toEqual(folderItem.type);\n    expect(createdBy.name).toEqual('Created By');\n    expect(createdBy.value).toEqual(folderItem.createdBy);\n    expect(modifiedBy.name).toEqual('Modified By');\n    expect(modifiedBy.value).toEqual(folderItem.modifiedBy);\n    expect(notes.value).toEqual('This object should have some notes');\n\n    expect(timestamp.name).toEqual('Created');\n    expect(new Date(timestamp.value).toString()).toEqual(new Date(folderItem.created).toString());\n  });\n\n  it('details show modified date', async () => {\n    const modifiedTimestamp = folderItem.created + 1000;\n    folderItem.modified = modifiedTimestamp;\n\n    openmct.selection.select(selection);\n    await nextTick();\n\n    const details = getDetails();\n    const timestamp = details[details.length - 1];\n\n    expect(timestamp.name).toEqual('Modified');\n    expect(new Date(timestamp.value).toString()).toEqual(new Date(folderItem.modified).toString());\n  });\n\n  it('displays custom details if provided through context', async () => {\n    const NAME_PREFIX = 'Custom Name';\n    const VALUE_PREFIX = 'Custom Value';\n    const indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8];\n    const customDetails = indexes.map((index) => {\n      return {\n        name: `${NAME_PREFIX} ${index}`,\n        value: `${VALUE_PREFIX} ${index}`\n      };\n    });\n\n    selection[0].context.details = customDetails;\n\n    openmct.selection.select(selection);\n    await nextTick();\n\n    const details = getDetails();\n\n    expect(details.length).toEqual(customDetails.length);\n\n    details.forEach((detail, index) => {\n      expect(detail.name).toEqual(customDetails[index].name);\n      expect(detail.value).toEqual(customDetails[index].value);\n    });\n  });\n\n  function getDetailsElements() {\n    const inspectorDetailsSection = appHolder.querySelector(`${INSPECTOR_SELECTOR_PREFIX}section`);\n    const details = inspectorDetailsSection.querySelectorAll(`${INSPECTOR_SELECTOR_PREFIX}row`);\n\n    return details;\n  }\n\n  function getDetails() {\n    const detailsElements = getDetailsElements();\n    const details = Array.from(detailsElements).map((element) => {\n      return {\n        name: getText(element, 'label'),\n        value: getText(element, 'value')\n      };\n    });\n\n    return details;\n  }\n\n  function getText(element, selectorSuffix) {\n    return element\n      .querySelector(`${INSPECTOR_SELECTOR_PREFIX}${selectorSuffix}`)\n      .textContent.trim();\n  }\n});\n"
  },
  {
    "path": "src/ui/inspector/InspectorPanel.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector js-inspector\">\n    <ObjectName />\n    <InspectorTabs :is-editing=\"isEditing\" @select-tab=\"selectTab\" />\n    <InspectorViews :selected-tab=\"selectedTab\" />\n  </div>\n</template>\n\n<script>\nimport InspectorTabs from './InspectorTabs.vue';\nimport InspectorViews from './InspectorViews.vue';\nimport ObjectName from './ObjectName.vue';\n\nexport default {\n  components: {\n    ObjectName,\n    InspectorTabs,\n    InspectorViews\n  },\n  inject: ['openmct'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  data() {\n    return {\n      selectedTab: undefined\n    };\n  },\n  methods: {\n    selectTab(tab) {\n      this.selectedTab = tab;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/inspector/InspectorStylesSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { mockLocalStorage } from 'utils/testing/mockLocalStorage';\nimport { nextTick } from 'vue';\n\nimport StylesView from '@/plugins/condition/components/inspector/StylesView.vue';\n\nimport SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue';\nimport stylesManager from '../../plugins/inspectorViews/styles/StylesManager.js';\nimport {\n  mockMultiSelectionMixedStyles,\n  mockMultiSelectionNonSpecificStyles,\n  mockMultiSelectionSameStyles,\n  mockStyle,\n  mockTelemetryTableSelection\n} from './InspectorStylesSpecMocks.js';\n\ndescribe('the inspector', () => {\n  let openmct;\n  let selection;\n  let stylesViewComponent;\n  let savedStylesViewComponent;\n\n  mockLocalStorage();\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));\n    openmct.on('start', done);\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  it('should allow a style to be saved', () => {\n    selection = mockTelemetryTableSelection;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(0);\n\n    stylesViewComponent.$refs.root.saveStyle(mockStyle);\n\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1);\n  });\n\n  it('should display all saved styles', async () => {\n    selection = mockTelemetryTableSelection;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(0);\n    stylesViewComponent.$refs.root.saveStyle(mockStyle);\n\n    await nextTick();\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1);\n  });\n\n  xit('should allow a saved style to be applied', () => {\n    spyOn(openmct.editor, 'isEditing').and.returnValue(true);\n\n    selection = mockTelemetryTableSelection;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    stylesViewComponent.$refs.root.saveStyle(mockStyle);\n\n    return stylesViewComponent.$nextTick().then(() => {\n      const styleSelectorComponent = savedStylesViewComponent.$refs.root.$refs.root;\n\n      styleSelectorComponent.selectStyle();\n\n      return savedStylesViewComponent.$nextTick().then(() => {\n        const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;\n        const styles = styleEditorComponent.$children.filter(\n          (component) => component.options.value === mockStyle.color\n        );\n\n        expect(styles.length).toBe(3);\n      });\n    });\n  });\n\n  it('should allow a saved style to be deleted', () => {\n    selection = mockTelemetryTableSelection;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    stylesViewComponent.$refs.root.saveStyle(mockStyle);\n\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1);\n\n    savedStylesViewComponent.$refs.root.deleteStyle(0);\n\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(0);\n  });\n\n  it('should prevent a style from being saved when the number of saved styles is at the limit', () => {\n    spyOn(SavedStylesView.methods, 'showLimitReachedDialog').and.callThrough();\n\n    selection = mockTelemetryTableSelection;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    for (let i = 1; i <= 20; i++) {\n      stylesViewComponent.$refs.root.saveStyle(mockStyle);\n    }\n\n    expect(SavedStylesView.methods.showLimitReachedDialog).not.toHaveBeenCalled();\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(20);\n\n    stylesViewComponent.$refs.root.saveStyle(mockStyle);\n\n    expect(SavedStylesView.methods.showLimitReachedDialog).toHaveBeenCalled();\n    expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(20);\n  });\n\n  it('should allow styles from multi-selections to be saved', async () => {\n    spyOn(openmct.editor, 'isEditing').and.returnValue(true);\n\n    selection = mockMultiSelectionSameStyles;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    await nextTick();\n    const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;\n    const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;\n\n    expect(saveStyleButton).not.toBe(undefined);\n\n    saveStyleButton.$refs.button.click();\n\n    expect(savedStylesViewComponent.$refs.root.$data.savedStyles.length).toBe(1);\n  });\n\n  it('should prevent mixed styles from being saved', async () => {\n    spyOn(openmct.editor, 'isEditing').and.returnValue(true);\n\n    selection = mockMultiSelectionMixedStyles;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    await nextTick();\n    const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;\n    const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;\n\n    // Saving should not be enabled, thus the button ref should be undefined\n    expect(saveStyleButton).toBe(undefined);\n  });\n\n  it('should prevent non-specific styles from being saved', async () => {\n    spyOn(openmct.editor, 'isEditing').and.returnValue(true);\n\n    selection = mockMultiSelectionNonSpecificStyles;\n    stylesViewComponent = createViewComponent(StylesView, selection, openmct);\n    savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);\n\n    await nextTick();\n    const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;\n    const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;\n\n    // Saving should not be enabled, thus the button ref should be undefined\n    expect(saveStyleButton).toBe(undefined);\n  });\n\n  function createViewComponent(component) {\n    const element = document.createElement('div');\n    const child = document.createElement('div');\n    element.appendChild(child);\n\n    const config = {\n      provide: {\n        openmct,\n        selection,\n        stylesManager\n      },\n      components: {},\n      template: `<${component.name} ref=\"root\"/>`\n    };\n\n    config.components[component.name] = component;\n\n    const { vNode } = mount(config, {\n      element\n    });\n    return vNode.componentInstance;\n  }\n});\n"
  },
  {
    "path": "src/ui/inspector/InspectorStylesSpecMocks.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport const mockTelemetryTableSelection = [\n  [\n    {\n      context: {\n        item: {\n          configuration: {},\n          type: 'table',\n          identifier: {\n            key: 'mock-telemetry-table-1',\n            namespace: ''\n          }\n        }\n      }\n    }\n  ]\n];\n\nexport const mockStyle = {\n  backgroundColor: '#ff0000',\n  border: '#ff0000',\n  color: '#ff0000'\n};\n\nconst mockDisplayLayoutPath = {\n  context: {\n    item: {\n      identifier: {\n        key: '6af3200d-928b-4ff0-8ed0-b94a0e6752d1',\n        namespace: ''\n      },\n      type: 'layout',\n      configuration: {\n        items: [\n          {\n            id: 'dd3202e5-40d0-4112-8951-00f0f1ed6a29',\n            type: 'text-view',\n            fontSize: 'default',\n            font: 'default'\n          },\n          {\n            id: 'b522d636-90b2-4f5f-9588-2a0345c30f87',\n            type: 'text-view',\n            fontSize: 'default',\n            font: 'default'\n          },\n          {\n            id: '537b7596-b442-44fe-b464-07f56bdc67c8',\n            type: 'text-view',\n            fontSize: 'default',\n            font: 'default'\n          },\n          {\n            id: '3f17162f-a822-4e39-8332-6aa39b79d022',\n            type: 'text-view',\n            fontSize: 'default',\n            font: 'default'\n          },\n          {\n            id: 'c1c5acd8-a14b-450c-8c94-ce0075dd9912',\n            type: 'text-view',\n            fontSize: '8',\n            font: 'monospace-bold'\n          }\n        ],\n        objectStyles: {\n          'dd3202e5-40d0-4112-8951-00f0f1ed6a29': {\n            staticStyle: {\n              style: {\n                backgroundColor: '#0000ff',\n                border: '1px solid #0000ff'\n              }\n            }\n          },\n          'b522d636-90b2-4f5f-9588-2a0345c30f87': {\n            staticStyle: {\n              style: {\n                backgroundColor: '#ff0000',\n                border: '1px solid #ff0000'\n              }\n            }\n          },\n          '537b7596-b442-44fe-b464-07f56bdc67c8': {\n            staticStyle: {\n              style: {\n                backgroundColor: '#ff0000',\n                border: '1px solid #ff0000'\n              }\n            }\n          },\n          '3f17162f-a822-4e39-8332-6aa39b79d022': {\n            staticStyle: {\n              style: {\n                backgroundColor: '#0000ff',\n                border: '1px solid #0000ff',\n                color: '#0000ff'\n              }\n            }\n          },\n          'c1c5acd8-a14b-450c-8c94-ce0075dd9912': {\n            staticStyle: {\n              style: {\n                backgroundColor: '#0000ff',\n                border: '1px solid #0000ff',\n                color: '#0000ff'\n              }\n            }\n          }\n        }\n      }\n    },\n    supportsMultiSelect: true\n  }\n};\n\nconst mockTextBox1Path = {\n  context: {\n    index: 0,\n    layoutItem: {\n      id: 'dd3202e5-40d0-4112-8951-00f0f1ed6a29',\n      type: 'text-view',\n      fontSize: 'default',\n      font: 'default'\n    }\n  }\n};\n\nconst mockTextBox2Path = {\n  context: {\n    index: 1,\n    layoutItem: {\n      id: 'b522d636-90b2-4f5f-9588-2a0345c30f87',\n      type: 'text-view',\n      fontSize: 'default',\n      font: 'default'\n    }\n  }\n};\n\nconst mockTextBox3Path = {\n  context: {\n    index: 2,\n    layoutItem: {\n      id: '537b7596-b442-44fe-b464-07f56bdc67c8',\n      type: 'text-view',\n      fontSize: 'default',\n      font: 'default'\n    }\n  }\n};\n\nconst mockTextBox4Path = {\n  context: {\n    index: 3,\n    layoutItem: {\n      id: '3f17162f-a822-4e39-8332-6aa39b79d022',\n      type: 'text-view',\n      fontSize: 'default',\n      font: 'default'\n    }\n  }\n};\n\nconst mockTextBox5Path = {\n  context: {\n    index: 4,\n    layoutItem: {\n      id: 'c1c5acd8-a14b-450c-8c94-ce0075dd9912',\n      type: 'text-view',\n      fontSize: '8',\n      font: 'default-bold'\n    }\n  }\n};\n\nexport const mockMultiSelectionSameStyles = [\n  [mockTextBox2Path, mockDisplayLayoutPath],\n  [mockTextBox3Path, mockDisplayLayoutPath]\n];\n\nexport const mockMultiSelectionMixedStyles = [\n  [mockTextBox1Path, mockDisplayLayoutPath],\n  [mockTextBox2Path, mockDisplayLayoutPath]\n];\n\nexport const mockMultiSelectionNonSpecificStyles = [\n  [mockTextBox4Path, mockDisplayLayoutPath],\n  [mockTextBox5Path, mockDisplayLayoutPath]\n];\n"
  },
  {
    "path": "src/ui/inspector/InspectorTabs.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__tabs c-tabs\" role=\"tablist\">\n    <div\n      v-for=\"tab in visibleTabs\"\n      :key=\"tab.key\"\n      role=\"tab\"\n      class=\"c-inspector__tab c-tab\"\n      :class=\"{ 'is-current': isSelected(tab) }\"\n      tabindex=\"0\"\n      :title=\"tab.name\"\n      @click=\"selectTab(tab)\"\n      @keydown.enter=\"selectTab(tab)\"\n    >\n      <span class=\"c-inspector__tab-name c-tab__name\">{{ tab.name }}</span>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    isEditing: {\n      type: Boolean,\n      required: true\n    }\n  },\n  emits: ['select-tab'],\n  data() {\n    return {\n      tabs: [],\n      selectedTab: undefined\n    };\n  },\n  computed: {\n    visibleTabs() {\n      return this.tabs.filter((tab) => {\n        return tab.showTab === undefined || tab.showTab(this.isEditing);\n      });\n    }\n  },\n  watch: {\n    visibleTabs: {\n      handler() {\n        this.selectDefaultTabIfSelectedNotVisible();\n      },\n      deep: true\n    }\n  },\n  mounted() {\n    this.updateSelection();\n    this.openmct.editor.on('isEditing', this.updateSelection);\n    this.openmct.selection.on('change', this.updateSelection);\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.updateSelection);\n    this.openmct.selection.off('change', this.updateSelection);\n  },\n  methods: {\n    updateSelection() {\n      const previousSelectedTab = this.selectedTab?.key;\n      const inspectorViews = this.openmct.inspectorViews.get(this.openmct.selection.get());\n      const isEditing = this.openmct.editor.isEditing();\n\n      this.tabs = inspectorViews.map((view) => {\n        return {\n          key: view.key,\n          name: view.name,\n          glyph: view.glyph ?? 'icon-object',\n          showTab: view.showTab\n        };\n      });\n\n      const stylesTabIndex = this.tabs.findIndex((tab) => tab.key === 'stylesInspectorView');\n\n      if (isEditing && previousSelectedTab === 'stylesInspectorView' && stylesTabIndex !== -1) {\n        this.selectTab(this.tabs[stylesTabIndex]);\n      } else {\n        this.selectTab(this.visibleTabs[0]);\n      }\n    },\n    isSelected(tab) {\n      return this.selectedTab?.key === tab.key;\n    },\n    selectTab(tab) {\n      this.selectedTab = tab;\n      this.$emit('select-tab', tab);\n    },\n    selectDefaultTabIfSelectedNotVisible() {\n      const selectedTabIsVisible = this.visibleTabs.some((tab) => this.isSelected(tab));\n\n      if (!selectedTabIsVisible) {\n        this.selectTab(this.visibleTabs[0]);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/inspector/InspectorViews.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__content\" role=\"tabpanel\" aria-label=\"Inspector Views\"></div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    selectedTab: {\n      type: Object,\n      default: undefined\n    }\n  },\n  watch: {\n    selectedTab() {\n      this.clearAndShowViewsForTab();\n    }\n  },\n  mounted() {\n    this.updateSelectionViews();\n    this.openmct.editor.on('isEditing', this.updateSelectionViews);\n    this.openmct.selection.on('change', this.updateSelectionViews);\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.updateSelectionViews);\n    this.openmct.selection.off('change', this.updateSelectionViews);\n  },\n  methods: {\n    updateSelectionViews() {\n      this.clearViews();\n      this.selectedViews = this.openmct.inspectorViews.get(this.openmct.selection.get());\n      this.showViewsForTab();\n    },\n    clearViews() {\n      if (this.visibleViews) {\n        this.visibleViews.forEach((visibleView) => {\n          visibleView.destroy();\n        });\n\n        this.visibleViews = [];\n        this.$el.innerHTML = '';\n      }\n    },\n    showViewsForTab() {\n      this.visibleViews = this.selectedTab\n        ? this.selectedViews.filter((view) => view.key === this.selectedTab.key)\n        : [];\n\n      this.visibleViews.forEach((visibleView) => {\n        visibleView.show(this.$el);\n      });\n    },\n    clearAndShowViewsForTab() {\n      this.clearViews();\n      this.showViewsForTab();\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/inspector/ObjectName.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div class=\"c-inspector__header\">\n    <div v-if=\"!multiSelect\" class=\"c-inspector__selected c-object-label\" :class=\"[statusClass]\">\n      <div class=\"c-object-label__type-icon\" :class=\"typeCssClass\">\n        <span\n          class=\"is-status__indicator\"\n          :aria-label=\"`This item is ${status}`\"\n          :title=\"`This item is ${status}`\"\n        ></span>\n      </div>\n      <span v-if=\"!singleSelectNonObject\" class=\"c-inspector__selected c-object-label__name\">{{\n        item.name\n      }}</span>\n      <div\n        v-if=\"singleSelectNonObject\"\n        class=\"c-inspector__selected c-inspector__selected--non-domain-object c-object-label\"\n      >\n        <span class=\"c-object-label__name\">{{ heading }}</span>\n      </div>\n    </div>\n    <div v-if=\"multiSelect\" class=\"c-inspector__multiple-selected\">\n      {{ itemsSelected }} items selected\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  data() {\n    return {\n      domainObject: {},\n      activity: undefined,\n      layoutItem: undefined,\n      keyString: undefined,\n      multiSelect: false,\n      itemsSelected: 0,\n      status: undefined\n    };\n  },\n  computed: {\n    item() {\n      return this.domainObject || {};\n    },\n    heading() {\n      if (this.activity) {\n        return this.activity.name;\n      }\n\n      return 'Layout Item';\n    },\n    type() {\n      return this.openmct.types.get(this.item.type);\n    },\n    typeCssClass() {\n      if (this.activity) {\n        return 'icon-activity';\n      }\n\n      if (!this.domainObject && this.layoutItem) {\n        const layoutItemType = this.openmct.types.get(this.layoutItem.type);\n\n        return layoutItemType.definition.cssClass;\n      }\n\n      if (this.type.definition.cssClass === undefined) {\n        return 'icon-object';\n      }\n\n      return this.type.definition.cssClass;\n    },\n    singleSelectNonObject() {\n      return !this.item.identifier && !this.multiSelect;\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    }\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.updateSelection);\n    this.updateSelection(this.openmct.selection.get());\n  },\n  beforeUnmount() {\n    this.openmct.selection.off('change', this.updateSelection);\n\n    if (this.statusUnsubscribe) {\n      this.statusUnsubscribe();\n    }\n    if (this.nameUnsubscribe) {\n      this.nameUnsubscribe();\n    }\n  },\n  methods: {\n    updateSelection(selection) {\n      if (this.statusUnsubscribe) {\n        this.statusUnsubscribe();\n        this.statusUnsubscribe = null;\n      }\n      if (this.nameUnsubscribe) {\n        this.nameUnsubscribe();\n        this.nameUnsubscribe = null;\n      }\n\n      if (selection.length === 0 || selection[0].length === 0) {\n        this.resetDomainObject();\n\n        return;\n      }\n\n      if (selection.length > 1) {\n        this.multiSelect = true;\n        this.itemsSelected = selection.length;\n        this.resetDomainObject();\n\n        return;\n      } else {\n        this.multiSelect = false;\n        this.domainObject = selection[0][0].context.item;\n        this.activity = selection[0][0].context.activity;\n        if (this.domainObject) {\n          this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n          this.status = this.openmct.status.get(this.keyString);\n          this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);\n          this.nameUnsubscribe = this.openmct.objects.observe(\n            this.domainObject,\n            'name',\n            this.updateName\n          );\n        } else if (selection[0][0].context.layoutItem) {\n          this.layoutItem = selection[0][0].context.layoutItem;\n        }\n      }\n    },\n    resetDomainObject() {\n      this.domainObject = {};\n      this.status = undefined;\n      this.keyString = undefined;\n    },\n    updateStatus(status) {\n      this.status = status;\n    },\n    updateName(newName) {\n      this.domainObject = { ...this.domainObject, name: newName };\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/inspector/inspector.scss",
    "content": ".c-inspector {\n  display: flex;\n  flex: 1 1 auto;\n  gap: $interiorMarginSm;\n  flex-direction: column;\n  overflow: hidden;\n\n  &__selected,\n  &__multiple-selected {\n    @include headerFont(1.1em);\n    padding: $interiorMarginSm 0;\n  }\n\n  &__multiple-selected {\n    $p: $interiorMarginLg;\n    background: rgba($colorWarningLo, 0.3);\n    border-radius: $basicCr;\n    display: inline-block;\n    font-style: italic;\n    padding-left: $p;\n    padding-right: $p;\n  }\n\n  &__selected {\n    .c-object-label__type-icon {\n      opacity: $objectLabelTypeIconOpacity;\n    }\n\n    &--non-domain-object .c-object-label__name {\n      font-style: italic;\n    }\n  }\n\n  &__tabs {\n    flex: 0 0 auto;\n    font-size: 11px;\n    text-transform: uppercase;\n\n    &.c-tabs {\n      flex-wrap: nowrap;\n    }\n\n    .c-tab {\n      background: $colorTabBg;\n      color: $colorTabFg;\n      padding: $interiorMargin;\n\n      &:not(.is-current) {\n        overflow: hidden;\n\n        &:after {\n          background-image: linear-gradient(90deg, transparent 0%, rgba($colorTabBg, 1) 70%);\n          content: '';\n          display: block;\n          position: absolute;\n          right: 0;\n          height: 100%;\n          width: 15px;\n          z-index: 1;\n        }\n      }\n\n      &.is-current {\n        background: $colorTabCurrentBg;\n        color: $colorTabCurrentFg;\n        padding-right: $interiorMargin + 3;\n      }\n\n      &__name {\n        overflow: hidden;\n      }\n    }\n  }\n\n  &__content {\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n    overflow: auto;\n  }\n\n  &__elements {\n    height: 200px; // Initial height\n\n    .tree-item {\n      .t-object-label {\n        // Elements pool is a flat list, so don't indent items.\n        left: 0;\n      }\n    }\n  }\n\n  &__saved-styles {\n    height: 300px;\n  }\n\n  .c-color-swatch {\n    $d: 12px;\n    display: block;\n    flex: 0 0 auto;\n    width: $d;\n    height: $d;\n  }\n\n  .c-tree {\n    // When a tree is in the Inspector, remove scrolling and right pad\n    overflow: visible;\n    padding-right: 0;\n  }\n\n  textarea {\n    // When a textarea is in the Inspector, only allow vertical resize\n    resize: vertical;\n  }\n\n  /************************************************************** LEGACY */\n  .l-inspector-part {\n    display: contents;\n  }\n\n  h2 {\n    @include propertiesHeader();\n    font-size: 0.65rem;\n    grid-column: 1 / 3;\n    margin: $interiorMargin 0;\n\n    &.--first {\n      margin-top: 0;\n    }\n  }\n\n  .c-tree .grid-properties {\n    margin-left: $treeItemIndent;\n  }\n\n  .l-multipane {\n    .l-pane {\n      min-height: 50px;\n    }\n  }\n}\n\n.c-inspect-properties,\n.c-inspect-tags {\n  [class*='header'] {\n    @include propertiesHeader();\n    flex: 0 0 auto;\n    //font-size: 0.85em;\n  }\n}\n\n.c-inspect-properties,\n.c-inspect-styles {\n  [class*='header'] {\n    @include propertiesHeader();\n    flex: 0 0 auto;\n    font-size: 11px;\n    text-transform: uppercase;\n\n    &:not(:first-child) {\n      // Allow multiple headers within a component\n      margin-top: $interiorMarginLg;\n    }\n  }\n}\n/********************************************* INSPECTOR PROPERTIES TAB */\n.c-inspect-properties {\n  display: grid;\n  grid-row-gap: $interiorMarginSm;\n  grid-template-columns: 1fr 2fr;\n  align-items: start;\n  min-width: 150px;\n\n  [class*='span-all'],\n  [class*='header'] {\n    grid-column: 1 / 3;\n  }\n\n  + .c-inspect-properties {\n    margin-top: $interiorMarginLg;\n  }\n\n  &__section,\n  &__row {\n    display: contents;\n  }\n\n  &__row + &__row,\n  &__section + &__section {\n    [class*='__label'],\n    [class*='__value'] {\n      // Row borders, effected via border-top on child elements of the row\n      border-top: 1px solid $colorInteriorBorder;\n    }\n  }\n\n  &__label,\n  &__value {\n    padding: 3px $interiorMarginLg 3px 0;\n  }\n\n  &__label,\n  &__hint {\n    color: $colorInspectorPropName;\n\n    &[title]:not([title='']) {\n      // When a cell has a title, assume it's helpful text\n      cursor: help;\n    }\n  }\n\n  &__value {\n    color: $colorInspectorPropVal;\n    &:first-child {\n      // If there is no preceding .label element, make value span columns\n      grid-column: 1 / 3;\n    }\n    .hint {\n      color: $colorBodyFg\n    }\n  }\n}\n\n.is-editing {\n  .c-inspect-properties {\n    &__value,\n    &__label {\n      line-height: 160%; // Prevent buttons/selects from overlapping when wrapping\n    }\n  }\n  .grid-row--pad-label-for-button {\n    // Add extra space at the top of the label grid cell because there's a button to the right\n    [class*='label'] {\n      line-height: 1.8em;\n    }\n  }\n\n  .c-location {\n    // Always make the location element span columns\n    grid-column: 1 / 3;\n  }\n}\n\n/********************************************* INSPECTOR PROPERTIES TAB */\n.c-saved-style {\n  cursor: default;\n}\n\n/********************************************* LEGACY SUPPORT */\n.c-inspector {\n  // FilterField.vue\n  .u-contents + .u-contents {\n    li.grid-row > * {\n      border-top: 1px solid $colorInspectorSectionHeaderBg;\n    }\n  }\n\n  .grid-row + .grid-row {\n    > * {\n      border-top: 1px solid $colorInspectorSectionHeaderBg;\n    }\n  }\n\n  .grid-row .label {\n    color: $colorInspectorPropName;\n  }\n\n  .grid-row .value {\n    color: $colorInspectorPropVal;\n    word-break: break-all;\n    &:first-child {\n      // If there is no preceding .label element, make value span columns\n      grid-column: 1 / 3;\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/AboutDialog.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <!-- eslint-disable vue/no-v-html -->\n  <div class=\"c-about c-about--splash\">\n    <div class=\"c-about__image c-splash-image\" role=\"img\" alt=\"Open MCT Splash Logo\"></div>\n    <div class=\"c-about__text s-text\">\n      <div\n        v-if=\"branding.aboutHtml\"\n        class=\"c-about__text__element\"\n        v-html=\"branding.aboutHtml\"\n      ></div>\n      <div class=\"c-about__text__element\">\n        <h1 class=\"l-title s-title\">Open MCT</h1>\n        <div class=\"l-description s-description\">\n          <p>\n            Open MCT, Copyright &copy; 2014-2024, United States Government as represented by the\n            Administrator of the National Aeronautics and Space Administration. All rights reserved.\n          </p>\n          <p>\n            Open MCT is licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n            use this file except in compliance with the License. You may obtain a copy of the\n            License at\n            <a\n              target=\"_blank\"\n              href=\"http://www.apache.org/licenses/LICENSE-2.0\"\n              rel=\"noopener noreferrer\"\n              >http://www.apache.org/licenses/LICENSE-2.0</a\n            >.\n          </p>\n          <p>\n            Unless required by applicable law or agreed to in writing, software distributed under\n            the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n            KIND, either express or implied. See the License for the specific language governing\n            permissions and limitations under the License.\n          </p>\n          <p>\n            Open MCT includes source code licensed under additional open source licenses. See the\n            Open Source Licenses file included with this distribution or\n            <a @click=\"showLicenses\">click here for third party licensing information</a>.\n          </p>\n        </div>\n        <h2>Version Information</h2>\n        <ul id=\"versionInformation\" class=\"t-info l-info s-info\">\n          <li aria-label=\"Version Number\">Version: {{ buildInfo.version || 'Unknown' }}</li>\n          <li aria-label=\"Build Date\">Build Date: {{ buildInfo.buildDate || 'Unknown' }}</li>\n          <li aria-label=\"Revision\">Revision: {{ buildInfo.revision || 'Unknown' }}</li>\n          <li aria-label=\"Branch\">Branch: {{ buildInfo.branch || 'Unknown' }}</li>\n        </ul>\n      </div>\n    </div>\n  </div>\n</template>\n<script>\nexport default {\n  inject: ['openmct'],\n  data() {\n    return {\n      branding: JSON.parse(JSON.stringify(this.openmct.branding())),\n      buildInfo: JSON.parse(JSON.stringify(this.openmct.buildInfo))\n    };\n  },\n  methods: {\n    showLicenses() {\n      window.open('#/licenses');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/AppLayout.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"l-shell\"\n    :class=\"{\n      'is-editing': isEditing\n    }\"\n  >\n    <div id=\"splash-screen\"></div>\n\n    <div\n      class=\"l-shell__head\"\n      :class=\"{\n        'l-shell__head--expanded': headExpanded,\n        'l-shell__head--minify-indicators': !headExpanded,\n        'l-shell__head--indicators-single-line': !indicatorsMultiline\n      }\"\n    >\n      <CreateButton class=\"l-shell__create-button\" />\n      <GrandSearch ref=\"grand-search\" />\n      <StatusIndicators ref=\"indicatorsComponent\" />\n      <button\n        class=\"l-shell__head__button\"\n        :class=\"indicatorsMultilineCssClass\"\n        :aria-label=\"indicatorsMultilineLabel\"\n        :title=\"indicatorsMultilineLabel\"\n        @click=\"toggleIndicatorsMultiline\"\n      ></button>\n      <button\n        class=\"l-shell__head__button\"\n        :class=\"headExpanded ? 'icon-items-collapse' : 'icon-items-expand'\"\n        :aria-label=\"`Show ${headExpanded ? 'icon only' : 'icon and name'}`\"\n        :title=\"`Show ${headExpanded ? 'icon only' : 'icon and name'}`\"\n        @click=\"toggleShellHead\"\n      ></button>\n      <NotificationBanner />\n      <div class=\"l-shell__head-section l-shell__controls\">\n        <button\n          class=\"c-icon-button c-icon-button--major icon-new-window\"\n          title=\"Open in a new browser tab\"\n          target=\"_blank\"\n          @click=\"openInNewTab\"\n        ></button>\n        <button\n          :class=\"[\n            'c-icon-button c-icon-button--major',\n            fullScreen ? 'icon-fullscreen-collapse' : 'icon-fullscreen-expand'\n          ]\"\n          :aria-label=\"`${fullScreen ? 'Exit' : 'Enable'} full screen mode`\"\n          :title=\"`${fullScreen ? 'Exit' : 'Enable'} full screen mode`\"\n          @click=\"fullScreenToggle\"\n        ></button>\n      </div>\n      <AppLogo />\n    </div>\n\n    <div class=\"l-shell__drawer c-drawer c-drawer--push c-drawer--align-top\"></div>\n\n    <Multipane class=\"l-shell__main\" :class=\"[resizingClass]\" type=\"horizontal\">\n      <Pane\n        class=\"l-shell__pane-tree\"\n        handle=\"after\"\n        label=\"Browse\"\n        hide-param=\"hideTree\"\n        :persist-position=\"true\"\n        @start-resizing=\"onStartResizing\"\n        @end-resizing=\"onEndResizing\"\n      >\n        <template #controls>\n          <button\n            class=\"c-icon-button l-shell__reset-tree-button icon-folders-collapse\"\n            aria-label=\"Collapse all tree items\"\n            title=\"Collapse all tree items\"\n            @click=\"handleTreeReset\"\n          ></button>\n          <button\n            class=\"c-icon-button l-shell__sync-tree-button icon-target\"\n            aria-label=\"Show selected item in tree\"\n            title=\"Show selected item in tree\"\n            @click=\"handleSyncTreeNavigation\"\n          ></button>\n        </template>\n        <Multipane type=\"vertical\">\n          <Pane>\n            <MctTree\n              ref=\"mctTree\"\n              :sync-tree-navigation=\"triggerSync\"\n              :reset-tree-navigation=\"triggerReset\"\n              class=\"l-shell__tree\"\n            />\n          </Pane>\n          <Pane\n            handle=\"before\"\n            label=\"Recently Viewed\"\n            :persist-position=\"true\"\n            collapse-type=\"horizontal\"\n            hide-param=\"hideRecents\"\n          >\n            <RecentObjectsList\n              ref=\"recentObjectsList\"\n              class=\"l-shell__tree\"\n              @open-and-scroll-to=\"openAndScrollTo($event)\"\n              @set-clear-button-disabled=\"setClearButtonDisabled\"\n            />\n            <template #controls>\n              <button\n                class=\"c-icon-button icon-clear-data\"\n                aria-label=\"Clear Recently Viewed\"\n                title=\"Clear Recently Viewed\"\n                :disabled=\"disableClearButton\"\n                @click=\"handleClearRecentObjects\"\n              ></button>\n            </template>\n          </Pane>\n        </Multipane>\n      </Pane>\n      <Pane class=\"l-shell__pane-main\" role=\"main\">\n        <BrowseBar\n          ref=\"browseBar\"\n          class=\"l-shell__main-view-browse-bar\"\n          :action-collection=\"actionCollection\"\n          @sync-tree-navigation=\"handleSyncTreeNavigation\"\n        />\n        <Toolbar v-if=\"toolbar\" class=\"l-shell__toolbar\" />\n        <ObjectView\n          ref=\"browseObject\"\n          class=\"l-shell__main-container js-main-container js-notebook-snapshot-item\"\n          :class=\"{ '--has-toolbar': toolbar }\"\n          data-selectable\n          :show-edit-view=\"true\"\n          @change-action-collection=\"setActionCollection\"\n        />\n        <component\n          :is=\"conductorComponent\"\n          class=\"l-shell__time-conductor\"\n          aria-label=\"Global Time Conductor\"\n        />\n      </Pane>\n      <Pane\n        class=\"l-shell__pane-inspector l-pane--holds-multipane\"\n        handle=\"before\"\n        label=\"Inspect\"\n        hide-param=\"hideInspector\"\n        :persist-position=\"true\"\n        @start-resizing=\"onStartResizing\"\n        @end-resizing=\"onEndResizing\"\n      >\n        <Inspector ref=\"inspector\" :is-editing=\"isEditing\" />\n      </Pane>\n    </Multipane>\n  </div>\n</template>\n\n<script>\nimport { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';\n\nimport ObjectView from '../components/ObjectView.vue';\nimport Inspector from '../inspector/InspectorPanel.vue';\nimport Toolbar from '../toolbar/ToolbarContainer.vue';\nimport AppLogo from './AppLogo.vue';\nimport BrowseBar from './BrowseBar.vue';\nimport CreateButton from './CreateButton.vue';\nimport MctTree from './MctTree.vue';\nimport Multipane from './MultipaneContainer.vue';\nimport Pane from './PaneContainer.vue';\nimport RecentObjectsList from './RecentObjectsList.vue';\nimport GrandSearch from './search/GrandSearch.vue';\nimport NotificationBanner from './status-bar/NotificationBanner.vue';\nimport StatusIndicators from './status-bar/StatusIndicators.vue';\n\nconst SHELL_HEAD_LOCAL_STORAGE_KEY = 'openmct-shell-head';\nconst DEFAULT_HEAD_EXPANDED = true;\nconst DEFAULT_INDICATORS_MULTILINE = true;\n\nexport default {\n  components: {\n    Inspector,\n    MctTree,\n    ObjectView,\n    CreateButton,\n    GrandSearch,\n    Multipane,\n    Pane,\n    BrowseBar,\n    Toolbar,\n    AppLogo,\n    StatusIndicators,\n    NotificationBanner,\n    RecentObjectsList\n  },\n  inject: ['openmct'],\n  setup() {\n    let resizeObserver;\n    let element;\n\n    const storedHeadProps = localStorage.getItem(SHELL_HEAD_LOCAL_STORAGE_KEY);\n    const storedHeadPropsObject = JSON.parse(storedHeadProps);\n    const storedHeadExpanded = storedHeadPropsObject?.expanded;\n    const storedIndicatorsMultiline = storedHeadPropsObject?.multiline;\n\n    // template ref of StatusIndicators component\n    const indicatorsComponent = ref(null);\n\n    const width = ref(null);\n    const scrollWidth = ref(null);\n    const headExpanded = ref(storedHeadExpanded ?? DEFAULT_HEAD_EXPANDED);\n    const indicatorsMultiline = ref(storedIndicatorsMultiline ?? DEFAULT_INDICATORS_MULTILINE);\n\n    const isOverflowing = computed(() => scrollWidth.value > width.value);\n    const indicatorsMultilineCssClass = computed(() => {\n      const multilineClass = indicatorsMultiline.value ? 'icon-singleline' : 'icon-multiline';\n      const overflowingClass =\n        isOverflowing.value && !indicatorsMultiline.value\n          ? 'c-button c-button--major'\n          : 'c-icon-button';\n      return `${multilineClass} ${overflowingClass}`;\n    });\n    const indicatorsMultilineLabel = computed(() => {\n      return `Display as ${indicatorsMultiline.value ? 'single line' : 'multiple lines'}`;\n    });\n\n    const initialHeadProps = JSON.stringify({\n      expanded: headExpanded.value,\n      multiline: indicatorsMultiline.value\n    });\n\n    if (initialHeadProps !== storedHeadProps) {\n      localStorage.setItem(SHELL_HEAD_LOCAL_STORAGE_KEY, initialHeadProps);\n    }\n\n    onMounted(() => {\n      resizeObserver = new ResizeObserver((entries) => {\n        width.value = entries[0].target.clientWidth;\n        scrollWidth.value = entries[0].target.scrollWidth;\n      });\n\n      // indicatorsContainer is a template ref inside of indicatorsComponent\n      element = indicatorsComponent.value.$refs.indicatorsContainer;\n\n      if (!indicatorsMultiline.value) {\n        observeIndicatorsOverflow();\n      }\n    });\n\n    onUnmounted(() => {\n      resizeObserver.disconnect();\n    });\n\n    function observeIndicatorsOverflow() {\n      resizeObserver.observe(element);\n    }\n\n    function unObserveIndicatorsOverflow() {\n      resizeObserver.unobserve(element);\n    }\n\n    function checkIndicatorsElementWidths() {\n      if (!indicatorsMultiline.value) {\n        width.value = element.clientWidth;\n        scrollWidth.value = element.scrollWidth;\n      }\n    }\n\n    async function toggleShellHead() {\n      headExpanded.value = !headExpanded.value;\n      setLocalStorageShellHead();\n\n      // nextTick is used because the element width on toggle is updated using css\n      await nextTick();\n      checkIndicatorsElementWidths();\n    }\n\n    function toggleIndicatorsMultiline() {\n      indicatorsMultiline.value = !indicatorsMultiline.value;\n      setLocalStorageShellHead();\n\n      if (indicatorsMultiline.value) {\n        unObserveIndicatorsOverflow();\n      } else {\n        observeIndicatorsOverflow();\n      }\n    }\n\n    function setLocalStorageShellHead() {\n      localStorage.setItem(\n        SHELL_HEAD_LOCAL_STORAGE_KEY,\n        JSON.stringify({\n          expanded: headExpanded.value,\n          multiline: indicatorsMultiline.value\n        })\n      );\n    }\n\n    return {\n      indicatorsComponent,\n      isOverflowing,\n      headExpanded,\n      indicatorsMultiline,\n      indicatorsMultilineCssClass,\n      indicatorsMultilineLabel,\n      toggleIndicatorsMultiline,\n      toggleShellHead\n    };\n  },\n  data() {\n    return {\n      fullScreen: false,\n      conductorComponent: undefined,\n      isEditing: false,\n      hasToolbar: false,\n      actionCollection: undefined,\n      triggerSync: false,\n      triggerReset: false,\n      isResizing: false,\n      disableClearButton: false\n    };\n  },\n  computed: {\n    toolbar() {\n      return this.hasToolbar && this.isEditing;\n    },\n    resizingClass() {\n      return this.isResizing ? 'l-shell__resizing' : '';\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', (isEditing) => {\n      this.isEditing = isEditing;\n    });\n\n    this.openmct.selection.on('change', this.toggleHasToolbar);\n  },\n  methods: {\n    enterFullScreen() {\n      let docElm = document.documentElement;\n\n      if (docElm.requestFullscreen) {\n        docElm.requestFullscreen();\n      } else if (docElm.mozRequestFullScreen) {\n        /* Firefox */\n        docElm.mozRequestFullScreen();\n      } else if (docElm.webkitRequestFullscreen) {\n        /* Chrome, Safari and Opera */\n        docElm.webkitRequestFullscreen();\n      } else if (docElm.msRequestFullscreen) {\n        /* IE/Edge */\n        docElm.msRequestFullscreen();\n      }\n    },\n    exitFullScreen() {\n      if (document.exitFullscreen) {\n        document.exitFullscreen();\n      } else if (document.mozCancelFullScreen) {\n        document.mozCancelFullScreen();\n      } else if (document.webkitCancelFullScreen) {\n        document.webkitCancelFullScreen();\n      } else if (document.msExitFullscreen) {\n        document.msExitFullscreen();\n      }\n    },\n    fullScreenToggle() {\n      if (this.fullScreen) {\n        this.fullScreen = false;\n        this.exitFullScreen();\n      } else {\n        this.fullScreen = true;\n        this.enterFullScreen();\n      }\n    },\n    openInNewTab(event) {\n      window.open(window.location.href);\n    },\n    toggleHasToolbar(selection) {\n      let structure = undefined;\n\n      if (!selection || !selection[0]) {\n        structure = [];\n      } else {\n        structure = this.openmct.toolbars.get(selection);\n      }\n\n      this.hasToolbar = structure.length > 0;\n    },\n    openAndScrollTo(navigationPath) {\n      this.$refs.mctTree.openAndScrollTo(navigationPath);\n      this.$refs.mctTree.targetedPath = navigationPath;\n    },\n    setActionCollection(actionCollection) {\n      this.actionCollection = actionCollection;\n    },\n    handleSyncTreeNavigation() {\n      this.triggerSync = !this.triggerSync;\n    },\n    handleTreeReset() {\n      this.triggerReset = !this.triggerReset;\n    },\n    handleClearRecentObjects() {\n      this.$refs.recentObjectsList.clearRecentObjects();\n    },\n    onStartResizing() {\n      this.isResizing = true;\n    },\n    onEndResizing() {\n      this.isResizing = false;\n    },\n    setClearButtonDisabled(isDisabled) {\n      this.disableClearButton = isDisabled;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/AppLogo.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"aboutLogo\"\n    class=\"l-shell__app-logo\"\n    role=\"button\"\n    aria-label=\"About Modal\"\n    @click=\"launchAbout\"\n  ></div>\n</template>\n\n<script>\nimport mount from 'utils/mount';\n\nimport { encode_url } from '../../utils/encoding';\nimport AboutDialog from './AboutDialog.vue';\n\nexport default {\n  inject: ['openmct'],\n  mounted() {\n    const branding = this.openmct.branding();\n    if (branding.smallLogoImage) {\n      this.$refs.aboutLogo.style.backgroundImage = `url('${encode_url(branding.smallLogoImage)}')`;\n    }\n  },\n  methods: {\n    launchAbout() {\n      const { el, destroy } = mount(\n        {\n          components: { AboutDialog },\n          provide: {\n            openmct: this.openmct\n          },\n          template: '<about-dialog></about-dialog>'\n        },\n        {\n          app: this.openmct.app\n        }\n      );\n\n      el.classList.add('u-contents');\n\n      this.openmct.overlays.overlay({\n        element: el,\n        size: 'large',\n        onDestroy: destroy\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/BrowseBar.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"l-browse-bar\" aria-label=\"Browse bar\">\n    <div class=\"l-browse-bar__start\">\n      <button\n        v-if=\"hasParent\"\n        aria-label=\"Navigate up to parent\"\n        class=\"l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent\"\n        title=\"Navigate up to parent\"\n        @click=\"goToParent\"\n      ></button>\n      <div class=\"l-browse-bar__object-name--w c-object-label\" :class=\"[statusClass]\">\n        <div class=\"c-object-label__type-icon\" :class=\"cssClass\">\n          <span class=\"is-status__indicator\" :title=\"`This item is ${status}`\"></span>\n        </div>\n        <span\n          ref=\"objectName\"\n          aria-label=\"Browse bar object name\"\n          class=\"l-browse-bar__object-name c-object-label__name\"\n          :class=\"{ 'c-input-inline': isPersistable }\"\n          :contenteditable=\"isNameEditable\"\n          @blur=\"updateName\"\n          @keydown.enter.prevent\n          @keyup.enter.prevent=\"updateNameOnEnterKeyPress\"\n          @mouseover.ctrl=\"showToolTip\"\n          @mouseleave=\"hideToolTip\"\n        >\n          {{ domainObjectName }}\n        </span>\n      </div>\n    </div>\n\n    <div class=\"l-browse-bar__end\">\n      <div\n        v-if=\"supportsIndependentTime\"\n        class=\"c-conductor-holder--compact l-shell__main-independent-time-conductor\"\n      >\n        <IndependentTimeConductor\n          :domain-object=\"domainObject\"\n          :object-path=\"openmct.router.path\"\n        />\n      </div>\n      <ViewSwitcher v-if=\"!isEditing\" :current-view=\"currentView\" :views=\"views\" />\n      <!-- Action buttons -->\n      <NotebookMenuSwitcher\n        v-if=\"notebookEnabled\"\n        :domain-object=\"domainObject\"\n        :object-path=\"openmct.router.path\"\n        class=\"c-notebook-snapshot-menubutton\"\n      />\n      <div class=\"l-browse-bar__actions\">\n        <button\n          v-for=\"(item, index) in statusBarItems\"\n          :key=\"index\"\n          class=\"c-button\"\n          :aria-label=\"item.name\"\n          :title=\"item.name\"\n          :class=\"item.cssClass\"\n          @click=\"item.onItemClicked\"\n        ></button>\n\n        <button\n          v-if=\"shouldShowLock\"\n          :aria-label=\"lockedOrUnlockedTitle\"\n          :title=\"lockedOrUnlockedTitle\"\n          :class=\"{\n            'c-button icon-lock': domainObject.locked,\n            'c-icon-button icon-unlocked': !domainObject.locked\n          }\"\n          @click=\"toggleLock(!domainObject.locked)\"\n        ></button>\n\n        <span\n          v-else-if=\"domainObject?.locked\"\n          class=\"icon-lock\"\n          aria-label=\"Locked for editing, cannot be unlocked.\"\n          title=\"Locked for editing, cannot be unlocked.\"\n        ></span>\n\n        <button\n          v-if=\"isViewEditable && !isEditing && !domainObject.locked\"\n          class=\"l-browse-bar__actions__edit c-button c-button--major icon-pencil\"\n          title=\"Edit Object\"\n          aria-label=\"Edit Object\"\n          @click=\"edit()\"\n        ></button>\n\n        <div\n          v-if=\"isEditing\"\n          class=\"l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left\"\n        >\n          <button\n            class=\"c-button--menu c-button--major icon-save\"\n            title=\"Save\"\n            aria-label=\"Save\"\n            @click.stop=\"toggleSaveMenu\"\n          ></button>\n          <div v-show=\"showSaveMenu\" class=\"c-menu\">\n            <ul>\n              <li class=\"icon-save\" title=\"Save and Finish Editing\" @click=\"saveAndFinishEditing\">\n                Save and Finish Editing\n              </li>\n              <li\n                class=\"icon-save\"\n                title=\"Save and Continue Editing\"\n                @click=\"saveAndContinueEditing\"\n              >\n                Save and Continue Editing\n              </li>\n            </ul>\n          </div>\n        </div>\n\n        <button\n          v-if=\"isEditing\"\n          class=\"l-browse-bar__actions c-button icon-x\"\n          aria-label=\"Cancel Editing\"\n          title=\"Cancel Editing\"\n          @click=\"promptUserandCancelEditing()\"\n        ></button>\n        <button\n          class=\"l-browse-bar__actions c-icon-button icon-3-dots\"\n          title=\"More actions\"\n          aria-label=\"More actions\"\n          @click.prevent.stop=\"showMenuItems($event)\"\n        ></button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { toRaw } from 'vue';\n\nimport NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';\nimport IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';\n\nimport tooltipHelpers from '../../api/tooltips/tooltipMixins.js';\nimport { SupportedViewTypes } from '../../utils/constants.js';\nimport ViewSwitcher from './ViewSwitcher.vue';\n\nconst LOCALSTORAGE_VIEW_PREFS = 'openmct-stored-view-prefs';\n\nexport default {\n  components: {\n    IndependentTimeConductor,\n    NotebookMenuSwitcher,\n    ViewSwitcher\n  },\n  mixins: [tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    actionCollection: {\n      type: Object,\n      default: () => {\n        return undefined;\n      }\n    }\n  },\n  data() {\n    return {\n      notebookTypes: [],\n      showViewMenu: false,\n      showSaveMenu: false,\n      domainObject: undefined,\n      viewKey: undefined,\n      isEditing: this.openmct.editor.isEditing(),\n      notebookEnabled: this.openmct.types.get('notebook'),\n      statusBarItems: [],\n      status: ''\n    };\n  },\n  computed: {\n    isNameEditable() {\n      return this.isPersistable && !this.domainObject.locked;\n    },\n    shouldShowLock() {\n      if (this.domainObject === undefined) {\n        return false;\n      }\n      if (this.domainObject.disallowUnlock) {\n        return false;\n      }\n      return this.domainObject.locked || (this.isViewEditable && !this.isEditing);\n    },\n    statusClass() {\n      return this.status ? `is-status--${this.status}` : '';\n    },\n    supportsIndependentTime() {\n      return (\n        this.domainObject?.identifier &&\n        !this.openmct.objects.isMissing(this.domainObject) &&\n        SupportedViewTypes.includes(this.viewKey)\n      );\n    },\n    currentView() {\n      return this.views.filter((v) => v.key === this.viewKey)[0] || {};\n    },\n    views() {\n      if (this.domainObject && this.openmct.router.started === false) {\n        return [];\n      }\n\n      if (!this.domainObject) {\n        return [];\n      }\n\n      return this.openmct.objectViews.get(this.domainObject, this.openmct.router.path).map((p) => {\n        return {\n          key: p.key,\n          cssClass: p.cssClass,\n          name: p.name,\n          onItemClicked: () => this.setView({ key: p.key })\n        };\n      });\n    },\n    hasParent() {\n      return toRaw(this.domainObject) && this.parentUrl !== '/browse';\n    },\n    parentUrl() {\n      const objectKeyString = this.openmct.objects.makeKeyString(this.domainObject?.identifier);\n      const hash = this.openmct.router.getCurrentLocation().path;\n\n      return hash.slice(0, hash.lastIndexOf('/' + objectKeyString));\n    },\n    cssClass() {\n      if (!this.domainObject) {\n        return '';\n      }\n\n      const objectType = this.openmct.types.get(this.domainObject.type);\n      if (!objectType) {\n        return '';\n      }\n\n      return objectType?.definition?.cssClass ?? '';\n    },\n    objectTypeKey() {\n      if (!this.domainObject) {\n        return '';\n      }\n\n      const objectType = this.openmct.types.get(this.domainObject.type);\n      if (!objectType) {\n        return '';\n      }\n\n      return objectType.definition?.key ?? '';\n    },\n    isPersistable() {\n      const persistable =\n        this.domainObject?.identifier &&\n        this.openmct.objects.isPersistable(this.domainObject.identifier);\n\n      return persistable;\n    },\n    isViewEditable() {\n      let currentViewKey = this.currentView.key;\n      if (currentViewKey !== undefined) {\n        let currentViewProvider = this.openmct.objectViews.getByProviderKey(currentViewKey);\n\n        return (\n          currentViewProvider.canEdit &&\n          currentViewProvider.canEdit(this.domainObject, this.openmct.router.path)\n        );\n      }\n\n      return false;\n    },\n    lockedOrUnlockedTitle() {\n      let title;\n      if (this.domainObject.locked) {\n        if (this.domainObject.lockedBy !== undefined) {\n          title = `Locked for editing by ${this.domainObject.lockedBy}. `;\n        } else {\n          title = 'Locked for editing. ';\n        }\n        title += 'Click to unlock.';\n      } else {\n        title = 'Unlocked for editing, click to lock.';\n      }\n\n      return title;\n    },\n    domainObjectName() {\n      return this.domainObject?.name ?? '';\n    }\n  },\n  watch: {\n    domainObject() {\n      if (this.removeStatusListener) {\n        this.removeStatusListener();\n      }\n\n      this.status = this.openmct.status.get(this.domainObject.identifier, this.setStatus);\n      this.removeStatusListener = this.openmct.status.observe(\n        this.domainObject.identifier,\n        this.setStatus\n      );\n    },\n    actionCollection(actionCollection) {\n      if (this.actionCollection) {\n        this.unlistenToActionCollection();\n      }\n\n      this.actionCollection.on('update', this.updateActionItems);\n      this.updateActionItems(this.actionCollection.getActionsObject());\n    }\n  },\n  mounted() {\n    document.addEventListener('click', this.closeViewAndSaveMenu);\n    this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);\n    window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);\n    this.openmct.editor.on('isEditing', (isEditing) => {\n      this.isEditing = isEditing;\n    });\n  },\n  beforeUnmount() {\n    if (this.mutationObserver) {\n      this.mutationObserver();\n    }\n\n    if (this.actionCollection) {\n      this.unlistenToActionCollection();\n    }\n\n    if (this.removeStatusListener) {\n      this.removeStatusListener();\n    }\n\n    document.removeEventListener('click', this.closeViewAndSaveMenu);\n    window.removeEventListener('beforeunload', this.promptUserbeforeNavigatingAway);\n  },\n  methods: {\n    toggleSaveMenu() {\n      this.showSaveMenu = !this.showSaveMenu;\n    },\n    closeViewAndSaveMenu() {\n      this.showViewMenu = false;\n      this.showSaveMenu = false;\n    },\n    updateName(event) {\n      if (event.target.innerText !== this.domainObject.name && event.target.innerText.match(/\\S/)) {\n        this.openmct.objects.mutate(this.domainObject, 'name', event.target.innerText);\n      }\n    },\n    updateNameOnEnterKeyPress(event) {\n      event.target.blur();\n    },\n    setView(view) {\n      this.viewKey = view.key;\n      this.storeViewPrefs(view.key);\n      this.openmct.router.updateParams({\n        view: this.viewKey\n      });\n    },\n    retrieveViewPrefs() {\n      return JSON.parse(window.localStorage.getItem(LOCALSTORAGE_VIEW_PREFS)) || {};\n    },\n    storeViewPrefs(view) {\n      let storedViews = this.retrieveViewPrefs();\n      storedViews[this.domainObject.type] = view;\n      window.localStorage.setItem(LOCALSTORAGE_VIEW_PREFS, JSON.stringify(storedViews));\n    },\n    edit() {\n      this.openmct.editor.edit();\n    },\n    promptUserandCancelEditing() {\n      let dialog = this.openmct.overlays.dialog({\n        iconClass: 'alert',\n        message: 'Any unsaved changes will be lost. Are you sure you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            emphasis: true,\n            callback: () => {\n              this.openmct.editor.cancel().then(() => {\n                //refresh object view\n                this.openmct.layout.$refs.browseObject.show(this.domainObject, this.viewKey, true);\n              });\n              dialog.dismiss();\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    },\n    promptUserbeforeNavigatingAway(event) {\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        event.returnValue = '';\n      }\n    },\n    saveAndFinishEditing() {\n      let dialog = this.openmct.overlays.progressDialog({\n        progressPerc: null,\n        message:\n          'Do not navigate away from this page or close this browser tab while this message is displayed.',\n        iconClass: 'info',\n        title: 'Saving'\n      });\n\n      const currentSelection = this.openmct.selection.selected[0];\n      const parentObject = currentSelection[currentSelection.length - 1];\n      this.openmct.selection.select(parentObject);\n\n      return this.openmct.editor\n        .save()\n        .then(() => {\n          dialog.dismiss();\n          this.openmct.notifications.info('Save successful');\n        })\n        .catch((error) => {\n          dialog.dismiss();\n          this.openmct.notifications.error('Error saving objects');\n          console.error(error);\n          this.openmct.editor.cancel();\n        });\n    },\n    saveAndContinueEditing() {\n      this.saveAndFinishEditing().then(() => {\n        this.openmct.editor.edit();\n      });\n    },\n    goToParent() {\n      this.openmct.router.navigate(this.parentUrl);\n    },\n    updateActionItems(actionItems) {\n      const statusBarItems = this.actionCollection.getStatusBarActions();\n      this.statusBarItems = this.openmct.menus.actionsToMenuItems(\n        statusBarItems,\n        this.actionCollection.objectPath,\n        this.actionCollection.view\n      );\n      this.menuActionItems = this.actionCollection.getVisibleActions();\n    },\n    showMenuItems(event) {\n      const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        sortedActions,\n        this.actionCollection.objectPath,\n        this.actionCollection.view\n      );\n      this.openmct.menus.showMenu(event.x, event.y, menuItems);\n    },\n    unlistenToActionCollection() {\n      this.actionCollection.off('update', this.updateActionItems);\n      delete this.actionCollection;\n    },\n    async toggleLock(flag) {\n      if (!this.domainObject.disallowUnlock) {\n        const wasTransactionActive = this.openmct.objects.isTransactionActive();\n        let transaction;\n\n        if (!wasTransactionActive) {\n          transaction = this.openmct.objects.startTransaction();\n        }\n\n        this.openmct.objects.mutate(this.domainObject, 'locked', flag);\n        const user = await this.openmct.user.getCurrentUser();\n\n        if (user !== undefined) {\n          this.openmct.objects.mutate(this.domainObject, 'lockedBy', user.id);\n        }\n\n        if (!wasTransactionActive) {\n          await transaction.commit();\n          this.openmct.objects.endTransaction();\n        }\n      }\n    },\n    setStatus(status) {\n      this.status = status;\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/Container.js",
    "content": "import { v4 as uuid } from 'uuid';\n\nclass Container {\n  constructor(size) {\n    this.id = uuid();\n    this.frames = [];\n    this.size = size;\n  }\n}\n\nexport default Container;\n"
  },
  {
    "path": "src/ui/layout/CreateButton.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div ref=\"createButton\" class=\"c-create-button--w\">\n    <button\n      class=\"c-create-button c-button--menu c-button--major icon-plus\"\n      :aria-disabled=\"isEditing\"\n      aria-labelledby=\"create-button-label\"\n      @click.prevent.stop=\"showCreateMenu\"\n    >\n      <span id=\"create-button-label\" class=\"c-button__label\">Create</span>\n    </button>\n  </div>\n</template>\n<script>\nimport { CREATE_ACTION_KEY } from '@/plugins/formActions/CreateAction';\n\nexport default {\n  inject: ['openmct'],\n  data: function () {\n    return {\n      menuItems: {},\n      isEditing: this.openmct.editor.isEditing(),\n      selectedMenuItem: {},\n      opened: false\n    };\n  },\n  computed: {\n    sortedItems() {\n      let items = this.getItems();\n\n      return items.sort((a, b) => {\n        if (a.name < b.name) {\n          return -1;\n        } else if (a.name > b.name) {\n          return 1;\n        } else {\n          return 0;\n        }\n      });\n    }\n  },\n  mounted() {\n    this.openmct.editor.on('isEditing', this.toggleEdit);\n  },\n  unmounted() {\n    this.openmct.editor.off('isEditing', this.toggleEdit);\n  },\n  methods: {\n    getItems() {\n      let keys = this.openmct.types.listKeys();\n\n      keys.forEach((key) => {\n        if (!this.menuItems[key]) {\n          let typeDef = this.openmct.types.get(key).definition;\n\n          if (typeDef.creatable) {\n            this.menuItems[key] = {\n              cssClass: typeDef.cssClass,\n              name: typeDef.name,\n              description: typeDef.description,\n              onItemClicked: () => this.create(key)\n            };\n          }\n        }\n      });\n\n      return Object.values(this.menuItems);\n    },\n    showCreateMenu() {\n      const elementBoundingClientRect = this.$refs.createButton.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y + elementBoundingClientRect.height;\n\n      const menuOptions = {\n        menuClass: 'c-create-menu'\n      };\n\n      this.openmct.menus.showSuperMenu(x, y, this.sortedItems, menuOptions);\n    },\n    toggleEdit(isEditing) {\n      this.isEditing = isEditing;\n    },\n    create(key) {\n      const createAction = this.openmct.actions.getAction(CREATE_ACTION_KEY);\n      createAction.invoke(key, this.openmct.router.path[0]);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/Frame.js",
    "content": "import { v4 as uuid } from 'uuid';\n\nclass Frame {\n  constructor(domainObjectIdentifier, size) {\n    this.id = uuid();\n    this.domainObjectIdentifier = domainObjectIdentifier;\n    this.size = size;\n\n    this.noFrame = false;\n  }\n}\n\nexport default Frame;\n"
  },
  {
    "path": "src/ui/layout/LayoutSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport Layout from './AppLayout.vue';\n\ndescribe('Open MCT Layout:', () => {\n  let openmct;\n  let element;\n  let components;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    openmct.on('start', done);\n\n    // to silence error from BrowseBar.vue\n    spyOn(openmct.objectViews, 'get').and.callFake(() => []);\n\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    return resetApplicationState(openmct);\n  });\n\n  describe('the pane:', () => {\n    it('is displayed on layout load', async () => {\n      await createLayout();\n      await nextTick();\n\n      Object.entries(components).forEach(([name, component]) => {\n        expect(component.pane).toBeTruthy();\n\n        expect(isCollapsed(component.pane)).toBeFalse();\n      });\n    });\n\n    it('is collapsed on layout load if specified by a hide param', async () => {\n      setHideParams();\n\n      await createLayout();\n      await nextTick();\n      await nextTick();\n\n      Object.entries(components).forEach(([name, component]) => {\n        expect(isCollapsed(component.pane)).toBeTrue();\n      });\n    });\n\n    it('on toggle collapses if expanded', async () => {\n      await createLayout();\n      await nextTick();\n      toggleCollapseButtons();\n      await nextTick();\n\n      Object.entries(components).forEach(([name, component]) => {\n        expect(openmct.router.getSearchParam(component.param)).toEqual('true');\n\n        expect(isCollapsed(component.pane)).toBeTrue();\n      });\n    });\n\n    it('on toggle expands if collapsed', async () => {\n      setHideParams();\n\n      await createLayout();\n      await nextTick();\n      toggleExpandButtons();\n\n      Object.entries(components).forEach(([name, component]) => {\n        expect(openmct.router.getSearchParam(component.param)).not.toEqual('true');\n\n        expect(isCollapsed(component.pane)).toBeFalse();\n      });\n    });\n  });\n\n  // eslint-disable-next-line require-await\n  async function createLayout() {\n    const el = document.createElement('div');\n    const child = document.createElement('div');\n    el.appendChild(child);\n\n    const { vNode } = mount(\n      {\n        el,\n        components: {\n          Layout\n        },\n        provide: {\n          openmct\n        },\n        template: `<Layout ref=\"layout\"/>`\n      },\n      {\n        element: el\n      }\n    );\n\n    element = vNode.el;\n\n    setComponents();\n  }\n\n  function setComponents() {\n    components = {\n      tree: {\n        param: 'hideTree',\n        pane: element.querySelector('.l-shell__pane-tree'),\n        collapseButton: element.querySelector('.l-shell__pane-tree .l-pane__collapse-button'),\n        expandButton: element.querySelector('.l-shell__pane-tree .l-pane__expand-button')\n      },\n      inspector: {\n        param: 'hideInspector',\n        pane: element.querySelector('.l-shell__pane-inspector'),\n        collapseButton: element.querySelector('.l-shell__pane-inspector .l-pane__collapse-button'),\n        expandButton: element.querySelector('.l-shell__pane-inspector .l-pane__expand-button')\n      }\n    };\n  }\n\n  function isCollapsed(el) {\n    return el.classList.contains('l-pane--collapsed');\n  }\n\n  function setHideParams() {\n    Object.entries(components).forEach(([name, component]) => {\n      openmct.router.setSearchParam(component.param, true);\n    });\n  }\n\n  function toggleCollapseButtons() {\n    Object.entries(components).forEach(([name, component]) => {\n      component.collapseButton.click();\n    });\n  }\n\n  function toggleExpandButtons() {\n    Object.entries(components).forEach(([name, component]) => {\n      component.expandButton.click();\n    });\n  }\n});\n"
  },
  {
    "path": "src/ui/layout/MctTree.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"treeContainer\"\n    class=\"c-tree-and-search\"\n    :class=\"{\n      'c-selector': isSelectorTree\n    }\"\n  >\n    <div ref=\"search\" class=\"c-tree-and-search__search\">\n      <Search\n        v-show=\"isSelectorTree\"\n        ref=\"shell-search\"\n        class=\"c-search\"\n        :value=\"searchValue\"\n        @input=\"searchTree\"\n        @clear=\"searchTree\"\n      />\n    </div>\n\n    <!-- search loading -->\n    <div\n      v-if=\"searchLoading && activeSearch\"\n      class=\"c-tree__item c-tree-and-search__loading loading\"\n    >\n      <span class=\"c-tree__item__label\">Searching...</span>\n    </div>\n\n    <!-- no results -->\n    <div v-if=\"showNoSearchResults\" class=\"c-tree-and-search__no-results\">No results found</div>\n\n    <!-- main tree -->\n    <div\n      ref=\"mainTree\"\n      class=\"c-tree-and-search__tree c-tree\"\n      role=\"tree\"\n      :aria-label=\"getAriaLabel\"\n      aria-expanded=\"true\"\n    >\n      <div\n        ref=\"dummyItem\"\n        class=\"c-tree__item-h\"\n        style=\"left: -1000px; position: absolute; visibility: hidden\"\n      >\n        <div class=\"c-tree__item\">\n          <span class=\"c-tree__item__view-control c-nav__up is-enabled\"></span>\n          <a class=\"c-tree__item__label c-object-label\" draggable=\"true\" href=\"#\">\n            <div class=\"c-tree__item__type-icon c-object-label__type-icon icon-folder\">\n              <span title=\"Open MCT\"></span>\n            </div>\n            <div class=\"c-tree__item__name c-object-label__name\">Open MCT</div>\n          </a>\n          <span class=\"c-tree__item__view-control c-nav__down\"></span>\n        </div>\n      </div>\n\n      <div\n        ref=\"scrollable\"\n        class=\"c-tree__scrollable\"\n        :style=\"scrollableStyles\"\n        @scroll=\"updateVisibleItems()\"\n      >\n        <div :style=\"childrenHeightStyles\">\n          <TreeItem\n            v-for=\"(treeItem, index) in visibleItems\"\n            :key=\"`${treeItem.navigationPath}-${index}-${treeItem.object.name}`\"\n            :node=\"treeItem\"\n            :draggable=\"true\"\n            :is-selector-tree=\"isSelectorTree\"\n            :selected-item=\"selectedItem\"\n            :active-search=\"activeSearch\"\n            :left-offset=\"!activeSearch ? treeItem.leftOffset : '0px'\"\n            :is-new=\"treeItem.isNew\"\n            :item-offset=\"itemOffset\"\n            :item-index=\"index\"\n            :item-height=\"itemHeight\"\n            :open-items=\"openTreeItems\"\n            :loading-items=\"treeItemLoading\"\n            :targeted-path=\"targetedPath\"\n            @tree-item-mounted=\"scrollToCheck($event)\"\n            @tree-item-action=\"treeItemAction(treeItem, $event)\"\n            @tree-item-selection=\"treeItemSelection(treeItem)\"\n            @targeted-path-animation-end=\"targetedPathAnimationEnd()\"\n          />\n          <!-- main loading -->\n          <div v-if=\"isLoading\" class=\"c-tree__item c-tree-and-search__loading loading\">\n            <span class=\"c-tree__item__label\">Loading...</span>\n          </div>\n          <!-- end loading -->\n          <div v-if=\"showNoItems\" class=\"c-tree__item c-tree__item--empty\">No items</div>\n        </div>\n      </div>\n    </div>\n    <!-- end main tree -->\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\nimport { markRaw, reactive } from 'vue';\n\nimport Search from '../components/SearchComponent.vue';\nimport TreeItem from './TreeItem.vue';\n\nconst ITEM_BUFFER = 25;\nconst LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';\nconst SORT_MY_ITEMS_ALPH_ASC = true;\nconst TREE_ITEM_INDENT_PX = 18;\nconst LOCATOR_ITEM_COUNT_HEIGHT = 10; // how many tree items to make the locator selection box show\n\nexport default {\n  name: 'MctTree',\n  components: {\n    Search,\n    TreeItem\n  },\n  inject: ['openmct'],\n  props: {\n    isSelectorTree: {\n      type: Boolean,\n      required: false,\n      default() {\n        return false;\n      }\n    },\n    initialSelection: {\n      type: Object,\n      required: false,\n      default() {\n        return {};\n      }\n    },\n    syncTreeNavigation: {\n      type: Boolean,\n      required: false\n    },\n    resetTreeNavigation: {\n      type: Boolean,\n      required: false\n    }\n  },\n  emits: ['tree-item-selection'],\n  data() {\n    return {\n      isLoading: false,\n      treeItemLoading: {},\n      mainTreeHeight: undefined,\n      searchLoading: false,\n      searchValue: '',\n      treeItems: [],\n      openTreeItems: [],\n      compositionCollections: {},\n      searchResultItems: [],\n      visibleItems: [],\n      updatingView: false,\n      itemHeight: 27,\n      itemOffset: 0,\n      activeSearch: false,\n      mainTreeTopMargin: undefined,\n      selectedItem: {},\n      targetedPath: ''\n    };\n  },\n  computed: {\n    childrenHeight() {\n      const childrenCount = this.focusedItems.length || 1;\n\n      return this.itemHeight * childrenCount - this.mainTreeTopMargin; // 5px margin\n    },\n    childrenHeightStyles() {\n      return { height: `${this.childrenHeight}px` };\n    },\n    focusedItems() {\n      return this.activeSearch ? this.searchResultItems : this.treeItems;\n    },\n    getAriaLabel() {\n      return this.isSelectorTree ? 'Create Modal Tree' : 'Main Tree';\n    },\n    pageThreshold() {\n      return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER;\n    },\n    scrollableStyles() {\n      return { height: `${this.mainTreeHeight}px` };\n    },\n    showNoItems() {\n      return (\n        this.visibleItems.length === 0 &&\n        !this.activeSearch &&\n        this.searchValue === '' &&\n        !this.isLoading\n      );\n    },\n    showNoSearchResults() {\n      return this.searchValue && this.searchResultItems.length === 0 && !this.searchLoading;\n    },\n    treeHeight() {\n      if (!this.isSelectorTree) {\n        return {};\n      } else {\n        return { minHeight: `${this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT}px` };\n      }\n    }\n  },\n  watch: {\n    syncTreeNavigation() {\n      this.searchValue = '';\n\n      // if there is an abort controller, then a search is in progress and will need to be canceled\n      if (this.abortSearchController) {\n        this.abortSearchController.abort();\n        delete this.abortSearchController;\n      }\n\n      if (!this.openmct.router.path) {\n        return;\n      }\n\n      this.$nextTick(this.showCurrentPathInTree);\n    },\n    resetTreeNavigation() {\n      [...this.openTreeItems].reverse().map(this.closeTreeItemByPath);\n    },\n    searchValue() {\n      if (this.searchValue !== '' && !this.activeSearch) {\n        this.activeSearch = true;\n        this.$refs.scrollable.scrollTop = 0;\n      } else if (this.searchValue === '') {\n        this.activeSearch = false;\n      }\n    },\n    mainTreeHeight() {\n      this.updateVisibleItems();\n    },\n    focusedItems: {\n      handler(val, oldVal) {\n        this.updateVisibleItems();\n      },\n      deep: true\n    },\n    openTreeItems: {\n      handler(val, oldVal) {\n        this.setSavedOpenItems();\n      },\n      deep: true\n    }\n  },\n  async mounted() {\n    this.initialize();\n    await this.loadRoot();\n    this.isLoading = false;\n\n    if (!this.isSelectorTree) {\n      await this.syncTreeOpenItems();\n    } else {\n      if (this.initialSelection.identifier) {\n        const objectPath = await this.openmct.objects.getOriginalPath(\n          this.initialSelection.identifier\n        );\n        const navigationPath = this.buildNavigationPath(objectPath);\n\n        this.openAndScrollTo(navigationPath);\n      }\n    }\n  },\n  created() {\n    this.getSearchResults = _.debounce(this.getSearchResults, 400);\n    this.handleTreeResize = _.debounce(this.handleTreeResize, 300);\n    this.scrollEndEvent = _.debounce(this.scrollEndEvent, 100);\n  },\n  unmounted() {\n    if (this.treeResizeObserver) {\n      this.treeResizeObserver.disconnect();\n    }\n\n    this.destroyObservers();\n    this.destroyMutables();\n  },\n  methods: {\n    initialize() {\n      this.observers = {};\n      this.mutables = {};\n      this.isLoading = true;\n      this.getSavedOpenItems();\n      this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);\n      this.treeResizeObserver.observe(this.$el);\n\n      // need to wait for the first tick to get the height of the tree\n      this.$nextTick().then(this.calculateHeights);\n\n      return;\n    },\n    async loadRoot() {\n      this.treeItems = [];\n      const root = await this.openmct.objects.get('ROOT');\n\n      if (!root.identifier) {\n        return false;\n      }\n\n      // will need to listen for root composition changes as well\n      this.treeItems = await this.loadAndBuildTreeItemsFor(root.identifier, []);\n    },\n    treeItemAction(parentItem, type) {\n      if (type === 'close') {\n        this.closeTreeItem(parentItem);\n      } else {\n        this.openTreeItem(parentItem);\n      }\n    },\n    targetedPathAnimationEnd() {\n      this.targetedPath = null;\n    },\n    treeItemSelection(item) {\n      this.selectedItem = item;\n      this.$emit('tree-item-selection', item);\n    },\n    async openTreeItem(parentItem) {\n      const parentPath = parentItem.navigationPath;\n      const abortSignal = this.startItemLoad(parentPath);\n\n      // pass in abort signal when functional\n      const childrenItems = await this.loadAndBuildTreeItemsFor(\n        parentItem.object.identifier,\n        parentItem.objectPath,\n        abortSignal\n      );\n      const parentIndex = this.treeItems.findIndex((item) => item.navigationPath === parentPath);\n\n      // if it's not loading, it was aborted\n      if (!this.isItemLoading(parentPath) || parentIndex === -1) {\n        return;\n      }\n\n      this.endItemLoad(parentPath);\n\n      const newTreeItems = [...this.treeItems];\n      newTreeItems.splice(parentIndex + 1, 0, ...childrenItems);\n      this.treeItems = [...newTreeItems];\n\n      if (!this.isTreeItemOpen(parentItem)) {\n        this.openTreeItems.push(parentPath);\n      }\n\n      for (let item of childrenItems) {\n        if (this.isTreeItemOpen(item)) {\n          this.openTreeItem(item);\n        }\n      }\n\n      return;\n    },\n    closeTreeItemByPath(path) {\n      // if actively loading, abort\n      if (this.isItemLoading(path)) {\n        this.abortItemLoad(path);\n      }\n\n      const pathIndex = this.openTreeItems.indexOf(path);\n\n      if (pathIndex === -1) {\n        return;\n      }\n\n      const newTreeItems = this.treeItems.filter((item) => {\n        const otherPath = item.navigationPath;\n        if (otherPath !== path && this.isTreeItemAChildOf(otherPath, path)) {\n          this.destroyObserverByPath(otherPath);\n          this.destroyMutableByPath(otherPath);\n\n          return false;\n        }\n\n        return true;\n      });\n      this.treeItems = [...newTreeItems];\n      const newOpenTreeItems = [...this.openTreeItems];\n      newOpenTreeItems.splice(pathIndex, 1);\n      this.openTreeItems = [...newOpenTreeItems];\n      this.removeCompositionListenerFor(path);\n    },\n    closeTreeItem(item) {\n      this.closeTreeItemByPath(item.navigationPath);\n    },\n    // returns an AbortController signal to be passed on to requests\n    startItemLoad(path) {\n      if (this.isItemLoading(path)) {\n        this.abortItemLoad(path);\n      }\n\n      this.treeItemLoading[path] = new AbortController();\n\n      return this.treeItemLoading[path].signal;\n    },\n    endItemLoad(path) {\n      this.treeItemLoading[path] = undefined;\n      delete this.treeItemLoading[path];\n    },\n    abortItemLoad(path) {\n      if (this.treeItemLoading[path]) {\n        this.treeItemLoading[path].abort();\n        this.endItemLoad(path);\n      }\n    },\n    isItemLoading(path) {\n      return this.treeItemLoading[path] instanceof AbortController;\n    },\n    showCurrentPathInTree() {\n      const currentPath = this.buildNavigationPath(this.openmct.router.path);\n\n      if (this.getTreeItemByPath(currentPath)) {\n        this.scrollTo(currentPath);\n      } else {\n        this.openAndScrollTo(currentPath);\n      }\n    },\n    async syncTreeOpenItems() {\n      const items = [...this.treeItems];\n\n      for (let item of items) {\n        if (this.isTreeItemOpen(item)) {\n          await this.openTreeItem(item);\n        }\n      }\n    },\n    openAndScrollTo(navigationPath) {\n      if (navigationPath.includes('/ROOT')) {\n        navigationPath = navigationPath.split('/ROOT').join('');\n      }\n\n      let idArray = navigationPath.split('/');\n      let fullPathArray = [];\n      let pathsToOpen;\n\n      this.scrollToPath = navigationPath;\n\n      // skip root\n      idArray.splice(0, 2);\n      idArray[0] = 'browse/' + idArray[0];\n      idArray.reduce((parentPath, childPath) => {\n        let fullPath = [parentPath, childPath].join('/');\n\n        fullPathArray.push(fullPath);\n\n        return fullPath;\n      }, '');\n\n      pathsToOpen = fullPathArray.filter(\n        (fullPath) => !this.isTreeItemPathOpen(fullPath) && fullPath !== navigationPath\n      );\n\n      pathsToOpen\n        .reduce(async (parentLoaded, childPath) => {\n          await parentLoaded;\n\n          return this.openTreeItem(this.getTreeItemByPath(childPath));\n        }, Promise.resolve())\n        .then(() => {\n          if (this.isSelectorTree) {\n            let item = this.getTreeItemByPath(navigationPath);\n            // If item is missing due to error in object creation,\n            // walk up the navigationPath until we find an item\n            while (!item && navigationPath !== '') {\n              const startIndex = 0;\n              const endIndex = navigationPath.lastIndexOf('/');\n              navigationPath = navigationPath.substring(startIndex, endIndex);\n\n              item = this.getTreeItemByPath(navigationPath);\n            }\n\n            this.treeItemSelection(item);\n          }\n\n          this.scrollToCheck(navigationPath);\n          this.scrollToPath = null;\n        });\n    },\n    scrollToCheck(navigationPath) {\n      if (this.scrollToPath && this.scrollToPath === navigationPath) {\n        this.scrollTo(navigationPath);\n      }\n    },\n    scrollTo(navigationPath) {\n      if (!this.$refs.scrollable || this.isItemInView(navigationPath)) {\n        return;\n      }\n\n      const indexOfScroll = this.treeItems.findIndex(\n        (item) => item.navigationPath === navigationPath\n      );\n\n      if (indexOfScroll !== -1) {\n        const scrollTopAmount = indexOfScroll * this.itemHeight;\n\n        this.$refs.scrollable.scrollTo({\n          top: scrollTopAmount,\n          behavior: 'smooth'\n        });\n      } else if (this.scrollToPath) {\n        this.scrollToPath = null;\n      }\n    },\n    scrollEndEvent() {\n      if (!this.$refs.scrollable) {\n        return;\n      }\n\n      this.$nextTick(() => {\n        if (this.scrollToPath) {\n          if (!this.isItemInView(this.scrollToPath)) {\n            this.scrollTo(this.scrollToPath);\n          } else {\n            this.scrollToPath = null;\n          }\n        }\n      });\n    },\n    setTargetedItem(navigationPath) {\n      this.targetedItem = navigationPath;\n    },\n    isItemInView(navigationPath) {\n      const indexOfScroll = this.treeItems.findIndex(\n        (item) => item.navigationPath === navigationPath\n      );\n      const scrollTopAmount = indexOfScroll * this.itemHeight;\n      const treeStart = this.$refs.scrollable.scrollTop;\n      const treeEnd = treeStart + this.mainTreeHeight;\n\n      return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd;\n    },\n    getLowercaseObjectName(domainObject) {\n      let objectName;\n      if (!domainObject) {\n        return objectName;\n      }\n\n      if (domainObject.name) {\n        objectName = domainObject.name.toLowerCase();\n      }\n\n      if (domainObject.object && domainObject.object.name) {\n        objectName = domainObject.object.name.toLowerCase();\n      }\n\n      return objectName;\n    },\n    sortNameAscending(a, b) {\n      // sorting tree children items\n      let objectAName = this.getLowercaseObjectName(a);\n      let objectBName = this.getLowercaseObjectName(b);\n      if (!objectAName || !objectBName) {\n        return 0;\n      }\n\n      // sorting composition items\n      if (objectAName > objectBName) {\n        return 1;\n      }\n\n      if (objectBName > objectAName) {\n        return -1;\n      }\n\n      return 0;\n    },\n    isSortable(parentObjectPath) {\n      // determine if any part of the parent's path includes a key value of mine; aka My Items\n      return Boolean(parentObjectPath.find((path) => path.identifier.key === 'mine'));\n    },\n    async loadAndBuildTreeItemsFor(identifier, parentObjectPath, abortSignal) {\n      const domainObject = await this.openmct.objects.get(identifier);\n\n      const collection = this.openmct.composition.get(domainObject);\n      let composition = await collection.load(abortSignal);\n\n      if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) {\n        const sortedComposition = composition.slice().sort(this.sortNameAscending);\n        composition = sortedComposition;\n      }\n\n      if (!this.isSelectorTree) {\n        let navigationPath = this.buildNavigationPath(parentObjectPath);\n\n        if (this.compositionCollections[navigationPath]) {\n          this.removeCompositionListenerFor(navigationPath);\n        }\n\n        this.compositionCollections[navigationPath] = {};\n        this.compositionCollections[navigationPath].collection = markRaw(collection);\n        this.compositionCollections[navigationPath].addHandler =\n          this.compositionAddHandler(navigationPath);\n        this.compositionCollections[navigationPath].removeHandler =\n          this.compositionRemoveHandler(navigationPath);\n\n        this.compositionCollections[navigationPath].collection.on(\n          'add',\n          this.compositionCollections[navigationPath].addHandler\n        );\n        this.compositionCollections[navigationPath].collection.on(\n          'remove',\n          this.compositionCollections[navigationPath].removeHandler\n        );\n      }\n\n      return composition.map((object) => {\n        // Only add observers and mutables if this is NOT a selector tree\n        if (!this.isSelectorTree) {\n          if (this.openmct.objects.supportsMutation(object.identifier)) {\n            object = this.openmct.objects.toMutable(object);\n            this.addMutable(object, parentObjectPath);\n          }\n\n          this.addTreeItemObserver(object, parentObjectPath);\n        }\n\n        return this.buildTreeItem(object, parentObjectPath);\n      });\n    },\n    buildTreeItem(domainObject, parentObjectPath, isNew = false) {\n      let objectPath = [domainObject].concat(parentObjectPath);\n      let navigationPath = this.buildNavigationPath(objectPath);\n\n      // Ensure that we create reactive objects for the tree\n      return reactive({\n        id: this.openmct.objects.makeKeyString(domainObject.identifier),\n        object: domainObject,\n        leftOffset: (objectPath.length - 1) * TREE_ITEM_INDENT_PX + 'px',\n        isNew,\n        objectPath,\n        navigationPath\n      });\n    },\n    addMutable(mutableDomainObject, parentObjectPath) {\n      const objectPath = [mutableDomainObject].concat(parentObjectPath);\n      const navigationPath = this.buildNavigationPath(objectPath);\n\n      // If the mutable already exists, destroy it.\n      this.destroyMutableByPath(navigationPath);\n\n      this.mutables[navigationPath] = () =>\n        this.openmct.objects.destroyMutable(mutableDomainObject);\n    },\n    addTreeItemObserver(domainObject, parentObjectPath) {\n      const objectPath = [domainObject].concat(parentObjectPath);\n      const navigationPath = this.buildNavigationPath(objectPath);\n\n      if (this.observers[navigationPath]) {\n        this.observers[navigationPath]();\n      }\n\n      this.observers[navigationPath] = this.openmct.objects.observe(\n        domainObject,\n        'name',\n        this.sortTreeItems.bind(this, parentObjectPath)\n      );\n    },\n    sortTreeItems(parentObjectPath) {\n      const navigationPath = this.buildNavigationPath(parentObjectPath);\n      const parentItem = this.getTreeItemByPath(navigationPath);\n\n      // If the parent is not sortable, skip sorting\n      if (!this.isSortable(parentObjectPath)) {\n        return;\n      }\n\n      // Sort the renamed object and its siblings (direct descendants of the parent)\n      const directDescendants = this.getChildrenInTreeFor(parentItem, false);\n      directDescendants.sort(this.sortNameAscending);\n\n      // Take a copy of the sorted descendants array\n      const sortedTreeItems = directDescendants.slice();\n\n      directDescendants.forEach((descendant) => {\n        const parent = this.getTreeItemByPath(descendant.navigationPath);\n\n        // If descendant is not open, skip\n        if (!this.isTreeItemOpen(parent)) {\n          return;\n        }\n\n        // If descendant is open but has no children, skip\n        const children = this.getChildrenInTreeFor(parent, true);\n        if (children.length === 0) {\n          return;\n        }\n\n        // Splice in the children of the descendant\n        const parentIndex = sortedTreeItems\n          .map((item) => item.navigationPath)\n          .indexOf(parent.navigationPath);\n        sortedTreeItems.splice(parentIndex + 1, 0, ...children);\n      });\n\n      // Splice in all of the sorted descendants\n      const newTreeItems = [...this.treeItems];\n      newTreeItems.splice(\n        newTreeItems.indexOf(parentItem) + 1,\n        sortedTreeItems.length,\n        ...sortedTreeItems\n      );\n      this.treeItems = [...newTreeItems];\n    },\n    buildNavigationPath(objectPath) {\n      return (\n        '/browse/' +\n        [...objectPath]\n          .reverse()\n          .map((object) => this.openmct.objects.makeKeyString(object.identifier))\n          .join('/')\n      );\n    },\n    compositionAddHandler(navigationPath) {\n      return (domainObject) => {\n        const parentItem = this.getTreeItemByPath(navigationPath);\n        const parentObjectPath = parentItem?.objectPath || [];\n        const newItem = this.buildTreeItem(domainObject, parentObjectPath, true);\n        const descendants = this.getChildrenInTreeFor(navigationPath, true);\n        const directDescendants = this.getChildrenInTreeFor(navigationPath);\n\n        if (domainObject.isMutable) {\n          this.addMutable(domainObject, parentObjectPath);\n        }\n\n        this.addTreeItemObserver(domainObject, parentObjectPath);\n\n        if (directDescendants.length === 0) {\n          this.addItemToTreeAfter(newItem, parentItem);\n\n          return;\n        }\n\n        if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) {\n          const newItemIndex = directDescendants.findIndex(\n            (descendant) => this.sortNameAscending(descendant, newItem) > 0\n          );\n          const shouldInsertFirst = newItemIndex === 0;\n          const shouldInsertLast = newItemIndex === -1;\n\n          if (shouldInsertFirst) {\n            this.addItemToTreeAfter(newItem, parentItem);\n          } else if (shouldInsertLast) {\n            this.addItemToTreeAfter(newItem, descendants.pop());\n          } else {\n            this.addItemToTreeBefore(newItem, directDescendants[newItemIndex]);\n          }\n\n          return;\n        }\n\n        this.addItemToTreeAfter(newItem, descendants.pop());\n      };\n    },\n    compositionRemoveHandler(navigationPath) {\n      return (identifier) => {\n        const removeKeyString = this.openmct.objects.makeKeyString(identifier);\n        const directDescendants = this.getChildrenInTreeFor(navigationPath);\n        const removeItem = directDescendants.find((item) => item.id === removeKeyString);\n\n        // Remove the item from the tree, unobserve it, and clean up any mutables\n        this.removeItemFromTree(removeItem);\n        this.destroyObserverByPath(removeItem.navigationPath);\n        this.destroyMutableByPath(removeItem.navigationPath);\n      };\n    },\n    removeCompositionListenerFor(navigationPath) {\n      if (this.compositionCollections[navigationPath]) {\n        this.compositionCollections[navigationPath].collection.off(\n          'add',\n          this.compositionCollections[navigationPath].addHandler\n        );\n        this.compositionCollections[navigationPath].collection.off(\n          'remove',\n          this.compositionCollections[navigationPath].removeHandler\n        );\n\n        this.compositionCollections[navigationPath] = undefined;\n        delete this.compositionCollections[navigationPath];\n      }\n    },\n    removeItemFromTree(item) {\n      if (this.isTreeItemOpen(item)) {\n        this.closeTreeItem(item);\n      }\n\n      const removeIndex = this.getTreeItemIndex(item.navigationPath);\n      const newTreeItems = [...this.treeItems];\n      newTreeItems.splice(removeIndex, 1);\n      this.treeItems = [...newTreeItems];\n    },\n    addItemToTreeBefore(addItem, beforeItem) {\n      const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);\n\n      this.addItemToTree(addItem, addIndex);\n    },\n    addItemToTreeAfter(addItem, afterItem) {\n      const addIndex = this.getTreeItemIndex(afterItem.navigationPath);\n\n      this.addItemToTree(addItem, addIndex + 1);\n    },\n    addItemToTree(addItem, index) {\n      const newTreeItems = [...this.treeItems];\n      newTreeItems.splice(index, 0, addItem);\n      this.treeItems = [...newTreeItems];\n\n      if (this.isTreeItemOpen(addItem)) {\n        this.openTreeItem(addItem);\n      }\n    },\n    searchTree(value) {\n      // if an abort controller exists, regardless of the value passed in,\n      // there is an active search that should be canceled\n      if (this.abortSearchController) {\n        this.abortSearchController.abort();\n        delete this.abortSearchController;\n      }\n\n      this.searchValue = value;\n      this.searchLoading = true;\n\n      if (this.searchValue !== '') {\n        // clear any previous search results\n        this.searchResultItems = [];\n\n        this.getSearchResults();\n      } else {\n        this.searchLoading = false;\n      }\n    },\n    getSearchResults() {\n      // an abort controller will be passed in that will be used\n      // to cancel an active searches if necessary\n      this.abortSearchController = new AbortController();\n      const abortSignal = this.abortSearchController.signal;\n      const searchPromises = this.openmct.objects.search(this.searchValue, abortSignal);\n\n      searchPromises.map((promise) =>\n        promise.then((results) => {\n          this.aggregateSearchResults(results, abortSignal);\n        })\n      );\n\n      Promise.all(searchPromises)\n        .catch((reason) => {\n          // search aborted\n        })\n        .finally(() => {\n          this.searchLoading = false;\n\n          if (this.abortSearchController) {\n            delete this.abortSearchController;\n          }\n        });\n    },\n    aggregateSearchResults(results, abortSignal) {\n      let resultPromises = [];\n\n      for (const result of results) {\n        if (!abortSignal.aborted) {\n          // Don't show deleted objects in search results\n          if (result.location === null) {\n            continue;\n          }\n\n          resultPromises.push(\n            this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {\n              // removing the item itself, as the path we pass to buildTreeItem is a parent path\n              objectPath.shift();\n\n              // if root, remove, we're not using in object path for tree\n              const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;\n              if (lastObject && lastObject.type === 'root') {\n                objectPath.pop();\n              }\n\n              this.searchResultItems.push(this.buildTreeItem(result, objectPath));\n            })\n          );\n        }\n      }\n\n      return resultPromises;\n    },\n    updateVisibleItems() {\n      this.scrollEndEvent();\n\n      if (this.updatingView) {\n        return;\n      }\n\n      this.updatingView = true;\n      requestAnimationFrame(() => {\n        let start = 0;\n        let end = this.pageThreshold;\n        let allItemsCount = this.focusedItems.length;\n\n        if (allItemsCount < this.pageThreshold) {\n          end = allItemsCount;\n        } else {\n          let firstVisible = this.calculateFirstVisibleItem();\n          let lastVisible = this.calculateLastVisibleItem();\n          let totalVisible = lastVisible - firstVisible;\n          let numberOffscreen = this.pageThreshold - totalVisible;\n\n          start = firstVisible - Math.floor(numberOffscreen / 2);\n          end = lastVisible + Math.ceil(numberOffscreen / 2);\n\n          if (start < 0) {\n            start = 0;\n            end = Math.min(this.pageThreshold, allItemsCount);\n          } else if (end >= allItemsCount) {\n            end = allItemsCount;\n            start = end - this.pageThreshold + 1;\n          }\n        }\n\n        this.itemOffset = start;\n        this.visibleItems = this.focusedItems.slice(start, end);\n        this.updatingView = false;\n      });\n    },\n    calculateFirstVisibleItem() {\n      if (!this.$refs.scrollable) {\n        return;\n      }\n\n      let scrollTop = this.$refs.scrollable.scrollTop;\n\n      return Math.floor(scrollTop / this.itemHeight);\n    },\n    calculateLastVisibleItem() {\n      if (!this.$refs.scrollable) {\n        return;\n      }\n\n      let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight;\n\n      return Math.ceil(scrollBottom / this.itemHeight);\n    },\n    calculateHeights() {\n      const RECHECK = 100;\n\n      return new Promise((resolve, reject) => {\n        let checkHeights = () => {\n          let treeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');\n          let paddingOffset = 0;\n\n          if (\n            this.$el &&\n            this.$refs.search &&\n            this.$refs.mainTree &&\n            this.$refs.treeContainer &&\n            this.$refs.dummyItem &&\n            this.$el.offsetHeight !== 0 &&\n            treeTopMargin > 0\n          ) {\n            if (this.isSelectorTree) {\n              paddingOffset = this.getElementStyleValue(this.$refs.treeContainer, 'padding');\n            }\n\n            this.mainTreeTopMargin = treeTopMargin;\n            this.mainTreeHeight =\n              this.$el.offsetHeight -\n              this.$refs.search.offsetHeight -\n              this.mainTreeTopMargin -\n              paddingOffset * 2;\n            this.itemHeight = this.getElementStyleValue(this.$refs.dummyItem, 'height');\n\n            resolve();\n          } else {\n            setTimeout(checkHeights, RECHECK);\n          }\n        };\n\n        checkHeights();\n      });\n    },\n    getTreeItemByPath(path) {\n      return this.treeItems.find((item) => item.navigationPath === path);\n    },\n    getTreeItemIndex(indexItem) {\n      let path = typeof indexItem === 'string' ? indexItem : indexItem.navigationPath;\n\n      return this.treeItems.findIndex((item) => item.navigationPath === path);\n    },\n    getChildrenInTreeFor(parent, allDescendants = false) {\n      const parentPath = typeof parent === 'string' ? parent : parent.navigationPath;\n      const parentDepth = parentPath.split('/').length;\n\n      return this.treeItems.filter((childItem) => {\n        const childDepth = childItem.navigationPath.split('/').length;\n        if (!allDescendants && childDepth > parentDepth + 1) {\n          return false;\n        }\n\n        return (\n          childItem.navigationPath !== parentPath && childItem.navigationPath.includes(parentPath)\n        );\n      });\n    },\n    isTreeItemOpen(item) {\n      return this.isTreeItemPathOpen(item.navigationPath);\n    },\n    isTreeItemPathOpen(path) {\n      return this.openTreeItems.includes(path);\n    },\n    isTreeItemAChildOf(childNavigationPath, parentNavigationPath) {\n      const childPathKeys = childNavigationPath.split('/');\n      const parentPathKeys = parentNavigationPath.split('/');\n\n      // If child path is shorter than or same length as\n      // the parent path, then it's not a child.\n      if (childPathKeys.length <= parentPathKeys.length) {\n        return false;\n      }\n\n      for (let i = 0; i < parentPathKeys.length; i++) {\n        if (childPathKeys[i] !== parentPathKeys[i]) {\n          return false;\n        }\n      }\n\n      return true;\n    },\n    getElementStyleValue(el, style) {\n      if (!el) {\n        return;\n      }\n\n      let styleString = window.getComputedStyle(el)[style];\n      let index = styleString.indexOf('px');\n\n      return Number(styleString.slice(0, index));\n    },\n    getSavedOpenItems() {\n      if (this.isSelectorTree) {\n        return;\n      }\n\n      let openItems = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);\n      this.openTreeItems = openItems ? JSON.parse(openItems) : [];\n    },\n    setSavedOpenItems() {\n      if (this.isSelectorTree) {\n        return;\n      }\n\n      localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(this.openTreeItems));\n    },\n    handleTreeResize() {\n      this.calculateHeights();\n    },\n    /**\n     * Destroy an observer for the given navigationPath.\n     */\n    destroyObserverByPath(navigationPath) {\n      if (this.observers[navigationPath]) {\n        this.observers[navigationPath]();\n        delete this.observers[navigationPath];\n      }\n    },\n    /**\n     * Destroy all observers.\n     */\n    destroyObservers() {\n      Object.entries(this.observers).forEach(([key, unobserve]) => {\n        if (unobserve) {\n          unobserve();\n        }\n\n        delete this.observers[key];\n      });\n    },\n    /**\n     * Destroy a mutable for the given navigationPath.\n     */\n    destroyMutableByPath(navigationPath) {\n      if (this.mutables[navigationPath]) {\n        this.mutables[navigationPath]();\n        delete this.mutables[navigationPath];\n      }\n    },\n    /**\n     * Destroy all mutables.\n     */\n    destroyMutables() {\n      Object.entries(this.mutables).forEach(([key, destroyMutable]) => {\n        if (destroyMutable) {\n          destroyMutable();\n        }\n\n        delete this.mutables[key];\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/MultipaneContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"l-multipane\"\n    :class=\"{\n      'l-multipane--vertical': type === 'vertical',\n      'l-multipane--horizontal': type === 'horizontal'\n    }\"\n  >\n    <slot></slot>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    type: {\n      type: String,\n      required: true,\n      validator: function (value) {\n        return ['vertical', 'horizontal'].indexOf(value) !== -1;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/PaneContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"l-pane\" :class=\"paneClasses\">\n    <div\n      v-if=\"handle\"\n      class=\"l-pane__handle\"\n      :aria-label=\"handleLabel\"\n      :aria-grabbed=\"resizing\"\n      @mousedown.prevent=\"startResizing\"\n    ></div>\n    <div class=\"l-pane__header\">\n      <span v-if=\"label\" class=\"l-pane__label\">{{ label }}</span>\n      <slot name=\"controls\"></slot>\n      <button\n        v-if=\"isCollapsable\"\n        class=\"l-pane__collapse-button c-icon-button\"\n        :name=\"collapseTitle\"\n        :aria-label=\"collapseTitle\"\n        :title=\"collapseTitle\"\n        @click=\"toggleCollapse\"\n      ></button>\n    </div>\n    <button\n      class=\"l-pane__expand-button\"\n      :name=\"expandTitle\"\n      :aria-label=\"expandTitle\"\n      :title=\"expandTitle\"\n      @click=\"toggleCollapse\"\n    >\n      <span class=\"l-pane__expand-button__label\">{{ label }}</span>\n    </button>\n    <div class=\"l-pane__contents\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nconst COLLAPSE_THRESHOLD_PX = 40;\nconst LOCAL_STORAGE_KEY__PANE_POSITIONS = 'mct-pane-positions';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    collapseType: {\n      type: String,\n      default: 'vertical'\n    },\n    handle: {\n      type: String,\n      default: '',\n      validator: function (value) {\n        return ['', 'before', 'after'].indexOf(value) !== -1;\n      }\n    },\n    label: {\n      type: String,\n      default: ''\n    },\n    hideParam: {\n      type: String,\n      default: ''\n    },\n    persistPosition: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['end-resizing', 'start-resizing'],\n  data() {\n    return {\n      collapsed: false,\n      resizing: false\n    };\n  },\n  computed: {\n    handleLabel() {\n      return `Resize ${this.label} Pane`;\n    },\n    isCollapsable() {\n      return this.hideParam?.length > 0;\n    },\n    collapseTitle() {\n      return `Collapse ${this.label} Pane`;\n    },\n    expandTitle() {\n      return `Expand ${this.label} Pane`;\n    },\n    localStorageKey() {\n      if (!this.label) {\n        return null;\n      }\n\n      return this.label.toLowerCase().replace(/ /g, '-');\n    },\n    paneClasses() {\n      return {\n        'l-pane--horizontal-handle-before': this.type === 'horizontal' && this.handle === 'before',\n        'l-pane--horizontal-handle-after': this.type === 'horizontal' && this.handle === 'after',\n        'l-pane--vertical-handle-before': this.type === 'vertical' && this.handle === 'before',\n        'l-pane--vertical-handle-after': this.type === 'vertical' && this.handle === 'after',\n        'l-pane--collapsed': this.collapsed,\n        'collapse-horizontal': this.collapseType === 'horizontal',\n        'l-pane--reacts': !this.handle,\n        'l-pane--resizing': this.resizing === true\n      };\n    }\n  },\n  beforeMount() {\n    this.type = this.$parent.type;\n    this.styleProp = this.type === 'horizontal' ? 'width' : 'height';\n  },\n  created() {\n    this.handleHideUrl = this.handleHideUrl.bind(this);\n    // Hide tree and/or inspector pane if specified in URL\n    this.openmct.router.on('change:params', this.handleHideUrl);\n  },\n  async mounted() {\n    if (this.persistPosition) {\n      const savedPosition = this.getSavedPosition();\n      if (savedPosition) {\n        this.$el.style[this.styleProp] = savedPosition;\n      }\n    }\n\n    await this.$nextTick();\n    if (this.isCollapsable) {\n      this.handleHideUrl();\n    }\n  },\n  beforeUnmount() {\n    this.openmct.router.off('change:params', this.handleHideUrl);\n  },\n  methods: {\n    addHideParam(target) {\n      this.openmct.router.setSearchParam(target, 'true');\n    },\n    endResizing(_event) {\n      document.body.removeEventListener('mousemove', this.updatePosition);\n      document.body.removeEventListener('mouseup', this.endResizing);\n      this.resizing = false;\n      this.$emit('end-resizing');\n      this.trackSize();\n    },\n    getNewSize(event) {\n      const delta = this.startPosition - this.getPosition(event);\n      if (this.handle === 'before') {\n        return `${this.initial + delta}px`;\n      }\n\n      if (this.handle === 'after') {\n        return `${this.initial - delta}px`;\n      }\n    },\n    getSavedPosition() {\n      if (!this.localStorageKey) {\n        return null;\n      }\n\n      const savedPositionsString = localStorage.getItem(LOCAL_STORAGE_KEY__PANE_POSITIONS);\n      const savedPositions = savedPositionsString ? JSON.parse(savedPositionsString) : {};\n\n      return savedPositions[this.localStorageKey];\n    },\n    getPosition(event) {\n      return this.type === 'horizontal' ? event.pageX : event.pageY;\n    },\n    handleCollapse() {\n      this.currentSize = this.dragCollapse === true ? this.initial : this.$el.style[this.styleProp];\n      this.$el.style[this.styleProp] = '';\n      this.collapsed = true;\n    },\n    handleExpand() {\n      let size = this.currentSize ? this.currentSize : this.getSavedPosition();\n      this.$el.style[this.styleProp] = size;\n\n      delete this.currentSize;\n      delete this.dragCollapse;\n      this.collapsed = false;\n    },\n    handleHideUrl() {\n      const hideParam = this.openmct.router.getSearchParam(this.hideParam);\n\n      if (hideParam === 'true') {\n        this.handleCollapse();\n      }\n    },\n    removeHideParam(target) {\n      this.openmct.router.deleteSearchParam(target);\n    },\n    setSavedPosition(panePosition) {\n      const panePositionsString = localStorage.getItem(LOCAL_STORAGE_KEY__PANE_POSITIONS);\n      const panePositions = panePositionsString ? JSON.parse(panePositionsString) : {};\n      panePositions[this.localStorageKey] = panePosition;\n      localStorage.setItem(LOCAL_STORAGE_KEY__PANE_POSITIONS, JSON.stringify(panePositions));\n    },\n    startResizing(event) {\n      this.startPosition = this.getPosition(event);\n      document.body.addEventListener('mousemove', this.updatePosition);\n      document.body.addEventListener('mouseup', this.endResizing);\n      this.resizing = true;\n      this.$emit('start-resizing');\n      this.trackSize();\n    },\n    toggleCollapse(_event) {\n      if (this.collapsed) {\n        this.handleExpand();\n        this.removeHideParam(this.hideParam);\n      } else {\n        this.handleCollapse();\n        this.addHideParam(this.hideParam);\n      }\n    },\n    trackSize() {\n      if (!this.dragCollapse) {\n        if (this.type === 'vertical') {\n          this.initial = this.$el.offsetHeight;\n        } else if (this.type === 'horizontal') {\n          this.initial = this.$el.offsetWidth;\n        }\n\n        if (this.persistPosition) {\n          this.setSavedPosition(`${this.initial}px`);\n        }\n      }\n    },\n    updatePosition(event) {\n      const size = this.getNewSize(event);\n      const intSize = parseInt(size.substr(0, size.length - 2), 10);\n      if (intSize < COLLAPSE_THRESHOLD_PX && this.isCollapsable === true) {\n        this.dragCollapse = true;\n        this.endResizing();\n        this.toggleCollapse();\n      } else {\n        this.$el.style[this.styleProp] = size;\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/RecentObjectsList.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-tree-and-search l-shell__tree\">\n    <ul class=\"c-tree-and-search__tree c-tree c-tree__scrollable\" aria-label=\"Recent Objects\">\n      <RecentObjectsListItem\n        v-for=\"recentObject in recentObjects\"\n        :key=\"recentObject.navigationPath\"\n        :object-path=\"recentObject.objectPath\"\n        :navigation-path=\"recentObject.navigationPath\"\n        :domain-object=\"recentObject.domainObject\"\n        @open-and-scroll-to=\"openAndScrollTo($event)\"\n      />\n    </ul>\n  </div>\n</template>\n\n<script>\nconst MAX_RECENT_ITEMS = 20;\nconst LOCAL_STORAGE_KEY__RECENT_OBJECTS = 'mct-recent-objects';\nimport RecentObjectsListItem from './RecentObjectsListItem.vue';\nexport default {\n  name: 'RecentObjectsList',\n  components: {\n    RecentObjectsListItem\n  },\n  inject: ['openmct'],\n  props: {},\n  emits: ['open-and-scroll-to', 'set-clear-button-disabled'],\n  data() {\n    return {\n      recents: []\n    };\n  },\n  computed: {\n    recentObjects() {\n      return this.recents.filter((recentObject) => {\n        return recentObject.location !== null;\n      });\n    }\n  },\n  mounted() {\n    this.compositionCollections = {};\n    this.nameChangeListeners = {};\n    this.openmct.router.on('change:path', this.onPathChange);\n    this.getSavedRecentItems();\n  },\n  unmounted() {\n    this.openmct.router.off('change:path', this.onPathChange);\n    Object.values(this.nameChangeListeners).forEach((unlisten) => {\n      unlisten();\n    });\n  },\n  methods: {\n    addNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (!this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString] = this.openmct.objects.observe(\n          domainObject,\n          'name',\n          this.updateRecentObjectName.bind(this, keyString)\n        );\n      }\n    },\n    updateRecentObjectName(keyString, newName) {\n      this.recents = this.recents.map((recentObject) => {\n        if (\n          this.openmct.objects.makeKeyString(recentObject.domainObject.identifier) === keyString\n        ) {\n          return {\n            ...recentObject,\n            domainObject: { ...recentObject.domainObject, name: newName }\n          };\n        }\n        return recentObject;\n      });\n    },\n    removeNameListenerFor(domainObject) {\n      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);\n      if (this.nameChangeListeners[keyString]) {\n        this.nameChangeListeners[keyString]();\n        delete this.nameChangeListeners[keyString];\n      }\n    },\n    /**\n     * Add a composition collection to the map and register its remove handler\n     * @param {string} navigationPath\n     */\n    addCompositionListenerFor(navigationPath) {\n      this.compositionCollections[navigationPath].removeHandler =\n        this.compositionRemoveHandler(navigationPath);\n      this.compositionCollections[navigationPath].collection.on(\n        'remove',\n        this.compositionCollections[navigationPath].removeHandler\n      );\n    },\n    /**\n     * Handler for composition collection remove events.\n     * Removes the object and any of its children from the recents list.\n     * @param {string} navigationPath\n     */\n    compositionRemoveHandler(navigationPath) {\n      /**\n       * @param {import('../../api/objects/ObjectAPI').Identifier | string} identifier\n       */\n      return (identifier) => {\n        // Construct the navigationPath of the removed object itself\n        const removedNavigationPath = `${navigationPath}/${this.openmct.objects.makeKeyString(\n          identifier\n        )}`;\n\n        // Remove the object and any of its children from the recents list\n        this.recents = this.recents.filter((recentObject) => {\n          return !recentObject.navigationPath.includes(removedNavigationPath);\n        });\n\n        this.removeCompositionListenerFor(removedNavigationPath);\n      };\n    },\n    /**\n     * Restores the RecentObjects list from localStorage, retrieves composition collections,\n     * and registers composition listeners for composable objects.\n     */\n    getSavedRecentItems() {\n      const savedRecentsString = localStorage.getItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);\n      const savedRecents = savedRecentsString ? JSON.parse(savedRecentsString) : [];\n\n      // Get composition collections and add composition listeners for composable objects\n      savedRecents.forEach((recentObject) => {\n        const { domainObject, navigationPath } = recentObject;\n        this.addNameListenerFor(domainObject);\n        if (this.shouldTrackCompositionFor(domainObject)) {\n          this.compositionCollections[navigationPath] = {};\n          this.compositionCollections[navigationPath].collection =\n            this.openmct.composition.get(domainObject);\n          this.addCompositionListenerFor(navigationPath);\n        }\n      });\n\n      this.recents = savedRecents;\n    },\n    /**\n     * Handler for 'change:path' router events.\n     * Adds or moves to the top the object at the given path to the recents list.\n     * Registers compositionCollection listeners for composable objects.\n     * Enforces the MAX_RECENT_ITEMS limit.\n     * @param {string} navigationPath\n     */\n    async onPathChange(navigationPath) {\n      // Short-circuit if the path is not a navigationPath\n      if (!navigationPath.startsWith('/browse/')) {\n        return;\n      }\n\n      const objectPath = await this.openmct.objects.getRelativeObjectPath(navigationPath);\n      if (!objectPath.length) {\n        return;\n      }\n\n      const domainObject = objectPath[0];\n\n      // Get rid of '/ROOT' if it exists in the navigationPath.\n      // Handles for the case of navigating to \"My Items\" from a RecentObjectsListItem.\n      // Could lead to dupes of \"My Items\" in the RecentObjectsList if we don't drop the 'ROOT' here.\n      if (navigationPath.includes('/ROOT')) {\n        navigationPath = navigationPath.replace('/ROOT', '');\n      }\n\n      if (this.shouldTrackCompositionFor(domainObject, navigationPath)) {\n        this.compositionCollections[navigationPath] = {};\n        this.compositionCollections[navigationPath].collection =\n          this.openmct.composition.get(domainObject);\n        this.addCompositionListenerFor(navigationPath);\n      }\n\n      // Don't add deleted objects to the recents list\n      if (domainObject?.location === null) {\n        return;\n      }\n\n      this.addNameListenerFor(domainObject);\n\n      // Move the object to the top if its already existing in the recents list\n      const existingIndex = this.recents.findIndex((recentObject) => {\n        return navigationPath === recentObject.navigationPath;\n      });\n      if (existingIndex !== -1) {\n        this.recents.splice(existingIndex, 1);\n      }\n\n      this.recents.unshift({\n        objectPath,\n        navigationPath,\n        domainObject\n      });\n\n      // Enforce a max number of recent items\n      while (this.recents.length > MAX_RECENT_ITEMS) {\n        const poppedRecentItem = this.recents.pop();\n        this.removeCompositionListenerFor(poppedRecentItem.navigationPath);\n        this.removeNameListenerFor(poppedRecentItem.domainObject);\n      }\n\n      this.setSavedRecentItems();\n    },\n    /**\n     * Delete the composition collection and unregister its remove handler\n     * @param {string} navigationPath\n     */\n    removeCompositionListenerFor(navigationPath) {\n      if (this.compositionCollections[navigationPath]) {\n        this.compositionCollections[navigationPath].collection.off(\n          'remove',\n          this.compositionCollections[navigationPath].removeHandler\n        );\n        delete this.compositionCollections[navigationPath];\n      }\n    },\n    openAndScrollTo(navigationPath) {\n      this.$emit('open-and-scroll-to', navigationPath);\n    },\n    /**\n     * Saves the Recent Objects list to localStorage.\n     */\n    setSavedRecentItems() {\n      localStorage.setItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS, JSON.stringify(this.recents));\n      // send event to parent for enabled button\n      if (this.recents.length === 1) {\n        this.$emit('set-clear-button-disabled', false);\n      }\n    },\n    /**\n     * Returns true if the `domainObject` supports composition and we are not already\n     * tracking its composition.\n     * @param {import('openmct').DomainObject} domainObject\n     * @param {string} navigationPath\n     */\n    shouldTrackCompositionFor(domainObject, navigationPath) {\n      return (\n        this.compositionCollections[navigationPath] === undefined &&\n        this.openmct.composition.supportsComposition(domainObject)\n      );\n    },\n    /**\n     * Clears the Recent Objects list in localStorage and in the component.\n     * Before clearing, prompts the user to confirm the action with a dialog.\n     */\n    clearRecentObjects() {\n      const dialog = this.openmct.overlays.dialog({\n        title: 'Clear Recently Viewed Objects',\n        iconClass: 'alert',\n        message:\n          'This action will clear the Recently Viewed Objects list. Are you sure you want to continue?',\n        buttons: [\n          {\n            label: 'Ok',\n            callback: () => {\n              localStorage.removeItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);\n              Object.values(this.nameChangeListeners).forEach((unlisten) => {\n                unlisten();\n              });\n              this.recents = [];\n              dialog.dismiss();\n              this.$emit('set-clear-button-disabled', true);\n            }\n          },\n          {\n            label: 'Cancel',\n            callback: () => {\n              dialog.dismiss();\n            }\n          }\n        ]\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/RecentObjectsListItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <li\n    class=\"c-recentobjects-listitem c-recentobjects-listitem--object\"\n    :class=\"isAlias\"\n    :aria-label=\"`${domainObject.name}`\"\n  >\n    <div\n      class=\"c-recentobjects-listitem__type-icon recent-object-icon\"\n      :class=\"resultTypeIcon\"\n    ></div>\n    <div class=\"c-recentobjects-listitem__body\">\n      <span\n        ref=\"recentObjectName\"\n        class=\"c-recentobjects-listitem__title\"\n        :name=\"domainObject.name\"\n        draggable=\"true\"\n        @dragstart=\"dragStart\"\n        @click.prevent=\"clickedRecent\"\n        @mouseover.ctrl=\"showToolTip\"\n        @mouseleave=\"hideToolTip\"\n      >\n        {{ domainObject.name }}\n      </span>\n\n      <ObjectPath\n        class=\"c-recentobjects-listitem__object-path\"\n        :read-only=\"false\"\n        :domain-object=\"domainObject\"\n        :object-path=\"objectPath\"\n      />\n    </div>\n    <div class=\"c-recentobjects-listitem__target-button\">\n      <button\n        class=\"c-icon-button icon-target\"\n        :aria-label=\"`Open and scroll to ${domainObject.name}`\"\n        @click=\"openAndScrollTo(navigationPath)\"\n      ></button>\n    </div>\n  </li>\n</template>\n\n<script>\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport tooltipHelpers from '../../api/tooltips/tooltipMixins.js';\nimport ObjectPath from '../components/ObjectPath.vue';\n\nexport default {\n  name: 'RecentObjectsListItem',\n  components: {\n    ObjectPath\n  },\n  mixins: [tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    domainObject: {\n      type: Object,\n      required: true\n    },\n    navigationPath: {\n      type: String,\n      required: true\n    },\n    objectPath: {\n      type: Array,\n      required: true\n    }\n  },\n  emits: ['open-and-scroll-to', 'preview-changed'],\n  computed: {\n    isAlias() {\n      return this.openmct.objects.isObjectPathToALink(this.domainObject, this.objectPath)\n        ? { 'is-alias': true }\n        : undefined;\n    },\n    resultTypeIcon() {\n      return this.openmct.types.get(this.domainObject.type).definition.cssClass;\n    }\n  },\n  mounted() {\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n    this.previewAction.on('isVisible', this.togglePreviewState);\n  },\n  unmounted() {\n    this.previewAction.off('isVisible', this.togglePreviewState);\n  },\n  methods: {\n    clickedRecent(_event) {\n      if (this.openmct.editor.isEditing()) {\n        this.preview();\n      } else {\n        this.openmct.router.navigate(`#${this.navigationPath}`);\n      }\n    },\n    togglePreviewState(previewState) {\n      this.$emit('preview-changed', previewState);\n    },\n    preview() {\n      if (this.previewAction.appliesTo(this.objectPath)) {\n        this.previewAction.invoke(this.objectPath);\n      }\n    },\n    dragStart(event) {\n      const navigatedObject = this.openmct.router.path[0];\n      const serializedPath = JSON.stringify(this.objectPath);\n      const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);\n      if (this.openmct.composition.checkPolicy(navigatedObject, this.domainObject)) {\n        event.dataTransfer.setData(\n          'openmct/composable-domain-object',\n          JSON.stringify(this.domainObject)\n        );\n      }\n\n      event.dataTransfer.setData('openmct/domain-object-path', serializedPath);\n      event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);\n    },\n    openAndScrollTo(navigationPath) {\n      this.$emit('open-and-scroll-to', navigationPath);\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(), BELOW, 'recentObjectName');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/ResizeHandle/ResizeHandle.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    v-show=\"isEditing && !isDragging\"\n    class=\"c-fl-frame__resize-handle\"\n    :class=\"[dragOrientation]\"\n    :aria-grabbed=\"isGrabbed\"\n    @mousedown=\"mousedown\"\n  ></div>\n</template>\n\n<script>\nexport default {\n  props: {\n    dragOrientation: {\n      type: String,\n      required: true\n    },\n    index: {\n      type: Number,\n      required: true\n    },\n    isEditing: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['move', 'init-move', 'end-move'],\n  data() {\n    return {\n      initialPos: 0,\n      isDragging: false,\n      isGrabbed: false\n    };\n  },\n  mounted() {\n    document.addEventListener('dragstart', this.setDragging);\n    document.addEventListener('dragend', this.unsetDragging);\n    document.addEventListener('drop', this.unsetDragging);\n  },\n  beforeUnmount() {\n    document.removeEventListener('dragstart', this.setDragging);\n    document.removeEventListener('dragend', this.unsetDragging);\n    document.removeEventListener('drop', this.unsetDragging);\n  },\n  methods: {\n    mousedown(event) {\n      event.preventDefault();\n\n      this.isGrabbed = true;\n      this.$emit('init-move', this.index);\n\n      document.body.addEventListener('mousemove', this.mousemove);\n      document.body.addEventListener('mouseup', this.mouseup);\n    },\n    mousemove(event) {\n      event.preventDefault();\n\n      let elSize;\n      let mousePos;\n      let delta;\n\n      if (this.dragOrientation === 'horizontal') {\n        elSize = this.$el.getBoundingClientRect().x;\n        mousePos = event.clientX;\n      } else {\n        elSize = this.$el.getBoundingClientRect().y;\n        mousePos = event.clientY;\n      }\n\n      delta = mousePos - elSize;\n\n      this.$emit('move', this.index, delta, event);\n    },\n    mouseup(event) {\n      this.isGrabbed = false;\n      this.$emit('end-move', event);\n\n      document.body.removeEventListener('mousemove', this.mousemove);\n      document.body.removeEventListener('mouseup', this.mouseup);\n    },\n    setDragging(event) {\n      this.isDragging = true;\n    },\n    unsetDragging(event) {\n      this.isDragging = false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/TreeItem.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    class=\"c-tree__item-h\"\n    role=\"treeitem\"\n    :style=\"treeItemStyles\"\n    :aria-expanded=\"\n      !activeSearch && hasComposition ? (isOpen || isLoading ? 'true' : 'false') : undefined\n    \"\n  >\n    <div\n      class=\"c-tree__item\"\n      :class=\"{\n        'is-alias': isAlias,\n        'is-navigated-object': shouldHighlight,\n        'is-targeted-item': isTargetedItem,\n        'is-context-clicked': contextClickActive,\n        'is-new': isNewItem\n      }\"\n      @animationend=\"targetedPathAnimationEnd($event)\"\n      @click.capture=\"itemClick\"\n      @contextmenu.capture=\"handleContextMenu\"\n    >\n      <ViewControl\n        ref=\"action\"\n        class=\"c-tree__item__view-control\"\n        :domain-object=\"node.object\"\n        :value=\"isOpen || isLoading\"\n        :enabled=\"!activeSearch && hasComposition\"\n        @input=\"itemAction()\"\n      />\n      <ObjectLabel\n        ref=\"objectLabel\"\n        :domain-object=\"node.object\"\n        :object-path=\"node.objectPath\"\n        :navigate-to-path=\"navigationPath\"\n        @context-click-active=\"setContextClickActive\"\n      />\n      <span v-if=\"isLoading\" class=\"loading\"></span>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ObjectLabel from '../components/ObjectLabel.vue';\nimport ViewControl from '../components/ViewControl.vue';\n\nexport default {\n  name: 'TreeItem',\n  components: {\n    ViewControl,\n    ObjectLabel\n  },\n  inject: ['openmct'],\n  props: {\n    node: {\n      type: Object,\n      required: true\n    },\n    isSelectorTree: {\n      type: Boolean,\n      required: true\n    },\n    targetedPath: {\n      type: String,\n      default: null\n    },\n    selectedItem: {\n      type: Object,\n      required: true\n    },\n    activeSearch: {\n      type: Boolean,\n      default: false\n    },\n    leftOffset: {\n      type: String,\n      default: '0px'\n    },\n    isNew: {\n      type: Boolean,\n      default: false\n    },\n    itemIndex: {\n      type: Number,\n      required: false,\n      default: undefined\n    },\n    itemOffset: {\n      type: Number,\n      required: false,\n      default: undefined\n    },\n    itemHeight: {\n      type: Number,\n      required: false,\n      default: 0\n    },\n    openItems: {\n      type: Array,\n      required: true\n    },\n    loadingItems: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: [\n    'tree-item-mounted',\n    'tree-item-action',\n    'targeted-path-animation-end',\n    'tree-item-selection'\n  ],\n  data() {\n    this.navigationPath = this.node.navigationPath;\n\n    return {\n      hasComposition: false,\n      navigated: this.isNavigated(),\n      contextClickActive: false\n    };\n  },\n  computed: {\n    isAlias() {\n      let parent = this.node.objectPath[1];\n\n      if (!parent) {\n        return false;\n      }\n\n      let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);\n\n      return parentKeyString !== this.node.object.location;\n    },\n    isSelectedItem() {\n      return this.selectedItem.objectPath === this.node.objectPath;\n    },\n    isTargetedItem() {\n      return this.targetedPath === this.navigationPath;\n    },\n    isNewItem() {\n      return this.isNew;\n    },\n    isLoading() {\n      return Boolean(this.loadingItems[this.navigationPath]);\n    },\n    isOpen() {\n      return this.openItems.includes(this.navigationPath);\n    },\n    shouldHighlight() {\n      if (this.isSelectorTree) {\n        return this.isSelectedItem;\n      } else {\n        return this.navigated;\n      }\n    },\n    treeItemStyles() {\n      let itemTop = (this.itemOffset + this.itemIndex) * this.itemHeight + 'px';\n\n      return {\n        top: itemTop,\n        position: 'absolute',\n        'padding-left': this.leftOffset\n      };\n    }\n  },\n  mounted() {\n    this.domainObject = this.node.object;\n\n    if (this.openmct.composition.get(this.domainObject)) {\n      this.hasComposition = true;\n    }\n\n    this.openmct.router.on('change:path', this.highlightIfNavigated);\n\n    this.$emit('tree-item-mounted', this.navigationPath);\n  },\n  unmounted() {\n    this.openmct.router.off('change:path', this.highlightIfNavigated);\n  },\n  methods: {\n    targetedPathAnimationEnd($event) {\n      $event.target.classList.remove('is-targeted-item');\n      this.$emit('targeted-path-animation-end');\n    },\n    itemAction() {\n      this.$emit('tree-item-action', this.isOpen || this.isLoading ? 'close' : 'open');\n    },\n    itemClick(event) {\n      // skip for navigation, let viewControl handle click\n      if (this.$refs.action.$el === event.target) {\n        return;\n      }\n\n      event.stopPropagation();\n\n      if (!this.isSelectorTree) {\n        this.$refs.objectLabel.navigateOrPreview(event);\n      } else {\n        this.$emit('tree-item-selection', this.node);\n      }\n    },\n    handleContextMenu(event) {\n      event.stopPropagation();\n\n      if (this.isSelectorTree) {\n        return;\n      }\n\n      this.$refs.objectLabel.showContextMenu(event);\n    },\n    isNavigated() {\n      return this.navigationPath === this.openmct.router.currentLocation.path;\n    },\n    highlightIfNavigated() {\n      this.navigated = this.isNavigated();\n    },\n    setContextClickActive(active) {\n      this.contextClickActive = active;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/ViewSwitcher.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"views.length > 1\"\n    class=\"l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left\"\n  >\n    <button\n      class=\"c-icon-button c-button--menu\"\n      :class=\"currentView.cssClass\"\n      :title=\"viewSwitcherLabel\"\n      :aria-label=\"viewSwitcherLabel\"\n      @click.prevent.stop=\"showMenu\"\n    >\n      <span class=\"c-icon-button__label\">\n        {{ currentView.name }}\n      </span>\n    </button>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    currentView: {\n      type: Object,\n      required: true\n    },\n    views: {\n      type: Array,\n      required: true\n    }\n  },\n  emits: ['set-view'],\n  computed: {\n    viewSwitcherLabel() {\n      return 'Open the View Switcher Menu';\n    }\n  },\n  methods: {\n    setView(view) {\n      this.$emit('set-view', view);\n    },\n    showMenu() {\n      const elementBoundingClientRect = this.$el.getBoundingClientRect();\n      const x = elementBoundingClientRect.x;\n      const y = elementBoundingClientRect.y + elementBoundingClientRect.height;\n\n      this.openmct.menus.showMenu(x, y, this.views);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/app-logo.scss",
    "content": ".l-shell__app-logo {\n  cursor: pointer;\n  width: 70px;\n  height: 20px;\n  background: url('assets/images/logo-openmct.svg') center no-repeat;\n}\n"
  },
  {
    "path": "src/ui/layout/create-button.scss",
    "content": ".c-create-button,\n.c-create-menu {\n  font-size: 1.1em;\n}\n\n.c-create-button {\n  .c-button__label {\n    text-transform: $createBtnTextTransform;\n  }\n}\n\n.c-create-menu {\n  max-height: 80vh;\n  width: 500px;\n  min-height: 250px;\n  z-index: 70;\n\n  [class*='__icon'] {\n    filter: $colorKeyFilter;\n  }\n\n  [class*='__item-description'] {\n    min-width: 200px;\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/layout.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/******************************* SHELL */\n.l-shell {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  overflow: hidden;\n\n\n  &__drawer {\n    background: $drawerBg;\n    display: flex;\n    flex-direction: column;\n    height: 0;\n    min-height: 0;\n    max-height: 15%;\n    overflow: hidden;\n    @include transition(min-height);\n\n    &.is-expanded {\n      min-height: 100px;\n    }\n  }\n\n  &__pane-tree {\n    width: 40%;\n\n    [class*='collapse-button'] {\n      // For mobile, collapse button becomes menu icon\n      body.mobile & {\n        @include cClickIconButton();\n        color: $colorKey !important;\n        position: absolute;\n        right: -18px;\n        top: $interiorMarginSm;\n        transform: translateX(100%);\n        width: $mobileMenuIconD;\n        z-index: 2;\n\n        &:before {\n          content: $glyph-icon-menu-hamburger;\n        }\n      }\n    }\n  }\n\n  &__pane-tree,\n  &__pane-inspector {\n    .l-pane__contents {\n      display: flex;\n      flex-flow: column nowrap;\n      overflow-x: hidden;\n    }\n  }\n\n  &__pane-tree {\n    .l-pane__contents {\n      > * {\n        flex: 0 0 auto;\n\n        + * {\n          margin-top: $interiorMargin;\n        }\n      }\n    }\n  }\n\n  &__pane-main {\n    .l-pane__header {\n      display: none;\n    }\n\n    .l-pane__contents {\n      .l-shell__main-.l-shell__main-view-browse-bar {\n        position: relative;\n      }\n\n      // Using `position: absolute` due to flex having repaint issues with the Time Conductor, #5247\n      .l-shell__time-conductor,\n      .l-shell__main-container {\n        position: absolute;\n        left: 0;\n        right: 0;\n        height: auto;\n        width: auto;\n      }\n\n      // Using `position: absolute` due to flex having repaint issues with the Time Conductor, #5247\n      .l-shell__main-container {\n        top: $shellMainBrowseBarH + $interiorMarginLg;\n        bottom: $shellTimeConductorH + $interiorMargin;\n\n        &.--has-toolbar {\n          .is-editing & {\n            top: $shellToolBarH + $shellMainBrowseBarH + $interiorMarginLg !important;\n          }\n        }\n\n        > .c-object-view {\n          border: 1px dashed transparent;\n          .is-editing & {\n            border-color: rgba($editUIAreaBaseColor, 0.6);\n          }\n\n          &[s-selected] {\n            .is-editing & {\n              border-color: $editUIAreaBaseColor\n            }\n          }\n        }\n      }\n\n      @include phonePortrait() {\n        .l-shell__main-container {\n          bottom: $shellTimeConductorMobileH + $interiorMargin;\n        }\n      }\n\n      .l-shell__time-conductor {\n        bottom: 0;\n      }\n    }\n  }\n\n  body.mobile & {\n    &__pane-main,\n    &__pane-tree {\n      padding: $interiorMarginLg;\n    }\n\n    &__pane-tree {\n      background: linear-gradient(90deg, transparent 70%, rgba(black, 0.2) 99%, rgba(black, 0.3));\n\n      .l-pane__header {\n        // Hide all buttons except the collapse button\n        > :not(.l-pane__collapse-button) {\n          display: none;\n        }\n      }\n\n      [class*='expand-button'] {\n        display: none;\n      }\n\n      &[class*='--collapsed'] {\n        [class*='collapse-button'] {\n          right: -8px;\n        }\n      }\n    }\n  }\n\n  body.phone.portrait & {\n    &__pane-tree {\n      width: calc(100% - #{$mobileMenuIconD + (2 * nth($shellPanePad, 2))});\n\n      + .l-pane {\n        // Hide pane-main when this pane is expanded\n        opacity: 0;\n        pointer-events: none;\n      }\n\n      &[class*='--collapsed'] + .l-pane {\n        // Show pane-main when tree is collapsed\n        opacity: 1;\n        pointer-events: inherit;\n        transition: opacity 250ms ease 250ms;\n      }\n    }\n  }\n\n  //&__head,\n  &__pane-inspector {\n    body.mobile & {\n      display: none;\n    }\n  }\n\n  &__status {\n    flex: 0 0 auto;\n    display: flex;\n  }\n\n  /******************************* HEAD */\n  &__main-view-browse-bar {\n    flex: 0 0 auto;\n    margin-bottom: $interiorMargin; // Needs some additional visual separation\n  }\n\n  &__head {\n    body.mobile & {\n      .c-create-button,\n      .c-create-menu,\n      .c-indicator,\n      .c-icon-button {\n        // Making status area visible, but hiding some widgets for mobile.\n        display: none;\n      }\n    }\n  }\n\n  body.mobile & .l-shell__main-view-browse-bar {\n    margin-left: $mobileMenuIconD; // Make room for the hamburger!\n    .c-button[class*='__actions__edit'] {\n      display: none; // Hide the main view edit button when in mobile context\n    }\n  }\n\n  &__head {\n    align-items: center;\n    background: $colorHeadBg;\n    display: grid;\n    grid-template-columns: min-content 1fr 3fr repeat(4, min-content);\n    grid-column-gap: $interiorMargin;\n    padding: $interiorMargin $interiorMargin + 2;\n\n    .l-shell__head__button {\n      color: $colorBtnMajorBg;\n      flex: 0 0 auto;\n      font-size: 0.9em;\n    }\n\n    &-section {\n      // Subdivides elements across the head\n      display: flex;\n    }\n\n    &--expanded {\n      .c-indicator__label {\n        transition: none !important;\n      }\n    }\n  }\n\n  &__controls {\n    $brdr: 1px solid $colorInteriorBorder;\n    border-right: $brdr;\n    border-left: $brdr;\n    align-items: start;\n    $p: $interiorMarginSm;\n    padding-left: $p;\n    padding-right: $p;\n  }\n\n  &__create-button,\n  &__app-logo {\n    flex: 0 0 auto;\n  }\n\n  &__create-button {\n    body.mobile & {\n      // Fixes weird gap in mobile status area\n      margin-right: 0px;\n    }\n  }\n\n  &__indicators {\n    // Style as multiline by default\n    display: flex;\n    flex-wrap: wrap;\n    font-size: 11px;\n    min-height: 25px;\n    justify-content: flex-end;\n\n    .l-shell__head--indicators-single-line & {\n      flex-wrap: nowrap;\n      justify-content: flex-start; // Overflow detection doesn't work with flex-end.\n      overflow: hidden;\n\n      > *:first-child {\n        margin-left: auto; // Mimics justify-content: flex-end when in single line mode.\n      }\n    }\n  }\n\n  /******************************* MAIN AREA */\n  &__main-container {\n    // Wrapper for main views\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto !important;\n    height: 100%; // Chrome 73 overflow bug fix\n    overflow: auto;\n\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n\n    > .c-object-view {\n      flex: 1 1 auto;\n      overflow: auto;\n    }\n  }\n\n  &__tree {\n    // Tree component within __pane-tree\n    flex: 1 1 auto !important;\n  }\n\n  &__main {\n    > .l-pane {\n      padding: nth($shellPanePad, 1) 0;\n    }\n  }\n\n  body.desktop & {\n    &__main {\n      // Top and bottom padding in container that holds tree, __pane-main and Inspector\n      padding: nth($shellPanePad, 1) 0;\n      min-height: 0;\n\n      > .l-pane {\n        padding-top: 0;\n        padding-bottom: 0;\n      }\n\n      .l-pane__expand-button__label {\n        // Add a plus icon before the label\n        &:before {\n          font-family: symbolsfont;\n          content: $glyph-icon-plus;\n          display: inline-block;\n        }\n      }\n    }\n\n    &__pane-tree,\n    &__pane-inspector {\n      max-width: 70%;\n    }\n\n    &__pane-tree {\n      width: 300px;\n      padding-left: nth($shellPanePad, 2);\n    }\n\n    &__pane-inspector {\n      width: 200px;\n      padding-right: nth($shellPanePad, 2);\n    }\n  }\n\n  &__toolbar {\n    // Toolbar in the main shell, used by Display Layouts\n    $p: $interiorMargin;\n    background: $editUIBaseColor;\n    border-radius: $basicCr;\n    height: $p + 24px; // Need to standardize the height\n    justify-content: space-between;\n    padding: $p;\n    z-index: 2;\n  }\n\n  &__resizing {\n    iframe {\n      pointer-events: none;\n    }\n  }\n}\n\n.c-object-view {\n  display: block;\n  height: 100%;\n  overflow: auto;\n\n  &.is-stale {\n    @include isStaleHolder();\n  }\n}\n\n/************************** BROWSE BAR */\n.l-browse-bar {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n\n  &__start,\n  &__end,\n  &__actions {\n    display: flex;\n    align-items: center;\n  }\n\n  &__actions,\n  &__end {\n    .c-button {\n      &[class*='icon-']:before {\n        min-width: 1em;\n        text-align: center;\n      }\n    }\n\n    > * + * {\n      margin-left: $interiorMarginSm;\n    }\n  }\n\n  &__start {\n    flex: 1 1 auto;\n    min-width: 0; // Forces interior to compress when pushed on\n\n    [class*='button'] {\n      flex: 0 0 auto;\n    }\n  }\n\n  &__end {\n    flex: 0 0 auto;\n  }\n\n  &__nav-to-parent-button,\n  &__disclosure-button {\n    //flex: 0 0 auto;\n  }\n\n  &__nav-to-parent-button {\n    // This is an icon-button\n    margin-right: $interiorMargin;\n\n    .is-editing & {\n      display: none;\n    }\n  }\n\n  &__object-name--w,\n  &__object-name {\n    flex: 0 1 auto;\n  }\n\n  .c-object-label__type-icon {\n    opacity: $objectLabelTypeIconOpacity;\n  }\n\n  &__object-name--w {\n    @include headerFont(1.5em);\n    min-width: 0;\n\n    .is-status__indicator {\n      right: -5px !important;\n      top: -4px !important;\n    }\n  }\n\n  &__object-details {\n    opacity: 0.5;\n  }\n}\n\n/************************** DRAWER */\n.c-drawer {\n  /* Sliding overlay or push element to contain things\n    * Designed for mobile and compact desktop scenarios\n    * Variations:\n    * --overlays: position absolute, overlays neighboring elements\n    * --push: position relative, pushs/collapses neighboring elements\n    * --align-left, align-top: opens from left or top respectively\n    * &.is-expanded: applied when expanded.\n     */\n\n  $transProps: width, min-width, height, min-height;\n\n  min-height: 0;\n  min-width: 0;\n  overflow: hidden;\n\n  &:not(.is-expanded) {\n    // When collapsed, hide internal elements\n    > * {\n      display: none;\n    }\n  }\n\n  &.c-drawer--align-left {\n    @include transition($prop: $transProps, $dur: $transOutTime);\n    height: 100%;\n  }\n\n  &.c-drawer--align-top {\n    @include transition($prop: $transProps, $dur: $transOutTime);\n  }\n\n  &.c-drawer--overlays {\n    position: absolute;\n    z-index: 3;\n\n    &.is-expanded {\n      // Height and width must be set per usage\n      &.c-drawer--align-left {\n        box-shadow: rgba(black, 0.7) 3px 0 20px;\n      }\n\n      &.c-drawer--align-top {\n        box-shadow: rgba(black, 0.7) 0 3px 20px;\n      }\n    }\n  }\n\n  &.c-drawer--push {\n    position: relative;\n\n    &.is-expanded {\n      // Height and width must be set per usage\n      &.c-drawer--align-left {\n        box-shadow: rgba(black, 0.2) 3px 0 20px;\n        margin-right: $interiorMarginLg;\n      }\n\n      &.c-drawer--align-top {\n        box-shadow: rgba(black, 0.2) 0 3px 20px;\n        margin-bottom: $interiorMarginLg; // Not sure this is desired here\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/mct-tree.scss",
    "content": "@use 'sass:math';\n\n.c-tree-and-search {\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 auto;\n  overflow: auto;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__search {\n    flex: 0 0 auto;\n  }\n\n  &__no-results {\n    font-style: italic;\n    opacity: 0.6;\n  }\n\n  &__tree {\n    flex: 1 1 auto;\n    height: 0; // Chrome 73 overflow bug fix\n  }\n\n  .c-tree {\n    flex: 1 1 auto;\n    overflow: hidden;\n    transition: all;\n\n    .c-tree__item-h {\n      width: 100%;\n    }\n\n    &__scrollable {\n      overflow: auto;\n      padding-right: $interiorMargin;\n    }\n\n    &__item--empty {\n      // Styling for empty tree items\n      // Indent should allow for c-nav view-control width and icon spacing\n      font-style: italic;\n      padding: $interiorMarginSm * 2 1px;\n      opacity: 0.7;\n      pointer-events: none;\n\n      &:before {\n        content: '';\n        display: inline-block;\n        width: $treeNavArrowD + $interiorMarginLg;\n      }\n    }\n  }\n}\n\n.c-tree,\n.c-list {\n  @include userSelectNone();\n  overflow-x: hidden;\n  overflow-y: auto;\n\n  .icon-arrow-nav-to-parent {\n    visibility: hidden;\n\n    &.is-enabled {\n      visibility: visible;\n    }\n  }\n\n  li {\n    position: relative;\n\n    &[class*='__item-h'] {\n      display: block;\n      width: 100%;\n    }\n\n    + li {\n      margin-top: 1px;\n    }\n  }\n\n  &__item {\n    display: flex;\n    align-items: center;\n    cursor: pointer;\n    line-height: 110%;\n    padding: $interiorMarginSm $interiorMargin;\n    transition: background 150ms ease;\n\n    &__type-icon {\n      color: $colorItemTreeIcon;\n    }\n\n    @include hover {\n      background: $colorItemTreeHoverBg;\n    }\n\n    &.is-navigated-object,\n    &.is-selected {\n      background: $colorItemTreeSelectedBg;\n\n      [class*='__name'] {\n        color: $colorItemTreeSelectedFg;\n      }\n    }\n    &.is-targeted-item {\n      $c: $colorBodyFg;\n      @include pulseProp(\n        $animName: flashTarget,\n        $dur: 500ms,\n        $iter: 8,\n        $prop: background,\n        $valStart: rgba($c, 0.4),\n        $valEnd: rgba($c, 0)\n      );\n    }\n\n    &.is-new {\n      animation-name: animTemporaryHighlight;\n      animation-timing-function: ease-out;\n      animation-duration: 3s;\n      animation-iteration-count: 1;\n    }\n\n    &.is-context-clicked {\n      box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;\n    }\n\n    .icon-arrow-nav-to-parent {\n      visibility: hidden;\n\n      &.is-enabled {\n        visibility: visible;\n      }\n    }\n  }\n}\n\n.c-tree {\n  .c-tree {\n    margin-left: 15px;\n  }\n\n  &__item {\n    border-radius: $smallCr;\n\n    [class*='view-control'] {\n      padding: 2px 10px;\n    }\n\n    > * + * {\n      margin-left: ceil(math.div($interiorMarginSm, 2));\n    }\n\n    @include hover {\n      background: $colorItemTreeHoverBg;\n    }\n\n    // Object labels in trees\n    &__label {\n      flex: 1 1 auto;\n    }\n\n    &.is-alias {\n      // Object is an alias to an original.\n      [class*='__type-icon'] {\n        @include isAlias();\n      }\n    }\n\n    body.mobile & {\n      @include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg);\n      height: $mobileTreeItemH;\n      margin-bottom: $interiorMarginSm;\n\n      [class*='view-control'] {\n        width: ceil($mobileTreeItemH * 0.5);\n      }\n    }\n\n    &.is-navigated-object,\n    &.is-selected {\n      background: $colorItemTreeSelectedBg;\n\n      [class*='__label'],\n      [class*='__name'] {\n        color: $colorItemTreeSelectedFg;\n      }\n\n      [class*='__type-icon']:before {\n        color: $colorItemTreeSelectedIcon;\n      }\n    }\n  }\n}\n\n.is-editing .is-navigated-object {\n  a[class*='__item__label'] {\n    [class*='__name'] {\n      font-style: italic;\n    }\n  }\n}\n\n.c-tree {\n  &__item {\n    body.mobile & {\n      @include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg);\n      height: $mobileTreeItemH;\n      margin-bottom: $interiorMarginSm;\n\n      [class*='view-control'] {\n        width: ceil($mobileTreeItemH * 0.5);\n      }\n    }\n  }\n\n  .c-tree {\n    margin-left: $treeItemIndent;\n  }\n}\n\n.c-list {\n  &__item {\n    border-radius: $smallCr;\n\n    &__name {\n      $p: $interiorMarginSm;\n      @include ellipsize();\n      padding-bottom: $p;\n      padding-top: $p;\n    }\n  }\n}\n\n.c-nav {\n  $dimension: $treeNavArrowD;\n\n  &__up,\n  &__down {\n    flex: 0 0 auto;\n    height: $dimension;\n    width: $dimension;\n    visibility: hidden;\n    position: relative;\n    text-align: center;\n\n    &.is-enabled {\n      visibility: visible;\n    }\n\n    &:before {\n      // Nav arrow\n      $dimension: 9px;\n      $width: 3px;\n      border: solid $colorItemTreeVC;\n      border-width: 0 $width $width 0;\n      content: '';\n      display: block;\n      position: absolute;\n      left: 50%;\n      top: 50%;\n      height: $dimension;\n      width: $dimension;\n    }\n\n    @include desktop {\n      &:hover:before {\n        border-color: $colorItemTreeHoverFg;\n      }\n    }\n  }\n\n  &__up:before {\n    transform: translate(-30%, -50%) rotate(135deg);\n  }\n\n  &__down:before {\n    transform: translate(-70%, -50%) rotate(-45deg);\n  }\n}\n\n.c-selector {\n  &.c-tree-and-search {\n    background: rgba($colorFormLines, 0.1);\n    border-radius: $basicCr;\n    padding: 2px;\n    height: 100%;\n    min-height: 150px;\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/pane.scss",
    "content": "@use 'sass:math';\n\n/**************************** BASE - MOBILE AND DESKTOP */\n.l-multipane {\n  display: flex;\n  flex: 1 1 auto;\n  overflow: hidden;\n  \n\n  &--horizontal,\n  > .l-pane {\n    flex-flow: row nowrap;\n  }\n\n  &--vertical,\n  > .l-pane {\n    flex-flow: column nowrap;\n  }\n\n  &--vertical {\n    height: 100%;\n  }\n}\n\n.l-pane {\n  backface-visibility: hidden;\n  display: flex;\n  min-width: 0px;\n  min-height: 0px;\n  opacity: 1;\n  pointer-events: inherit;\n\n  &__handle,\n  &__label {\n    // __handle and __label don't appear in mobile\n    display: none;\n  }\n\n  &__header {\n    display: flex;\n    align-items: center;\n    @include desktop() {\n      margin-bottom: $interiorMargin;\n    }\n  }\n\n  &--reacts {\n    // This is the pane that doesn't hold the handle\n    // It reacts to other panes that are able to resize\n    flex: 1 1 0;\n  }\n\n  &--collapsed {\n    flex-basis: 0px !important;\n    transition: all 350ms ease;\n\n    .l-pane__contents {\n      transition: opacity 150ms ease;\n      opacity: 0;\n      pointer-events: none;\n      overflow: hidden; // Prevents toolbar from extending into Inspector\n\n      > * {\n        min-width: 0 !important;\n        min-height: 0 !important;\n      }\n    }\n  }\n\n  &[class*='--horizontal'] {\n    padding-left: $interiorMargin;\n    padding-right: $interiorMargin;\n    &.l-pane--collapsed {\n      padding-left: 0 !important;\n      padding-right: 0 !important;\n    }\n  }\n\n  &[class*='--vertical'] {\n    padding-top: $interiorMargin;\n    padding-bottom: $interiorMargin;\n    min-height: 30px; // For Recents holder\n\n    &.l-pane--collapsed {\n      padding-top: 0 !important;\n      padding-bottom: 0 !important;\n    }\n  }\n\n  /************************ CONTENTS */\n  &__contents {\n    flex: 1 1 100%;\n    opacity: 1;\n    overflow-x: hidden;\n    overflow-y: auto;\n    pointer-events: inherit;\n    transition: opacity 250ms ease 250ms;\n\n    .l-pane__contents {\n      // Don't pad all nested __contents\n      padding: 0;\n    }\n  }\n\n  /************************************************ DESKTOP STYLES */\n  body.desktop & {\n    &__handle {\n      background-color: $colorSplitterBg;\n      display: block;\n      position: absolute;\n      @include transition(background-color, $transOutTime);\n\n      &:before {\n        // Extended hit area\n        content: '';\n        display: block;\n        position: absolute;\n        z-index: -1;\n      }\n\n      &:hover {\n        background-color: $colorSplitterHover;\n        @include transition(background-color);\n      }\n    }\n\n    &__header {\n      font-size: 11px;\n    }\n\n    &__label {\n      // Name of the pane\n      @include ellipsize();\n      @include userSelectNone();\n      color: $splitterBtnLabelColorFg;\n      display: block;\n      text-transform: uppercase;\n      flex: 1 1 auto;\n    }\n\n    [class*='expand-button'] {\n      display: none; // Hidden by default\n      background-color: $splitterCollapsedBtnColorBg;\n      color: $splitterCollapsedBtnColorFg;\n\n      &:before {\n        // '+' icon\n        font-size: 0.8em;\n        margin-bottom: $interiorMarginSm; // margin-bottom is needed for Tree and Inspector\n        margin-right: $interiorMarginSm; // margin-right and margin-left are needed for Recent Objects\n        margin-left: $interiorMarginSm;\n      }\n\n      &:hover {\n        background-color: $splitterCollapsedBtnColorBgHov;\n        color: $splitterCollapsedBtnColorFgHov;\n        @include transition(background-color);\n      }\n    }\n\n    &--resizing {\n      // User is dragging the handle and resizing a pane\n      @include userSelectNone();\n\n      + .l-pane {\n        @include userSelectNone();\n      }\n\n      .l-pane {\n        &__handle {\n          background-color: $colorSplitterHover;\n        }\n      }\n    }\n\n    &[class*='--collapsed'] {\n      /********************************* STYLES FOR DESKTOP COLLAPSED PANES, ALL ORIENTATIONS */\n      $d: nth($splitterBtnD, 1);\n      flex-basis: $d;\n      min-width: $d;\n      min-height: $d;\n\n      > .l-pane__handle {\n        display: none;\n      }\n\n      [class*='collapse-button'] {\n        display: none;\n      }\n\n      [class*='expand-button'] {\n        display: block;\n      }\n    }\n\n    &[class*='--collapsed'] { // For Recent Objects Button\n      &.collapse-horizontal {\n        [class*='expand-button'] {\n          display: block;\n          position: absolute;\n          top: 0;\n          width: 100%;\n          border-top-right-radius: $controlCr;\n          border-top-left-radius: $controlCr;\n        }\n      }\n      [class*='expand-button'] {\n        position: absolute;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: 0;\n        height: auto;\n        width: 100%;\n        padding: $interiorMarginSm 1px;\n        font-size: 11px;\n\n        [class*='label'] {\n          text-orientation: mixed;\n          text-transform: uppercase;\n          writing-mode: horizontal-tb;\n        }\n      }\n    }\n\n    &[class*='--horizontal'] {\n      > .l-pane__handle {\n        cursor: col-resize;\n        top: 0;\n        bottom: 0;\n        width: $splitterHandleD;\n\n        &:before {\n          // Extended hit area\n          top: 0;\n          right: $splitterHandleHitMargin * -1;\n          bottom: 0;\n          left: $splitterHandleHitMargin * -1;\n        }\n      }\n\n      .l-pane__collapse-button {\n        &:before {\n          content: $glyph-icon-line-horz;\n        }\n      }\n\n      &[class*='--collapsed'] {\n        /************************ COLLAPSED HORIZONTAL SPLITTER, EITHER DIRECTION */\n        [class*='__header'] {\n          display: none;\n        }\n\n        [class*='expand-button'] {\n          position: absolute;\n          top: 0;\n          right: 0;\n          bottom: 0;\n          left: 0;\n          height: auto;\n          width: 100%;\n          padding: $interiorMargin 1px;\n          font-size: 11px;\n\n          [class*='label'] {\n            text-orientation: mixed;\n            text-transform: uppercase;\n            writing-mode: vertical-lr;\n          }\n        }\n      }\n\n      /************************** Horizontal Splitter Before */\n      // Example: Inspector pane\n      &[class*='-before'] {\n        margin-left: nth($shellPanePad, 2);\n        padding-left: nth($shellPanePad, 2);\n\n        > .l-pane__handle {\n          left: 0;\n          transform: translateX(floor(math.div($splitterHandleD, -2))); // Center over the pane edge\n        }\n\n        [class*='expand-button'] {\n          border-top-left-radius: $controlCr;\n          border-bottom-left-radius: $controlCr;\n        }\n      }\n\n      /************************** Horizontal Splitter After */\n      // Example: Tree pane and Recent Objects\n      &[class*='-after'] {\n        margin-right: nth($shellPanePad, 2);\n        padding-right: nth($shellPanePad, 2);\n\n        > .l-pane__handle {\n          right: 0;\n          transform: translateX(floor(math.div($splitterHandleD, 2)));\n        }\n\n        [class*='expand-button'] {\n          border-top-right-radius: $controlCr;\n          border-bottom-right-radius: $controlCr;\n        }\n      }\n    }\n\n    &[class*='--vertical'] {\n      // l-pane--vertical\n\n      > .l-pane__handle {\n        cursor: row-resize;\n        left: 0;\n        right: 0;\n        height: $splitterHandleD;\n\n        &:before {\n          // Extended hit area\n          left: 0;\n          top: $splitterHandleHitMargin * -1;\n          right: 0;\n          bottom: $splitterHandleHitMargin * -1;\n        }\n      }\n\n      /************************** Vertical Splitter Before */\n      // Pane collapses downward. Used by Recent Objects in Tree\n      &[class*='-before'] {\n        $m: $interiorMarginLg;\n        margin-top: $m;\n        padding-top: $m;\n        > .l-pane__handle {\n          top: 0;\n          transform: translateY(floor(math.div($splitterHandleD, -1)));\n        }\n\n        .l-pane__collapse-button:before {\n          content: $glyph-icon-line-horz;\n        }\n\n        &.l-pane--collapsed {\n          > .l-pane__collapse-button {\n            transform: scaleY(-1);\n          }\n        }\n      }\n\n      /************************** Vertical Splitter After */\n      // Pane collapses upward. Not sure we'll ever use this...\n      &[class*='-after'] {\n        > .l-pane__handle {\n          bottom: 0;\n          transform: translateY(floor(math.div($splitterHandleD, 1)));\n        }\n\n        &:not(.l-pane--collapsed) > .l-pane__collapse-button {\n          &:after {\n            transform: scaleY(-1);\n          }\n        }\n      }\n    }\n  } // Ends .body.desktop\n} // Ends .l-pane\n"
  },
  {
    "path": "src/ui/layout/recent-objects.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n.c-recentobjects-listitem {\n  display: flex;\n  padding: $interiorMargin $interiorMarginSm;\n  align-items: flex-start;\n\n  > * + * {\n    margin-left: $interiorMarginSm;\n  }\n\n  + .c-recentobjects-listitem {\n    border-top: 1px solid $colorInteriorBorder;\n  }\n\n  &.is-alias {\n    // Object is an alias to an original.\n    [class~='recent-object-icon'] {\n      @include isAlias();\n      &:after {\n        bottom: 20%;\n      }\n    }\n  }\n\n  &__object-path {\n    padding: 0 $interiorMarginSm;\n  }\n\n  &__target-button {\n    opacity: 0;\n  }\n\n  &__type-icon,\n  &__more-options-button {\n    flex: 0 0 auto;\n  }\n\n  &__type-icon {\n    color: $colorItemTreeIcon;\n    font-size: 1.25em;\n\n    // TEMP: uses object-label component, hide label part\n    .c-object-label__name {\n      display: none;\n    }\n  }\n\n  &__more-options-button {\n    display: none; // TEMP until enabled\n  }\n\n  &__body {\n    flex: 1 1 auto;\n    padding-top: 2px; // Align with type icon\n\n    > * + * {\n      margin-top: $interiorMarginSm;\n    }\n\n    .c-location {\n      font-size: 0.9em;\n      opacity: 0.8;\n\n      &__item {\n        > * + * {\n          background: blue !important;\n        }\n      }\n    }\n  }\n\n  &__tags {\n    display: flex;\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  &__title {\n    border-radius: $basicCr;\n    color: $colorItemTreeFg;\n    cursor: pointer;\n    padding: $interiorMarginSm;\n\n    &:hover {\n      background-color: $colorItemTreeHoverBg;\n    }\n  }\n\n  .c-tag {\n    font-size: 0.9em;\n  }\n}\n\n.c-recentobjects-listitem:hover .c-recentobjects-listitem__target-button {\n  opacity: 100;\n}\n"
  },
  {
    "path": "src/ui/layout/search/AnnotationSearchResult.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-gsearch-result c-gsearch-result--annotation\"\n    aria-label=\"Annotation Search Result\"\n    role=\"listitem\"\n  >\n    <div class=\"c-gsearch-result__type-icon\" :class=\"resultTypeIcon\"></div>\n    <div class=\"c-gsearch-result__body\">\n      <div class=\"c-gsearch-result__title\" @click=\"clickedResult\">\n        {{ getResultName }}\n      </div>\n\n      <ObjectPath :domain-object=\"domainObject\" :read-only=\"false\" :show-object-itself=\"true\" />\n\n      <div class=\"c-gsearch-result__tags\">\n        <div\n          v-for=\"(tag, index) in result.fullTagModels\"\n          :key=\"index\"\n          class=\"c-tag\"\n          :class=\"{ '--is-not-search-match': !isSearchMatched(tag) }\"\n          :style=\"{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }\"\n        >\n          {{ tag.label }}\n        </div>\n      </div>\n    </div>\n    <div class=\"c-gsearch-result__more-options-button\">\n      <button class=\"c-icon-button icon-3-dots\"></button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { Marked } from 'marked';\nimport sanitizeHtml from 'sanitize-html';\n\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport { identifierToString } from '../../../../src/tools/url.js';\nimport ObjectPath from '../../components/ObjectPath.vue';\n\nexport default {\n  name: 'AnnotationSearchResult',\n  components: {\n    ObjectPath\n  },\n  inject: ['openmct'],\n  props: {\n    result: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    }\n  },\n  data() {\n    return {};\n  },\n  computed: {\n    domainObject() {\n      return this.result.targetModels[0];\n    },\n    getResultName() {\n      if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK) {\n        const previewText = this.getNotebookPreviewText(this.result);\n        return previewText;\n      } else if (\n        this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL\n      ) {\n        const previewText = this.getGeospatialPreviewText(this.result);\n        return previewText;\n      } else {\n        return this.result.targetModels[0].name;\n      }\n    },\n    resultTypeIcon() {\n      return this.openmct.types.get(this.result.type).definition.cssClass;\n    },\n    tagBackgroundColor() {\n      return this.result.fullTagModels[0].backgroundColor;\n    },\n    tagForegroundColor() {\n      return this.result.fullTagModels[0].foregroundColor;\n    }\n  },\n  beforeMount() {\n    this.marked = new Marked();\n  },\n  mounted() {\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n    this.fireAnnotationSelection = this.fireAnnotationSelection.bind(this);\n  },\n  unmounted() {\n    this.openmct.selection.off('change', this.fireAnnotationSelection);\n  },\n  methods: {\n    getNotebookEntryTextById(entryIdToFind, notebookModel) {\n      const sections = Object.values(notebookModel);\n      for (const section of sections) {\n        const pages = Object.values(section);\n        for (const entries of pages) {\n          for (const entry of entries) {\n            if (entry.id === entryIdToFind) {\n              return entry.text;\n            }\n          }\n        }\n      }\n      return null;\n    },\n    getNotebookPreviewText(result) {\n      const targetID = Object.keys(this.result.targets)[0];\n      const entryIdToFind = this.result.targets[targetID].entryId;\n      const notebookModel = this.result.targetModels[0].configuration.entries;\n\n      const entryText = this.getNotebookEntryTextById(entryIdToFind, notebookModel);\n      if (entryText === null) {\n        return 'Could not find any matching Notebook entries';\n      }\n      const markDownHtml = this.marked.parse(entryText, {\n        breaks: true\n      });\n      // strip everything\n      const cleanedHtml = sanitizeHtml(markDownHtml, { allowedAttributes: [], allowedTags: [] });\n      // strip to 64 characters\n      let truncatedText = cleanedHtml.substring(0, 64);\n      // add ellipsis if necessary\n      if (truncatedText.length < entryText.length) {\n        truncatedText = `${truncatedText}...`;\n      }\n      return truncatedText;\n    },\n    getGeospatialPreviewText(result) {\n      const targetID = Object.keys(this.result.targets)[0];\n      const { layerName, name } = this.result.targets[targetID];\n\n      return layerName ? `${layerName} - ${name}` : name;\n    },\n    clickedResult(event) {\n      const objectPath = this.domainObject.originalPath;\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        this.preview(objectPath);\n      } else {\n        const resultUrl = identifierToString(this.openmct, objectPath);\n        if (!this.openmct.router.isNavigatedObject(objectPath)) {\n          // if we're not on the correct page, navigate to the object,\n          // then wait for the selection event to fire before issuing a new selection\n          if (this.result.annotationType) {\n            this.openmct.selection.on('change', this.fireAnnotationSelection);\n          }\n\n          this.openmct.router.navigate(resultUrl);\n        } else {\n          // if this is the navigated object, then we are already on the correct page\n          // and just need to issue the selection event\n          this.fireAnnotationSelection();\n        }\n      }\n    },\n    preview(objectPath) {\n      if (this.previewAction.appliesTo(objectPath)) {\n        this.previewAction.invoke(objectPath);\n      }\n    },\n    fireAnnotationSelection() {\n      this.openmct.selection.off('change', this.fireAnnotationSelection);\n      const selection = [\n        {\n          element: this.$el,\n          context: {\n            item: this.result.targetModels[0],\n            type: 'annotation-search-result',\n            targetDetails: this.result.targets,\n            targetDomainObjects: this.result.targetModels,\n            annotations: [this.result],\n            annotationType: this.result.annotationType,\n            onAnnotationChange: () => {}\n          }\n        }\n      ];\n      this.openmct.selection.select(selection, true);\n    },\n    isSearchMatched(tag) {\n      if (this.result.matchingTagKeys) {\n        return this.result.matchingTagKeys.includes(tag.tagID);\n      }\n\n      return false;\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/search/GrandSearch.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div ref=\"GrandSearch\" aria-label=\"OpenMCT Search\" class=\"c-gsearch\" role=\"search\">\n    <SearchResultsDropDown ref=\"searchResultsDropDown\" />\n    <Search\n      ref=\"shell-search\"\n      class=\"c-gsearch__input\"\n      :value=\"searchValue\"\n      @input=\"searchEverything\"\n      @clear=\"searchEverything\"\n      @click=\"showSearchResults\"\n    />\n  </div>\n</template>\n\n<script>\nimport Search from '../../components/SearchComponent.vue';\nimport SearchResultsDropDown from './SearchResultsDropDown.vue';\n\nconst SEARCH_DEBOUNCE_TIME = 200;\n\nexport default {\n  name: 'GrandSearch',\n  components: {\n    Search,\n    SearchResultsDropDown\n  },\n  inject: ['openmct'],\n  props: {},\n  data() {\n    return {\n      searchValue: '',\n      debouncedSearchTimeoutID: null,\n      searchLoading: false,\n      annotationSearchResults: [],\n      objectSearchResults: []\n    };\n  },\n  mounted() {\n    this.getSearchResults = this.debounceAsyncFunction(this.getSearchResults, SEARCH_DEBOUNCE_TIME);\n  },\n  unmounted() {\n    document.body.removeEventListener('click', this.handleOutsideClick);\n  },\n  methods: {\n    async searchEverything(value) {\n      // if an abort controller exists, regardless of the value passed in,\n      // there is an active search that should be canceled\n      if (this.abortSearchController) {\n        this.abortSearchController.abort();\n        delete this.abortSearchController;\n      }\n\n      this.searchValue = value;\n      // clear any previous search results\n      this.annotationSearchResults = [];\n      this.objectSearchResults = [];\n\n      if (this.searchValue) {\n        await this.getSearchResults();\n      } else {\n        clearTimeout(this.debouncedSearchTimeoutID);\n        const dropdownOptions = {\n          searchLoading: this.searchLoading,\n          searchValue: this.searchValue,\n          annotationSearchResults: this.annotationSearchResults,\n          objectSearchResults: this.objectSearchResults\n        };\n        this.$refs.searchResultsDropDown.showResults(dropdownOptions);\n      }\n    },\n    debounceAsyncFunction(functionToDebounce, debounceTime) {\n      return (...args) => {\n        clearTimeout(this.debouncedSearchTimeoutID);\n\n        return new Promise((resolve, reject) => {\n          this.debouncedSearchTimeoutID = setTimeout(() => {\n            functionToDebounce(...args)\n              .then(resolve)\n              .catch(reject);\n          }, debounceTime);\n        });\n      };\n    },\n    getPathsForObjects(objectsNeedingPaths, abortSignal) {\n      return Promise.all(\n        objectsNeedingPaths.map(async (domainObject) => {\n          if (!domainObject) {\n            // user interrupted search, return back\n            return null;\n          }\n\n          const originalPathObjects = await this.openmct.objects.getOriginalPath(\n            domainObject,\n            [],\n            abortSignal\n          );\n\n          return {\n            objectPath: originalPathObjects,\n            ...domainObject\n          };\n        })\n      );\n    },\n    async getSearchResults() {\n      // an abort controller will be passed in that will be used\n      // to cancel an active searches if necessary\n      this.searchLoading = true;\n      this.$refs.searchResultsDropDown.showSearchStarted();\n      this.abortSearchController = new AbortController();\n\n      try {\n        const searchObjectsPromise = this.searchObjects(this.abortSearchController.signal);\n        const searchAnnotationsPromise = this.searchAnnotations(this.abortSearchController.signal);\n\n        // Wait for all promises, but they process their results as they complete\n        await Promise.allSettled([searchObjectsPromise, searchAnnotationsPromise]);\n\n        this.searchLoading = false;\n        this.showSearchResults();\n      } catch (error) {\n        this.searchLoading = false;\n\n        // Is this coming from the AbortController?\n        // If so, we can swallow the error. If not, 🤮 it to console\n        if (error.name !== 'AbortError') {\n          console.error(`😞 Error searching`, error);\n        }\n      } finally {\n        if (this.abortSearchController) {\n          delete this.abortSearchController;\n        }\n      }\n    },\n    async searchObjects(abortSignal) {\n      const objectSearchPromises = this.openmct.objects.search(this.searchValue, abortSignal);\n      for await (const objectSearchResult of objectSearchPromises) {\n        const objectsWithPaths = await this.getPathsForObjects(objectSearchResult, abortSignal);\n        this.objectSearchResults.push(\n          ...objectsWithPaths.filter((result) => {\n            // Check if the result is NOT an annotation and has a reachable path\n            return (\n              !this.openmct.annotation.isAnnotation(result) &&\n              this.openmct.objects.isReachable(result?.objectPath)\n            );\n          })\n        );\n        // Display the available results so far for objects\n        this.showSearchResults();\n      }\n    },\n    async searchAnnotations(abortSignal) {\n      const annotationSearchResults = await this.openmct.annotation.searchForTags(\n        this.searchValue,\n        abortSignal\n      );\n      this.annotationSearchResults = annotationSearchResults;\n      // Display the available results so far for annotations\n      this.showSearchResults();\n    },\n    showSearchResults() {\n      const dropdownOptions = {\n        searchLoading: this.searchLoading,\n        searchValue: this.searchValue,\n        annotationSearchResults: this.annotationSearchResults,\n        objectSearchResults: this.objectSearchResults\n      };\n      this.$refs.searchResultsDropDown.showResults(dropdownOptions);\n      document.body.addEventListener('click', this.handleOutsideClick);\n    },\n    handleOutsideClick(event) {\n      // if click event is detected outside the dropdown while the\n      // dropdown is visible, this will collapse the dropdown.\n      if (this.$refs.GrandSearch) {\n        const clickedInsideDropdown = this.$refs.GrandSearch.contains(event.target);\n        const clickedPreviewClose =\n          event.target.parentElement &&\n          event.target.parentElement.querySelector('.js-preview-window');\n        const searchResultsDropDown = this.$refs.searchResultsDropDown._.data;\n        if (\n          !clickedInsideDropdown &&\n          searchResultsDropDown.resultsShown &&\n          !searchResultsDropDown.previewVisible &&\n          !clickedPreviewClose\n        ) {\n          searchResultsDropDown.resultsShown = false;\n          document.body.removeEventListener('click', this.handleOutsideClick);\n        }\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/search/GrandSearchSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport mount from 'utils/mount';\nimport { createOpenMct, resetApplicationState } from 'utils/testing';\nimport { nextTick } from 'vue';\n\nimport ExampleTagsPlugin from '../../../../example/exampleTags/plugin.js';\nimport DisplayLayoutPlugin from '../../../plugins/displayLayout/plugin.js';\nimport GrandSearch from './GrandSearch.vue';\n\ndescribe('GrandSearch', () => {\n  let openmct;\n  let grandSearchComponent;\n  let viewContainer;\n  let parent;\n  let sharedWorkerToRestore;\n  let mockDomainObject;\n  let mockAnnotationObject;\n  let mockDisplayLayout;\n  let mockFolderObject;\n  let mockAnotherFolderObject;\n  let mockTopObject;\n  let originalRouterPath;\n  let mockNewObject;\n  let mockObjectProvider;\n  let _destroy;\n\n  beforeEach((done) => {\n    openmct = createOpenMct();\n    originalRouterPath = openmct.router.path;\n    openmct.router.path = [mockDisplayLayout];\n    openmct.editor.edit();\n\n    openmct.install(new ExampleTagsPlugin());\n    openmct.install(new DisplayLayoutPlugin());\n    const availableTags = openmct.annotation.getAvailableTags();\n    mockDomainObject = {\n      type: 'notebook',\n      name: 'fooRabbitNotebook',\n      location: 'fooNameSpace:topObject',\n      identifier: {\n        key: 'some-object',\n        namespace: 'fooNameSpace'\n      },\n      configuration: {\n        entries: {\n          someSection: {\n            somePage: [\n              {\n                id: 'fooBarEntry',\n                text: 'Foo Bar Text'\n              }\n            ]\n          }\n        }\n      }\n    };\n    mockTopObject = {\n      type: 'root',\n      name: 'Top Folder',\n      composition: [],\n      identifier: {\n        key: 'topObject',\n        namespace: 'fooNameSpace'\n      }\n    };\n    mockAnotherFolderObject = {\n      type: 'folder',\n      name: 'Another Test Folder',\n      composition: [],\n      location: 'fooNameSpace:topObject',\n      identifier: {\n        key: 'someParent',\n        namespace: 'fooNameSpace'\n      }\n    };\n    mockFolderObject = {\n      type: 'folder',\n      name: 'Test Folder',\n      composition: [],\n      location: 'fooNameSpace:someParent',\n      identifier: {\n        key: 'someFolder',\n        namespace: 'fooNameSpace'\n      }\n    };\n    mockDisplayLayout = {\n      type: 'layout',\n      name: 'Bar Layout',\n      composition: [],\n      identifier: {\n        key: 'some-layout',\n        namespace: 'fooNameSpace'\n      },\n      configuration: {\n        items: [],\n        layoutGrid: [10, 10]\n      }\n    };\n    mockAnnotationObject = {\n      type: 'annotation',\n      name: 'Some Notebook Annotation',\n      annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,\n      tags: [availableTags[0].id, availableTags[1].id],\n      identifier: {\n        key: 'anAnnotationKey',\n        namespace: 'fooNameSpace'\n      },\n      targets: [\n        {\n          keyString: 'fooNameSpace:some-object',\n          entryId: 'fooBarEntry'\n        }\n      ]\n    };\n    mockNewObject = {\n      type: 'folder',\n      name: 'New Apple Test Folder',\n      composition: [],\n      location: 'fooNameSpace:topObject',\n      identifier: {\n        key: 'newApple',\n        namespace: 'fooNameSpace'\n      }\n    };\n\n    openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);\n    mockObjectProvider = jasmine.createSpyObj('mock object provider', [\n      'create',\n      'update',\n      'get',\n      'supportsSearchType',\n      'search'\n    ]);\n    // eslint-disable-next-line require-await\n    mockObjectProvider.get = async (identifier) => {\n      if (identifier.key === mockDomainObject.identifier.key) {\n        return mockDomainObject;\n      } else if (identifier.key === mockAnnotationObject.identifier.key) {\n        return mockAnnotationObject;\n      } else if (identifier.key === mockDisplayLayout.identifier.key) {\n        return mockDisplayLayout;\n      } else if (identifier.key === mockFolderObject.identifier.key) {\n        return mockFolderObject;\n      } else if (identifier.key === mockAnotherFolderObject.identifier.key) {\n        return mockAnotherFolderObject;\n      } else if (identifier.key === mockTopObject.identifier.key) {\n        return mockTopObject;\n      } else if (identifier.key === mockNewObject.identifier.key) {\n        return mockNewObject;\n      } else {\n        return null;\n      }\n    };\n\n    mockObjectProvider.create.and.returnValue(Promise.resolve(true));\n    mockObjectProvider.update.and.returnValue(Promise.resolve(true));\n\n    openmct.objects.addProvider('fooNameSpace', mockObjectProvider);\n\n    const mockViewProvider = jasmine.createSpyObj('mock view provider', ['key', 'view', 'canView']);\n\n    openmct.objectViews.addProvider(mockViewProvider);\n\n    openmct.on('start', async () => {\n      // use local worker\n      sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;\n      openmct.objects.inMemorySearchProvider.worker = null;\n      await openmct.objects.inMemorySearchProvider.index(mockTopObject);\n      await openmct.objects.inMemorySearchProvider.index(mockDomainObject);\n      await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);\n      await openmct.objects.inMemorySearchProvider.index(mockFolderObject);\n      await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);\n      parent = document.createElement('div');\n      document.body.appendChild(parent);\n      viewContainer = document.createElement('div');\n      parent.append(viewContainer);\n      const { vNode, destroy } = mount(\n        {\n          components: {\n            GrandSearch\n          },\n          provide: {\n            openmct\n          },\n          template: '<GrandSearch ref=\"root\"/>'\n        },\n        {\n          element: viewContainer\n        }\n      );\n      grandSearchComponent = vNode.componentInstance;\n      _destroy = destroy;\n      await nextTick();\n      done();\n    });\n    openmct.startHeadless();\n  });\n\n  afterEach(() => {\n    openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;\n    openmct.router.path = originalRouterPath;\n    _destroy();\n\n    return resetApplicationState(openmct);\n  });\n\n  it('should render an object search result', async () => {\n    await grandSearchComponent.$refs.root.searchEverything('foo');\n    await nextTick();\n    const searchResults = document.querySelectorAll(\n      '[aria-label=\"fooRabbitNotebook notebook result\"]'\n    );\n    expect(searchResults.length).toBe(1);\n    expect(searchResults[0].innerText).toContain('Rabbit');\n  });\n\n  it('should render an object search result if new object added', async () => {\n    delete mockObjectProvider.supportsSearchType;\n    delete mockObjectProvider.search;\n    const composition = openmct.composition.get(mockFolderObject);\n    composition.add(mockNewObject);\n    // after adding, need to wait a beat for the folder to be indexed\n    await nextTick();\n    await grandSearchComponent.$refs.root.searchEverything('apple');\n    await nextTick();\n    const searchResults = document.querySelectorAll(\n      '[aria-label=\"New Apple Test Folder folder result\"]'\n    );\n    expect(searchResults.length).toBe(1);\n    expect(searchResults[0].innerText).toContain('Apple');\n  });\n\n  it('should not use InMemorySearch provider if object provider provides search', async () => {\n    // eslint-disable-next-line require-await\n    mockObjectProvider.search.and.callFake((query, abortSignal, searchType) => {\n      if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {\n        return [mockNewObject];\n      } else {\n        return [];\n      }\n    });\n\n    mockObjectProvider.supportsSearchType.and.callFake((someType) => {\n      return true;\n    });\n\n    const composition = openmct.composition.get(mockFolderObject);\n    composition.add(mockNewObject);\n    await grandSearchComponent.$refs.root.searchEverything('apple');\n    await nextTick();\n    const searchResults = document.querySelectorAll(\n      '[aria-label=\"New Apple Test Folder folder result\"]'\n    );\n    // This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well\n    expect(searchResults.length).toBe(1);\n    expect(searchResults[0].innerText).toContain('Apple');\n  });\n\n  it('should render an annotation search result', async () => {\n    await grandSearchComponent.$refs.root.searchEverything('S');\n    await nextTick();\n    const annotationResults = document.querySelectorAll('[aria-label=\"Annotation Search Result\"]');\n    expect(annotationResults.length).toBe(1);\n    expect(annotationResults[0].innerText).toContain('Driving');\n  });\n\n  it('should render no annotation search results if no match', async () => {\n    await grandSearchComponent.$refs.root.searchEverything('Qbert');\n    await nextTick();\n    const annotationResults = document.querySelectorAll('[aria-label=\"Annotation Search Result\"]');\n    expect(annotationResults.length).toBe(0);\n  });\n\n  it('should preview object search results in edit mode if object clicked', async () => {\n    await grandSearchComponent.$refs.root.searchEverything('Folder');\n    grandSearchComponent.$refs.root.openmct.router.path = [mockDisplayLayout];\n    await nextTick();\n    const folderResult = document.querySelector('[name=\"Test Folder\"]');\n    expect(folderResult).not.toBeNull();\n    folderResult.click();\n    const previewWindow = document.querySelector('.js-preview-window');\n    expect(previewWindow.innerText).toContain('Snapshot');\n  });\n\n  it('should preview annotation search results in edit mode if annotation clicked', async () => {\n    await grandSearchComponent.$refs.root.searchEverything('Dri');\n    grandSearchComponent.$refs.root.openmct.router.path = [mockDisplayLayout];\n    await nextTick();\n    const annotationResults = document.querySelectorAll('[aria-label=\"Annotation Search Result\"]');\n    expect(annotationResults.length).toBe(1);\n    expect(annotationResults[0].innerText).toContain('Driving');\n    annotationResults[0].click();\n    const previewWindow = document.querySelector('.js-preview-window');\n    expect(previewWindow.innerText).toContain('Snapshot');\n  });\n});\n"
  },
  {
    "path": "src/ui/layout/search/ObjectSearchResult.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    class=\"c-gsearch-result c-gsearch-result--object\"\n    aria-label=\"Object Search Result\"\n    role=\"listitem\"\n  >\n    <div class=\"c-gsearch-result__type-icon\" :class=\"resultTypeIcon\"></div>\n    <div\n      class=\"c-gsearch-result__body\"\n      role=\"option\"\n      :aria-label=\"`${resultName} ${resultType} result`\"\n    >\n      <div\n        ref=\"resultName\"\n        class=\"c-gsearch-result__title\"\n        :name=\"resultName\"\n        draggable=\"true\"\n        @dragstart=\"dragStart\"\n        @click=\"clickedResult\"\n        @mouseover.ctrl=\"showToolTip\"\n        @mouseleave=\"hideToolTip\"\n      >\n        {{ resultName }}\n      </div>\n\n      <ObjectPath :read-only=\"false\" :domain-object=\"result\" />\n    </div>\n    <div class=\"c-gsearch-result__more-options-button\">\n      <button class=\"c-icon-button icon-3-dots\"></button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';\nimport { objectPathToUrl } from '../../../tools/url.js';\nimport ObjectPath from '../../components/ObjectPath.vue';\n\nexport default {\n  name: 'ObjectSearchResult',\n  components: {\n    ObjectPath\n  },\n  mixins: [tooltipHelpers],\n  inject: ['openmct'],\n  props: {\n    result: {\n      type: Object,\n      required: true,\n      default() {\n        return {};\n      }\n    }\n  },\n  emits: ['preview-changed'],\n  computed: {\n    resultName() {\n      return this.result.name;\n    },\n    resultTypeIcon() {\n      return this.openmct.types.get(this.result.type).definition.cssClass;\n    },\n    resultType() {\n      return this.result.type;\n    }\n  },\n  mounted() {\n    this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);\n    this.previewAction.on('isVisible', this.togglePreviewState);\n  },\n  unmounted() {\n    this.previewAction.off('isVisible', this.togglePreviewState);\n  },\n  methods: {\n    clickedResult(event) {\n      const { objectPath } = this.result;\n      if (this.openmct.editor.isEditing()) {\n        event.preventDefault();\n        this.preview(objectPath);\n      } else {\n        let resultUrl = objectPathToUrl(this.openmct, objectPath);\n\n        // Remove the vestigial 'ROOT' identifier from url if it exists\n        if (resultUrl.includes('/ROOT')) {\n          resultUrl = resultUrl.split('/ROOT').join('');\n        }\n\n        this.openmct.router.navigate(resultUrl);\n      }\n    },\n    togglePreviewState(previewState) {\n      this.$emit('preview-changed', previewState);\n    },\n    preview(objectPath) {\n      if (this.previewAction.appliesTo(objectPath)) {\n        this.previewAction.invoke(objectPath);\n      }\n    },\n    dragStart(event) {\n      const navigatedObject = this.openmct.router.path[0];\n      const { objectPath } = this.result;\n      const serializedPath = JSON.stringify(objectPath);\n      const keyString = this.openmct.objects.makeKeyString(this.result.identifier);\n      if (this.openmct.composition.checkPolicy(navigatedObject, this.result)) {\n        event.dataTransfer.setData('openmct/composable-domain-object', JSON.stringify(this.result));\n      }\n\n      event.dataTransfer.setData('openmct/domain-object-path', serializedPath);\n      event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.result);\n    },\n    async showToolTip() {\n      const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;\n      this.buildToolTip(await this.getObjectPath(this.result.identifier), BELOW, 'resultName');\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/search/SearchResultsDropDown.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n\n<template>\n  <div\n    v-show=\"resultsShown\"\n    role=\"dialog\"\n    aria-label=\"Search Results Dropdown\"\n    class=\"c-gsearch__dropdown\"\n  >\n    <button\n      aria-label=\"Close\"\n      class=\"c-gsearch__results-close c-click-icon c-overlay__close-button icon-x\"\n      @click=\"selectedResult\"\n    ></button>\n    <div class=\"c-gsearch__results\" :class=\"{ 'search-finished': !searchLoading }\">\n      <div\n        v-if=\"objectResults?.length\"\n        ref=\"objectResults\"\n        class=\"c-gsearch__results-section\"\n        role=\"list\"\n        aria-label=\"Object Results\"\n      >\n        <div class=\"c-gsearch__results-section-title\">Object Results</div>\n        <ObjectSearchResult\n          v-for=\"objectResult in objectResults\"\n          :key=\"openmct.objects.makeKeyString(objectResult.identifier)\"\n          :result=\"objectResult\"\n          @preview-changed=\"previewChanged\"\n          @click=\"selectedResult\"\n        />\n      </div>\n      <div\n        v-if=\"annotationResults?.length\"\n        ref=\"annotationResults\"\n        role=\"list\"\n        aria-label=\"Annotation Results\"\n      >\n        <div class=\"c-gsearch__results-section-title\">Annotation Results</div>\n        <AnnotationSearchResult\n          v-for=\"annotationResult in annotationResults\"\n          :key=\"makeKeyForAnnotationResult(annotationResult)\"\n          :result=\"annotationResult\"\n          @click=\"selectedResult\"\n        />\n      </div>\n      <div v-if=\"searchLoading\" class=\"c-gsearch__result-pane-msg\">\n        <div class=\"hint\">Searching...</div>\n        <ProgressBar :model=\"{ progressPerc: null }\" />\n      </div>\n      <div\n        v-if=\"\n          !searchLoading &&\n          (!annotationResults || !annotationResults.length) &&\n          (!objectResults || !objectResults.length)\n        \"\n        class=\"c-gsearch__result-pane-msg\"\n      >\n        <div class=\"hint\">No results found</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ProgressBar from '@/ui/components/ProgressBar.vue';\n\nimport AnnotationSearchResult from './AnnotationSearchResult.vue';\nimport ObjectSearchResult from './ObjectSearchResult.vue';\n\nexport default {\n  name: 'SearchResultsDropDown',\n  components: {\n    AnnotationSearchResult,\n    ObjectSearchResult,\n    ProgressBar\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      resultsShown: false,\n      searchLoading: false,\n      annotationResults: [],\n      objectResults: [],\n      previewVisible: false\n    };\n  },\n  methods: {\n    selectedResult() {\n      if (!this.previewVisible) {\n        this.resultsShown = false;\n      }\n    },\n    makeKeyForAnnotationResult(annotationResult) {\n      const annotationKeyString = this.openmct.objects.makeKeyString(annotationResult.identifier);\n      const firstTargetKeyString = Object.keys(annotationResult.targets)[0];\n\n      return `${annotationKeyString}-${firstTargetKeyString}`;\n    },\n    previewChanged(changedPreviewState) {\n      this.previewVisible = changedPreviewState;\n    },\n    showSearchStarted() {\n      this.searchLoading = true;\n      this.resultsShown = true;\n      this.annotationResults = [];\n      this.objectResults = [];\n    },\n    showResults({ searchLoading, searchValue, annotationSearchResults, objectSearchResults }) {\n      this.searchLoading = searchLoading;\n      this.annotationResults = annotationSearchResults;\n      this.objectResults = objectSearchResults;\n      if (searchValue?.length) {\n        this.resultsShown = true;\n      } else {\n        this.resultsShown = false;\n      }\n    }\n  },\n  template: 'Dropdown'\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/search/search.scss",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/******************************* EXPANDED SEARCH 2022 */\n.c-gsearch {\n  .l-shell__head & {\n    // Search input in the shell head\n    .c-search {\n      background: rgba($colorHeadFg, 0.2);\n      box-shadow: none;\n      flex: 1 1 auto;\n      body.mobile & {\n        @include phonePortrait() {\n          // This logic of expanding the search input upon click only happens in mobile portrait mode\n          background: $colorHeadBg;\n          width: 15%;\n          &:hover {\n            // When clicked, expand the search bar\n            background-color: rgba($colorHeadFg, 0.2);\n            width: 50vw;\n            transition: width 120ms;\n          }\n        }\n      }\n    }\n  }\n\n  &__dropdown {\n    @include menuOuter();\n    display: flex;\n    flex-direction: column;\n    padding: $interiorMarginLg;\n    min-width: 500px;\n    max-height: 500px;\n    top: $formInputH;\n    z-index: 60;\n    body.mobile & {\n      // Makes search in mobile look less like an overlay, and more fullscreen.\n      position: absolute;\n      top: 25px;\n      left: -12px;\n      height: calc(100vh - 22px);\n      min-width: 100vw;\n      max-height: none;\n      border-radius: 0px;\n      box-shadow: none;\n    }\n  }\n\n  &__results,\n  &__results-section {\n    flex: 1 1 auto;\n  }\n\n  &__results {\n    // Holds n __results-sections\n    padding-right: $interiorMargin; // Fend off scrollbar\n    overflow-y: auto;\n\n    > * + * {\n      margin-top: $interiorMarginLg;\n    }\n    body.mobile & {\n      // Add a margin to results so we have room for the close button\n      margin-right: 20px;\n    }\n  }\n\n  &__results-section {\n    > * + * {\n      margin-top: $interiorMarginSm;\n    }\n  }\n\n  &__results-section-title {\n    @include propertiesHeader();\n  }\n\n  &__result-pane-msg {\n    > * + * {\n      margin-top: $interiorMargin;\n    }\n  }\n\n  &__results-close {\n    // Close button that appears for mobile only\n    display: none;\n    body.mobile & {\n      display: block;\n    }\n  }\n\n  body.mobile & {\n    width: 50vw;\n    @include phonePortrait() {\n      // This logic only appears for a mobile portrait mode\n      width: 100%;\n    }\n  }\n}\n\n.c-gsearch-result {\n  display: flex;\n  padding: $interiorMarginSm 0;\n\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n\n  + .c-gsearch-result {\n    border-top: 1px solid $colorInteriorBorder;\n  }\n\n  &__type-icon,\n  &__more-options-button {\n    flex: 0 0 auto;\n  }\n\n  &__type-icon {\n    color: $colorItemTreeIcon;\n    font-size: 1.5em;\n\n    // TEMP: uses object-label component, hide label part\n    .c-object-label__name {\n      display: none;\n    }\n  }\n\n  &__more-options-button {\n    display: none; // TEMP until enabled\n  }\n\n  &__body {\n    flex: 1 1 auto;\n\n    > * + * {\n      margin-top: $interiorMarginSm;\n    }\n\n    .c-location {\n      color: $colorBodyFg;\n      font-size: 0.9em;\n      opacity: 0.8;\n    }\n  }\n\n  &__tags {\n    display: flex;\n\n    > * + * {\n      margin-left: $interiorMargin;\n    }\n  }\n\n  &__title {\n    border-radius: $basicCr;\n    color: pullForward($colorBodyFg, 30%);\n    cursor: pointer;\n    font-size: 1.15em;\n    padding: 3px $interiorMarginSm;\n\n    &:hover {\n      background-color: $colorItemTreeHoverBg;\n    }\n  }\n\n  .c-tag {\n    font-size: 0.9em;\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/status-bar/NotificationBanner.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    v-if=\"activeModel.message\"\n    class=\"c-message-banner\"\n    role=\"alert\"\n    :aria-live=\"activeModel.severity === 'error' ? 'assertive' : 'polite'\"\n    :class=\"[\n      activeModel.severity,\n      {\n        minimized: activeModel.minimized,\n        new: !activeModel.minimized\n      }\n    ]\"\n    @click=\"maximize()\"\n  >\n    <span class=\"c-message-banner__message\">{{ activeModel.message }}</span>\n    <span\n      v-if=\"haslink\"\n      class=\"c-message-banner__message\"\n      :class=\"[haslink ? getLinkProps.cssClass : '']\"\n      >{{ getLinkProps.text }}</span\n    >\n\n    <ProgressBar\n      v-if=\"activeModel.progressPerc\"\n      class=\"c-message-banner__progress-bar\"\n      :model=\"activeModel\"\n    />\n    <button\n      class=\"c-message-banner__close-button c-click-icon icon-x-in-circle\"\n      aria-label=\"Dismiss\"\n      @click.stop=\"dismiss()\"\n    ></button>\n  </div>\n</template>\n\n<script>\nimport ProgressBar from '../../components/ProgressBar.vue';\n\nlet activeNotification = undefined;\nlet maximizedDialog = undefined;\nlet minimizeButton = {\n  label: 'Dismiss',\n  callback: dismissMaximizedDialog\n};\n\nfunction dismissMaximizedDialog() {\n  if (maximizedDialog) {\n    maximizedDialog.dismiss();\n    maximizedDialog = undefined;\n  }\n}\n\nfunction updateMaxProgressBar(progressPerc, progressText) {\n  if (maximizedDialog) {\n    maximizedDialog.updateProgress(progressPerc, progressText);\n\n    if (progressPerc >= 100) {\n      dismissMaximizedDialog();\n    }\n  }\n}\n\nexport default {\n  components: {\n    ProgressBar: ProgressBar\n  },\n  inject: ['openmct'],\n  data() {\n    return {\n      activeModel: {\n        message: undefined,\n        progressPerc: null,\n        progressText: undefined,\n        minimized: undefined,\n        options: undefined\n      }\n    };\n  },\n  computed: {\n    haslink() {\n      const options = this.activeModel.options;\n\n      return options && options.link;\n    },\n    getLinkProps() {\n      return this.activeModel.options.link;\n    },\n    progressWidth() {\n      return {\n        width: this.activeModel.progress + '%'\n      };\n    }\n  },\n  mounted() {\n    this.openmct.notifications.on('notification', this.showNotification);\n    this.openmct.notifications.on('dismiss-all', this.clearModel);\n    if (this.openmct.notifications.activeNotification) {\n      activeNotification = this.openmct.notifications.activeNotification;\n      this.showNotification(activeNotification);\n    }\n  },\n  methods: {\n    showNotification(notification) {\n      if (activeNotification) {\n        activeNotification.off('progress', this.updateProgress);\n        activeNotification.off('minimized', this.minimized);\n        activeNotification.off('destroy', this.destroyActiveNotification);\n      }\n\n      activeNotification = notification;\n      this.clearModel();\n      this.applyModel(notification.model);\n\n      activeNotification.once('destroy', this.destroyActiveNotification);\n      activeNotification.on('progress', this.updateProgress);\n      activeNotification.on('minimized', this.minimized);\n    },\n    isEqual(modelA, modelB) {\n      return modelA.message === modelB.message && modelA.timestamp === modelB.timestamp;\n    },\n    applyModel(model) {\n      Object.keys(model).forEach((key) => (this.activeModel[key] = model[key]));\n    },\n    clearModel() {\n      Object.keys(this.activeModel).forEach((key) => (this.activeModel[key] = undefined));\n    },\n    updateProgress(progressPerc, progressText) {\n      this.activeModel.progressPerc = progressPerc;\n      this.activeModel.progressText = progressText;\n    },\n    destroyActiveNotification() {\n      this.clearModel();\n      activeNotification.off('destroy', this.destroyActiveNotification);\n      activeNotification = undefined;\n    },\n    dismiss() {\n      if (activeNotification.model.severity === 'info') {\n        activeNotification.dismiss();\n      } else {\n        this.openmct.notifications._minimize(activeNotification);\n      }\n    },\n    minimized() {\n      this.activeModel.minimized = true;\n      activeNotification.off('progress', this.updateProgress);\n      activeNotification.off('minimized', this.minimized);\n\n      activeNotification.off('progress', updateMaxProgressBar);\n      activeNotification.off('minimized', dismissMaximizedDialog);\n      activeNotification.off('destroy', dismissMaximizedDialog);\n    },\n    maximize() {\n      if (this.haslink) {\n        const linkProps = this.getLinkProps;\n        linkProps.onClick();\n\n        activeNotification.dismiss();\n\n        return;\n      }\n\n      if (this.activeModel.progressPerc) {\n        maximizedDialog = this.openmct.overlays.progressDialog({\n          buttons: [minimizeButton],\n          ...this.activeModel\n        });\n\n        activeNotification.on('progress', updateMaxProgressBar);\n        activeNotification.on('minimized', dismissMaximizedDialog);\n        activeNotification.on('destroy', dismissMaximizedDialog);\n      } else {\n        maximizedDialog = this.openmct.overlays.dialog({\n          iconClass: this.activeModel.severity,\n          buttons: [minimizeButton],\n          ...this.activeModel\n        });\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/status-bar/StatusIndicators.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div\n    ref=\"indicatorsContainer\"\n    aria-label=\"Status Indicators\"\n    class=\"l-shell__head-section l-shell__indicators\"\n  >\n    <component\n      :is=\"indicator.value.vueComponent\"\n      v-for=\"indicator in sortedIndicators\"\n      :key=\"indicator.value.key\"\n      role=\"status\"\n    />\n  </div>\n</template>\n\n<script>\nimport { defineExpose, ref, shallowRef } from 'vue';\n\nexport default {\n  inject: ['openmct'],\n  setup() {\n    const indicatorsContainer = ref(null);\n\n    defineExpose({ indicatorsContainer });\n  },\n  data() {\n    return {\n      indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef)\n    };\n  },\n  computed: {\n    sortedIndicators() {\n      if (this.indicators.length === 0) {\n        return [];\n      }\n\n      return [...this.indicators].sort((a, b) => b.value.priority - a.value.priority);\n    }\n  },\n  beforeUnmount() {\n    this.openmct.indicators.off('addIndicator', this.addIndicator);\n  },\n  created() {\n    this.openmct.indicators.on('addIndicator', this.addIndicator);\n  },\n  methods: {\n    addIndicator(indicator) {\n      this.indicators.push(shallowRef(indicator));\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/layout/status-bar/indicators.scss",
    "content": ".c-indicator {\n  @include cControl();\n  @include cClickIconButtonLayout();\n  border-radius: $controlCr;\n  overflow: visible;\n  position: relative;\n  text-transform: uppercase;\n\n  button {\n    text-transform: uppercase;\n  }\n\n  &.no-minify {\n    // For items that cannot be minified\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n\n    > *,\n    &:before {\n      flex: 1 1 auto;\n    }\n\n    &:before {\n      margin-right: $interiorMarginSm;\n    }\n  }\n\n  &:not(.no-minify) {\n    &:before {\n      margin-right: 0 !important;\n    }\n  }\n}\n\n.c-indicator__label {\n  // Label element. Appears as a hover bubble element when Indicators are minified;\n  // Appears as an inline element when not.\n  display: inline-block;\n  transition: none;\n  white-space: nowrap;\n\n  a,\n  button,\n  .s-button,\n  .c-button {\n    // Make <a> in label look like buttons\n    @include transition(background-color);\n    background-color: transparent;\n    border: 1px solid rgba($colorIndicatorMenuFg, 0.8);\n    border-radius: $controlCr;\n    box-sizing: border-box;\n    color: inherit;\n    font-size: inherit;\n    height: auto;\n    line-height: normal;\n    padding: 0 2px;\n    @include hover {\n      background-color: rgba($colorIndicatorMenuFg, 0.1);\n      border-color: rgba($colorIndicatorMenuFg, 0.75);\n      color: $colorIndicatorMenuFgHov;\n    }\n  }\n\n  [class*='icon-'] {\n    // If any elements within label include the class 'icon-*' then deal with their :before's\n    &:before {\n      font-size: 0.8em;\n      margin-right: $interiorMarginSm;\n    }\n  }\n}\n\n.c-indicator__count {\n  display: none; // Only displays when Indicator is minified, see below\n}\n\n[class*='minify-indicators'] {\n  // All styles for minified Indicators should go in here\n  .c-indicator:not(.no-minify) {\n    border: 1px solid transparent; // Hack to make minified sizing work in Safari. Have no idea why this works.\n    overflow: visible;\n    transition: transform;\n\n    @include hover() {\n      background: $colorIndicatorBgHov;\n      transition: transform 250ms ease-in 200ms; // Go-away transition\n\n      .c-indicator__label {\n        box-shadow: $colorIndicatorMenuBgShdw;\n        transform: scale(1);\n        overflow: visible;\n        transition: transform 100ms ease-out 100ms; // Appear transition\n      }\n    }\n    .c-indicator__label {\n      transition: transform 250ms ease-in 200ms; // Go-away transition\n      background: $colorIndicatorMenuBg;\n      color: $colorIndicatorMenuFg;\n      border-radius: $controlCr;\n      right: 0;\n      top: 130%;\n      padding: $interiorMargin $interiorMargin;\n      position: absolute;\n      transform-origin: 90% 0;\n      transform: scale(0);\n      overflow: hidden;\n      z-index: 50;\n\n      &:before {\n        // Infobubble-style arrow element\n        content: '';\n        display: block;\n        position: absolute;\n        bottom: 100%;\n        right: 8px;\n        @include triangle('up', $size: 4px, $ratio: 1, $color: $colorIndicatorMenuBg);\n      }\n    }\n\n    .c-indicator__count {\n      display: inline-block;\n      margin-left: $interiorMarginSm;\n    }\n  }\n}\n\n/* Mobile */\n// Hide the clock indicator when we're phone portrait\nbody.phone.portrait {\n  .c-indicator.t-indicator-clock {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/ui/layout/status-bar/notification-banner.scss",
    "content": "@mixin statusBannerColors($bg, $fg: $colorStatusFg) {\n  $bgPb: 10%;\n  $bgPbD: 10%;\n  background-color: darken($bg, $bgPb);\n  color: $fg;\n  &:hover {\n    background-color: darken($bg, $bgPb - $bgPbD);\n  }\n  .s-action {\n    background-color: darken($bg, $bgPb + $bgPbD);\n    &:hover {\n      background-color: darken($bg, $bgPb);\n    }\n  }\n}\n\n.c-message-banner {\n  $closeBtnSize: 7px;\n\n  border-radius: $controlCr;\n  @include statusBannerColors($colorStatusDefault, $colorStatusFg);\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  left: 50%;\n  top: 50%;\n  max-width: 50%;\n  max-height: 25px;\n  padding: $interiorMarginSm $interiorMargin $interiorMarginSm $interiorMarginLg;\n  position: absolute;\n  transform: translate(-50%, -50%);\n  z-index: 2;\n\n  > * + * {\n    margin-left: $interiorMargin;\n  }\n\n  &.ok {\n    @include statusBannerColors($colorOk, $colorOkFg);\n  }\n\n  &.info {\n    @include statusBannerColors($colorInfo, $colorInfoFg);\n  }\n  &.caution,\n  &.warning,\n  &.alert {\n    @include statusBannerColors($colorWarningLo, $colorWarningLoFg);\n  }\n  &.error {\n    @include statusBannerColors($colorWarningHi, $colorWarningHiFg);\n  }\n\n  &__message {\n    @include ellipsize();\n    flex: 1 1 auto;\n  }\n\n  &__progress-bar {\n    height: 7px;\n    width: 70px;\n\n    // Only show the progress bar\n    .c-progress-bar {\n      &__text {\n        display: none;\n      }\n    }\n  }\n\n  &__close-button {\n    font-size: 1.25em;\n  }\n}\n"
  },
  {
    "path": "src/ui/mixins/context-menu-gesture.js",
    "content": "import { toRaw } from 'vue';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    objectPath: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  data() {\n    return {\n      contextClickActive: false\n    };\n  },\n  mounted() {\n    this.unobserveObjects = {};\n    //TODO: touch support\n    this.$nextTick(() => {\n      this.$refs.root.addEventListener('contextmenu', this.showContextMenu);\n    });\n\n    function updateObject(oldObject, newObject) {\n      const rawNewObject = toRaw(newObject);\n      const rawOldObject = toRaw(oldObject);\n      Object.assign(rawOldObject, rawNewObject);\n    }\n\n    this.objectPath.forEach((object) => {\n      if (object) {\n        const key = this.openmct.objects.makeKeyString(object.identifier);\n        this.unobserveObjects[key] = this.openmct.objects.observe(\n          object,\n          '*',\n          updateObject.bind(this, object)\n        );\n      }\n    });\n  },\n  beforeUnmount() {\n    this.removeListeners();\n    this.$refs.root.removeEventListener('contextMenu', this.showContextMenu);\n  },\n  methods: {\n    removeListeners() {\n      Object.values(this.unobserveObjects).forEach((unobserve) => unobserve());\n      this.unobserveObjects = {};\n    },\n    showContextMenu(event) {\n      if (this.readOnly) {\n        return;\n      }\n\n      event.preventDefault();\n      event.stopPropagation();\n\n      let actionsCollection = this.openmct.actions.getActionsCollection(toRaw(this.objectPath));\n      let actions = actionsCollection.getVisibleActions();\n      let sortedActions = this.openmct.actions._groupAndSortActions(actions);\n\n      const menuOptions = {\n        onDestroy: this.onContextMenuDestroyed,\n        label: this.objectPath[0].name\n      };\n\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        sortedActions,\n        actionsCollection.objectPath,\n        actionsCollection.view\n      );\n      this.openmct.menus.showMenu(event.clientX, event.clientY, menuItems, menuOptions);\n      this.contextClickActive = true;\n      this.$emit('context-click-active', true);\n    },\n    onContextMenuDestroyed() {\n      this.contextClickActive = false;\n      this.$emit('context-click-active', false);\n    }\n  }\n};\n"
  },
  {
    "path": "src/ui/mixins/object-link.js",
    "content": "import { objectPathToUrl } from '../../tools/url.js';\n\nexport default {\n  inject: ['openmct'],\n  props: {\n    objectPath: {\n      type: Array,\n      default() {\n        return [];\n      }\n    }\n  },\n  computed: {\n    objectLink() {\n      if (!this.objectPath.length) {\n        return;\n      }\n\n      if (this.navigateToPath) {\n        return '#' + this.navigateToPath;\n      }\n\n      const url = objectPathToUrl(this.openmct, this.objectPath);\n\n      return url;\n    }\n  }\n};\n"
  },
  {
    "path": "src/ui/mixins/staleness-mixin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { isProxy, toRaw } from 'vue';\n\nimport { isIdentifier } from '@/api/objects/object-utils';\nimport StalenessUtils from '@/utils/staleness';\n\nexport default {\n  data() {\n    return {\n      staleObjects: [],\n      stalenessSubscription: {},\n      compositionObjectMap: new Map(),\n      setupClockChanged: false\n    };\n  },\n  computed: {\n    isStale() {\n      return this.staleObjects.length !== 0;\n    }\n  },\n  methods: {\n    getSubscriptionId(domainObject) {\n      // Only extract the identifier if it is not already an identifier\n      const identifier = isIdentifier(domainObject) ? domainObject : domainObject.identifier;\n      return this.openmct?.objects.makeKeyString(identifier);\n    },\n    setupClockChangedEvent(callback) {\n      this.setupClockChanged = true;\n      this.compositionIteratorCallback = this.compositionIterator(callback);\n      this.openmct.time.on('clockChanged', this.compositionIteratorCallback);\n    },\n    addToCompositionMap(id, domainObject) {\n      if (!this.compositionObjectMap.get(id)) {\n        this.compositionObjectMap.set(id, domainObject);\n      }\n    },\n    compositionIterator(callback) {\n      return () => {\n        this.staleObjects = [];\n        for (const [, object] of this.compositionObjectMap) {\n          let domainObject = object;\n          if (isProxy(domainObject)) {\n            domainObject = toRaw(object);\n          }\n          if (callback && typeof callback === 'function') {\n            callback(domainObject);\n          }\n        }\n      };\n    },\n    subscribeToStaleness(domainObjectList, callback) {\n      if (domainObjectList === null || domainObjectList === undefined) {\n        return;\n      }\n      if (!Array.isArray(domainObjectList)) {\n        domainObjectList = [domainObjectList];\n      }\n\n      domainObjectList.forEach((domainObject) => {\n        if (isProxy(domainObject)) {\n          domainObject = toRaw(domainObject);\n        }\n        const id = this.getSubscriptionId(domainObject);\n        this.addToCompositionMap(id, domainObject);\n        this.setupStalenessUtils(domainObject);\n        this.requestStaleness(domainObject, callback);\n        this.setupStalenessSubscription(domainObject, callback);\n      });\n    },\n    triggerUnsubscribeFromStaleness(domainObjectList, callback) {\n      if (domainObjectList === null || domainObjectList === undefined) {\n        return;\n      }\n      if (!Array.isArray(domainObjectList)) {\n        domainObjectList = [domainObjectList];\n      }\n\n      domainObjectList.forEach((domainObject) => {\n        if (isProxy(domainObject)) {\n          domainObject = toRaw(domainObject);\n        }\n\n        const id = this.getSubscriptionId(domainObject);\n        if (!this.stalenessSubscription[id]) {\n          return;\n        }\n        if (this.staleObjects.length !== 0) {\n          this.clearStaleness(id);\n        }\n        this.teardownStalenessSubscription(domainObject);\n        this.teardownStalenessUtils(domainObject);\n        delete this.stalenessSubscription[id];\n      });\n\n      if (callback && typeof callback === 'function') {\n        callback();\n      }\n    },\n    setupStalenessUtils(domainObject) {\n      const id = this.getSubscriptionId(domainObject);\n      if (this.stalenessSubscription[id]) {\n        return;\n      }\n      this.stalenessSubscription[id] = {};\n      this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(\n        this.openmct,\n        domainObject\n      );\n    },\n    teardownStalenessUtils(domainObject) {\n      const id = this.getSubscriptionId(domainObject);\n      const { stalenessUtils } = this.stalenessSubscription[id];\n      if (stalenessUtils) {\n        stalenessUtils.destroy();\n        delete this.stalenessSubscription[id].stalenessUtils;\n      }\n    },\n    setupStalenessSubscription(domainObject, callback) {\n      const id = this.getSubscriptionId(domainObject);\n      this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(\n        domainObject,\n        (stalenessResponse) => {\n          this.handleStalenessResponse(id, stalenessResponse, callback);\n        }\n      );\n    },\n    teardownStalenessSubscription(domainObject) {\n      const id = this.getSubscriptionId(domainObject);\n      const { unsubscribe } = this.stalenessSubscription[id];\n      if (unsubscribe) {\n        unsubscribe();\n        delete this.stalenessSubscription[id].unsubscribe;\n      }\n    },\n    resubscribeToStaleness(domainObject, callback, unsubscribeCallback) {\n      const id = this.getSubscriptionId(domainObject);\n      this.stalenessSubscription[id].resubscribe = () => {\n        this.staleObjects = [];\n        this.triggerUnsubscribeFromStaleness(domainObject, unsubscribeCallback);\n        this.setupStalenessSubscription(domainObject, callback);\n      };\n    },\n    async requestStaleness(domainObject, callback) {\n      const id = this.getSubscriptionId(domainObject);\n      const stalenessResponse = await this.openmct.telemetry.isStale(domainObject);\n      if (stalenessResponse !== undefined) {\n        this.handleStalenessResponse(id, stalenessResponse, callback);\n      }\n    },\n    handleStalenessResponse(id, stalenessResponse, callback) {\n      if (!id) {\n        id = Object.keys(this.stalenessSubscription)[0];\n      }\n\n      if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {\n        if (callback && typeof callback === 'function') {\n          callback(stalenessResponse);\n        } else {\n          this.addOrRemoveStaleObject(id, stalenessResponse);\n        }\n      }\n    },\n    clearStaleness(id) {\n      const stalenessResponse = { isStale: false };\n\n      if (!id) {\n        id = Object.keys(this.stalenessSubscription)[0];\n      }\n\n      this.addOrRemoveStaleObject(id, stalenessResponse);\n    },\n    addOrRemoveStaleObject(id, stalenessResponse) {\n      const index = this.staleObjects.indexOf(id);\n      if (stalenessResponse.isStale) {\n        if (index === -1) {\n          this.staleObjects.push(id);\n        }\n      } else {\n        if (index !== -1) {\n          this.staleObjects.splice(index, 1);\n        }\n      }\n    }\n  },\n  unmounted() {\n    let compositionObjects = [];\n    for (const [, object] of this.compositionObjectMap) {\n      compositionObjects.push(object);\n    }\n    this.triggerUnsubscribeFromStaleness(compositionObjects);\n\n    if (this.setupClockChanged) {\n      this.openmct.time.off('clockChanged', this.compositionIteratorCallback);\n      this.setupClockChanged = false;\n    }\n  }\n};\n"
  },
  {
    "path": "src/ui/mixins/toggle-mixin.js",
    "content": "export default {\n  data() {\n    return {\n      open: false\n    };\n  },\n  methods: {\n    toggle(event) {\n      if (this.open) {\n        if (this.isOpening) {\n          // Prevent document event handler from closing immediately\n          // after opening.  Can't use stopPropagation because that\n          // would break other menus with similar behavior.\n          this.isOpening = false;\n\n          return;\n        }\n\n        document.removeEventListener('click', this.toggle);\n        this.open = false;\n      } else {\n        document.addEventListener('click', this.toggle);\n        this.open = true;\n        this.isOpening = true;\n      }\n    }\n  },\n  unmounted() {\n    document.removeEventListener('click', this.toggle);\n  }\n};\n"
  },
  {
    "path": "src/ui/preview/PreviewAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\nimport mount from 'utils/mount';\n\nimport PreviewContainer from './PreviewContainer.vue';\n\nconst PREVIEW_ACTION_KEY = 'preview';\n\nclass PreviewAction extends EventEmitter {\n  constructor(openmct) {\n    super();\n    /**\n     * Metadata\n     */\n    this.name = 'View';\n    this.key = PREVIEW_ACTION_KEY;\n    this.description = 'View in large dialog';\n    this.cssClass = 'icon-items-expand';\n    this.group = 'windowing';\n    this.priority = 1;\n\n    /**\n     * Dependencies\n     */\n    this._openmct = openmct;\n\n    if (PreviewAction.isVisible === undefined) {\n      PreviewAction.isVisible = false;\n    }\n  }\n\n  invoke(objectPath, viewOptions) {\n    const { vNode, destroy } = mount(\n      {\n        components: {\n          PreviewContainer\n        },\n        provide: {\n          openmct: this._openmct,\n          objectPath: objectPath\n        },\n        data() {\n          return {\n            viewOptions\n          };\n        },\n        template: '<preview-container :view-options=\"viewOptions\"></preview-container>'\n      },\n      {\n        app: this._openmct.app\n      }\n    );\n\n    const overlay = this._openmct.overlays.overlay({\n      element: vNode.el,\n      size: 'large',\n      autoHide: false,\n      buttons: [\n        {\n          label: 'Done',\n          callback: () => {\n            overlay.dismiss();\n          }\n        }\n      ],\n      onDestroy: () => {\n        PreviewAction.isVisible = false;\n        destroy();\n        this.emit('isVisible', false);\n        overlay.dismiss();\n      }\n    });\n\n    PreviewAction.isVisible = true;\n    this.emit('isVisible', true);\n  }\n\n  appliesTo(objectPath, view = {}) {\n    const parentElement = view.parentElement;\n    const isObjectView = parentElement && parentElement.classList.contains('js-object-view');\n\n    return (\n      !PreviewAction.isVisible &&\n      !this._openmct.router.isNavigatedObject(objectPath) &&\n      !isObjectView\n    );\n  }\n\n  _preventPreview(objectPath) {\n    const noPreviewTypes = ['folder'];\n\n    return noPreviewTypes.includes(objectPath[0].type);\n  }\n}\n\nexport { PREVIEW_ACTION_KEY };\n\nexport default PreviewAction;\n"
  },
  {
    "path": "src/ui/preview/PreviewContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div role=\"dialog\" aria-label=\"Preview Container\" class=\"l-preview-window js-preview-window\">\n    <PreviewHeader\n      ref=\"previewHeader\"\n      :current-view=\"view\"\n      :domain-object=\"domainObject\"\n      :views=\"viewProviders\"\n    />\n    <div class=\"l-preview-window__object-view js-notebook-snapshot-item\">\n      <div ref=\"objectView\"></div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport StyleRuleManager from '@/plugins/condition/StyleRuleManager';\nimport { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';\n\nimport PreviewHeader from './PreviewHeader.vue';\n\nexport default {\n  components: {\n    PreviewHeader\n  },\n  inject: ['openmct', 'objectPath'],\n  props: {\n    viewOptions: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    existingView: {\n      type: Object,\n      default() {\n        return undefined;\n      }\n    }\n  },\n  data() {\n    let domainObject = this.objectPath[0];\n\n    return {\n      domainObject: domainObject,\n      viewKey: null,\n      view: null,\n      viewProviders: [],\n      currentViewProvider: {},\n      existingViewIndex: 0\n    };\n  },\n  mounted() {\n    this.viewProviders = this.openmct.objectViews.get(this.domainObject, this.objectPath);\n    this.viewProviders.forEach((provider, index) => {\n      provider.onItemClicked = () => {\n        if (this.existingView && provider.key === this.existingView.key) {\n          this.existingViewIndex = index;\n        }\n\n        this.setView(provider);\n      };\n    });\n\n    this.setView(this.viewProviders[0]);\n  },\n  beforeUnmount() {\n    if (this.stopListeningStyles) {\n      this.stopListeningStyles();\n    }\n\n    if (this.styleRuleManager) {\n      this.styleRuleManager.destroy();\n      delete this.styleRuleManager;\n    }\n  },\n  unmounted() {\n    if (!this.existingView) {\n      this.view.destroy();\n    } else if (this.existingViewElement) {\n      // if the existing view element is populated, it's the currently viewed view\n      // in preview and we need to add it back to the parent.\n      this.addExistingViewBackToParent();\n    }\n  },\n  methods: {\n    clear() {\n      if (this.view) {\n        if (this.view !== this.existingView) {\n          this.view.destroy();\n        } else {\n          this.addExistingViewBackToParent();\n        }\n\n        this.$refs.objectView.innerHTML = '';\n        delete this.view;\n        delete this.viewContainer;\n      }\n    },\n    setView(viewProvider) {\n      if (this.viewKey === viewProvider.key) {\n        return;\n      }\n\n      const isExistingView = viewProvider.key === this.existingView?.key;\n\n      this.clear();\n\n      this.viewKey = viewProvider.key;\n      this.initializeViewContainer();\n\n      if (isExistingView) {\n        this.view = this.existingView;\n        this.existingViewElement = this.existingView.parentElement.firstElementChild;\n        this.currentViewProvider = this.viewProviders[this.existingViewIndex];\n      } else {\n        this.currentViewProvider = viewProvider;\n        this.view = this.currentViewProvider.view(this.domainObject, this.objectPath);\n      }\n\n      if (isExistingView) {\n        this.viewContainer.appendChild(this.existingViewElement);\n      } else {\n        // in preview mode, we're always visible\n        this.viewOptions.renderWhenVisible = (func) => {\n          window.requestAnimationFrame(func);\n          return true;\n        };\n        this.view.show(this.viewContainer, false, this.viewOptions);\n      }\n\n      this.initObjectStyles();\n    },\n    addExistingViewBackToParent() {\n      this.existingView.parentElement.appendChild(this.existingViewElement);\n      delete this.existingViewElement;\n    },\n    initializeViewContainer() {\n      this.viewContainer = this.$refs.objectView;\n    },\n    initObjectStyles() {\n      if (!this.styleRuleManager) {\n        this.styleRuleManager = new StyleRuleManager(\n          this.domainObject.configuration && this.domainObject.configuration.objectStyles,\n          this.openmct,\n          this.updateStyle.bind(this)\n        );\n      } else {\n        this.styleRuleManager.updateObjectStyleConfig(\n          this.domainObject.configuration && this.domainObject.configuration.objectStyles\n        );\n      }\n\n      if (this.stopListeningStyles) {\n        this.stopListeningStyles();\n      }\n\n      this.stopListeningStyles = this.openmct.objects.observe(\n        this.domainObject,\n        'configuration.objectStyles',\n        (newObjectStyle) => {\n          //Updating styles in the inspector view will trigger this so that the changes are reflected immediately\n          this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);\n        }\n      );\n    },\n    updateStyle(styleObj) {\n      if (!styleObj) {\n        return;\n      }\n\n      let keys = Object.keys(styleObj);\n      let firstChild = this.$refs.objectView.querySelector(':first-child');\n\n      keys.forEach((key) => {\n        if (firstChild) {\n          if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) {\n            if (firstChild.style[key]) {\n              firstChild.style[key] = '';\n            }\n          } else {\n            if (\n              !styleObj.isStyleInvisible &&\n              firstChild.classList.contains(STYLE_CONSTANTS.isStyleInvisible)\n            ) {\n              firstChild.classList.remove(STYLE_CONSTANTS.isStyleInvisible);\n            } else if (\n              styleObj.isStyleInvisible &&\n              !firstChild.classList.contains(styleObj.isStyleInvisible)\n            ) {\n              firstChild.classList.add(styleObj.isStyleInvisible);\n            }\n\n            firstChild.style[key] = styleObj[key];\n          }\n        }\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/preview/PreviewHeader.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-preview-header l-browse-bar\">\n    <div class=\"l-browse-bar__start\">\n      <div class=\"l-browse-bar__object-name--w c-object-label\">\n        <div v-if=\"type\" class=\"c-object-label__type-icon\" :class=\"type.definition.cssClass\"></div>\n        <span class=\"l-browse-bar__object-name c-object-label__name\">\n          {{ domainObject.name }}\n        </span>\n      </div>\n    </div>\n    <div class=\"l-browse-bar__end\">\n      <ViewSwitcher :v-if=\"!hideViewSwitcher\" :views=\"views\" :current-view=\"currentView\" />\n      <NotebookMenuSwitcher\n        :domain-object=\"domainObject\"\n        :object-path=\"objectPath\"\n        :is-preview=\"true\"\n        :current-view=\"currentView\"\n        class=\"c-notebook-snapshot-menubutton\"\n      />\n      <div class=\"l-browse-bar__actions\">\n        <button\n          v-for=\"(item, index) in statusBarItems\"\n          :key=\"index\"\n          class=\"c-button\"\n          :class=\"item.cssClass\"\n          @click=\"item.onItemClicked\"\n        ></button>\n        <button\n          class=\"l-browse-bar__actions c-icon-button icon-3-dots\"\n          title=\"More actions\"\n          aria-label=\"More actions\"\n          @click.prevent.stop=\"showMenuItems($event)\"\n        ></button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { toRaw } from 'vue';\n\nimport { CREATE_ACTION_KEY } from '@/plugins/formActions/CreateAction.js';\nimport { MOVE_ACTION_KEY } from '@/plugins/move/MoveAction.js';\nimport NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';\nimport { RELOAD_ACTION_KEY } from '@/plugins/reloadAction/ReloadAction.js';\nimport { REMOVE_ACTION_KEY } from '@/plugins/remove/RemoveAction.js';\nimport { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';\nimport { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';\n\nimport ViewSwitcher from '../layout/ViewSwitcher.vue';\n\nexport default {\n  components: {\n    NotebookMenuSwitcher,\n    ViewSwitcher\n  },\n  inject: ['openmct', 'objectPath'],\n  props: {\n    currentView: {\n      type: Object,\n      default: () => {\n        return {};\n      }\n    },\n    domainObject: {\n      type: Object,\n      default: () => {\n        return {};\n      }\n    },\n    hideViewSwitcher: {\n      type: Boolean,\n      default: () => {\n        return false;\n      }\n    },\n    views: {\n      type: Array,\n      default: () => {\n        return [];\n      }\n    }\n  },\n  emits: ['set-view'],\n  data() {\n    return {\n      type: this.openmct.types.get(this.domainObject.type),\n      statusBarItems: [],\n      menuActionItems: []\n    };\n  },\n  watch: {\n    currentView: {\n      handler: function (newView) {\n        if (this.actionCollection) {\n          this.unlistenToActionCollection();\n        }\n        this.actionCollection = this.openmct.actions.getActionsCollection(\n          toRaw(this.objectPath),\n          toRaw(newView)\n        );\n\n        this.actionCollection.on('update', this.updateActionItems);\n        this.updateActionItems(this.actionCollection.getActionsObject());\n      },\n      flush: 'post' // Access the DOM after Vue has updated it\n    }\n  },\n  created() {\n    this.HIDDEN_ACTIONS = [\n      CREATE_ACTION_KEY,\n      REMOVE_ACTION_KEY,\n      MOVE_ACTION_KEY,\n      PREVIEW_ACTION_KEY,\n      VIEW_LARGE_ACTION_KEY,\n      RELOAD_ACTION_KEY\n    ];\n  },\n  beforeUnmount() {\n    if (this.actionCollection) {\n      this.actionCollection.off('update', this.updateActionItems);\n    }\n  },\n  methods: {\n    filterHiddenItems(menuItems) {\n      const items = [];\n      menuItems.forEach((menuItem) => {\n        const isGroup = Array.isArray(menuItem);\n        if (isGroup) {\n          items.push(this.filterHiddenItems(menuItem));\n        } else if (this.HIDDEN_ACTIONS.includes(menuItem.key) === false) {\n          items.push(menuItem);\n        }\n      });\n\n      return items;\n    },\n    setView(view) {\n      this.$emit('set-view', view);\n    },\n    unlistenToActionCollection() {\n      this.actionCollection.off('update', this.updateActionItems);\n      delete this.actionCollection;\n    },\n    updateActionItems() {\n      const statusBarItems = this.actionCollection.getStatusBarActions();\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        statusBarItems,\n        this.actionCollection.objectPath,\n        this.actionCollection.view\n      );\n      this.statusBarItems = this.filterHiddenItems(menuItems);\n      this.menuActionItems = this.actionCollection.getVisibleActions();\n    },\n    showMenuItems(event) {\n      let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);\n      const menuItems = this.openmct.menus.actionsToMenuItems(\n        sortedActions,\n        this.actionCollection.objectPath,\n        this.actionCollection.view\n      );\n\n      const visibleMenuItems = this.filterHiddenItems(menuItems);\n      this.openmct.menus.showMenu(event.x, event.y, visibleMenuItems);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/preview/ViewHistoricalDataAction.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport PreviewAction from './PreviewAction.js';\n\nconst VIEW_HISTORICAL_DATA_ACTION_KEY = 'viewHistoricalData';\n\nclass ViewHistoricalDataAction extends PreviewAction {\n  constructor(openmct) {\n    super(openmct);\n\n    this.name = 'View Historical Data';\n    this.key = VIEW_HISTORICAL_DATA_ACTION_KEY;\n    this.description = 'View Historical Data in a Table or Plot';\n    this.cssClass = 'icon-eye-open';\n    this.hideInDefaultMenu = true;\n  }\n\n  appliesTo(objectPath, view = {}) {\n    let viewContext = view.getViewContext && view.getViewContext();\n\n    return (\n      objectPath.length && viewContext && viewContext.row && viewContext.row.viewHistoricalData\n    );\n  }\n}\n\nexport { VIEW_HISTORICAL_DATA_ACTION_KEY };\n\nexport default ViewHistoricalDataAction;\n"
  },
  {
    "path": "src/ui/preview/plugin.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport PreviewAction from './PreviewAction.js';\nimport ViewHistoricalDataAction from './ViewHistoricalDataAction.js';\n\nexport default function () {\n  return function (openmct) {\n    openmct.actions.register(new PreviewAction(openmct));\n    openmct.actions.register(new ViewHistoricalDataAction(openmct));\n  };\n}\n"
  },
  {
    "path": "src/ui/preview/preview.scss",
    "content": ".l-preview-window {\n  display: flex;\n  flex-direction: column;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n\n  > * + * {\n    margin-top: $interiorMargin;\n  }\n\n  &__object-name {\n    flex: 0 0 auto;\n  }\n\n  &__object-view {\n    flex: 1 1 auto;\n    height: 100%; // Chrome 73\n    overflow: auto;\n\n    > div:not([class]) {\n      // Target an immediate child div without a class and make it display: contents\n      display: contents;\n    }\n  }\n}\n"
  },
  {
    "path": "src/ui/registries/InspectorViewRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst DEFAULT_VIEW_PRIORITY = 0;\n\n/**\n * A InspectorViewRegistry maintains the definitions for views\n * that may occur in the inspector.\n */\nexport default class InspectorViewRegistry {\n  constructor() {\n    /** @type {Record<string, ViewProvider>} */\n    this.providers = {};\n  }\n\n  /**\n   *\n   * @param {DomainObject} selection the object to be viewed\n   * @returns {ViewProvider[]} any providers\n   *          which can provide views of this object\n   * @private for platform-internal use\n   */\n  get(selection) {\n    function byPriority(providerA, providerB) {\n      const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY;\n      const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY;\n\n      return priorityB - priorityA;\n    }\n\n    return this.#getAllProviders()\n      .filter((provider) => provider.canView(selection))\n      .map((provider) => {\n        const view = provider.view(selection);\n        view.key = provider.key;\n        view.name = provider.name;\n        view.glyph = provider.glyph;\n\n        return view;\n      })\n      .sort(byPriority);\n  }\n\n  /**\n   * Registers a new inspector view provider.\n   *\n   * @param {ViewProvider} provider the provider for this view\n   */\n  addProvider(provider) {\n    const key = provider.key;\n    const name = provider.name;\n\n    if (key === undefined) {\n      throw \"View providers must have a unique 'key' property defined\";\n    }\n\n    if (name === undefined) {\n      throw \"View providers must have a 'name' property defined\";\n    }\n\n    if (this.providers[key] !== undefined) {\n      console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`);\n    }\n\n    this.providers[key] = provider;\n  }\n\n  /**\n   * Retrieves a view provider by its key.\n   * @param {string} key the key of the view provider\n   * @returns {ViewProvider} the view provider\n   */\n  getByProviderKey(key) {\n    return this.providers[key];\n  }\n\n  /**\n   * @returns {ViewProvider[]} all providers\n   */\n  #getAllProviders() {\n    return Object.values(this.providers);\n  }\n}\n\n/**\n * @typedef {import(\"openmct\").View} View\n * @typedef {import(\"openmct\").ViewProvider} ViewProvider\n * @typedef {import('openmct').DomainObject} DomainObject\n */\n"
  },
  {
    "path": "src/ui/registries/ToolbarRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * A ToolbarRegistry maintains the definitions for toolbars.\n *\n * @interface ToolbarRegistry\n */\nexport default class ToolbarRegistry {\n  constructor() {\n    this.providers = {};\n  }\n\n  /**\n   * Gets toolbar controls from providers which can provide a toolbar for this selection.\n   *\n   * @param {Object} selection the selection object\n   * @returns {Object[]} an array of objects defining controls for the toolbar\n   * @private for platform-internal use\n   */\n  get(selection) {\n    const providers = this.getAllProviders().filter(function (provider) {\n      return provider.forSelection(selection);\n    });\n\n    const structure = [];\n\n    providers.forEach((provider) => {\n      provider.toolbar(selection).forEach((item) => structure.push(item));\n    });\n\n    return structure;\n  }\n\n  /**\n   * @private\n   */\n  getAllProviders() {\n    return Object.values(this.providers);\n  }\n\n  /**\n   * @private\n   */\n  getByProviderKey(key) {\n    return this.providers[key];\n  }\n\n  /**\n   * Registers a new type of toolbar.\n   *\n   * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar\n   * @method addProvider\n   */\n  addProvider(provider) {\n    const key = provider.key;\n\n    if (key === undefined) {\n      throw \"Toolbar providers must have a unique 'key' property defined.\";\n    }\n\n    if (this.providers[key] !== undefined) {\n      console.warn(\"Provider already defined for key '%s'. Provider keys must be unique.\", key);\n    }\n\n    this.providers[key] = provider;\n  }\n}\n\n/**\n * Exposes types of toolbars in Open MCT.\n *\n * @interface ToolbarProvider\n * @property {string} key a unique identifier for this toolbar\n * @property {string} name the human-readable name of this toolbar\n * @property {string} [description] a longer-form description (typically\n *           a single sentence or short paragraph) of this kind of toolbar\n */\n\n/**\n * Checks if this provider can supply toolbar for a selection.\n *\n * @method forSelection\n * @param {module:openmct.selection} selection\n * @returns {boolean} 'true' if the toolbar applies to the provided selection,\n *          otherwise 'false'.\n */\n\n/**\n * Provides controls that comprise a toolbar.\n *\n * @method toolbar\n * @param {Object} selection the selection object\n * @returns {Object[]} an array of objects defining controls for the toolbar.\n */\n"
  },
  {
    "path": "src/ui/registries/ViewRegistry.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\n\nimport PRIORITIES from '../../api/priority/PriorityAPI';\n\n/**\n * A ViewRegistry maintains the definitions for different kinds of views\n * that may occur in different places in the user interface.\n */\nexport default class ViewRegistry extends EventEmitter {\n  constructor() {\n    super();\n    EventEmitter.apply(this);\n    /** @type {Record<string, ViewProvider>} */\n    this.providers = {};\n  }\n\n  /**\n   * for platform-internal use\n   * @param {import('openmct').DomainObject} item the object to be viewed\n   * @param {import('openmct').ObjectPath} objectPath - The current contextual object path of the view object\n   * @returns {ViewProvider[]} a list of providers that can provide views for this object, sorted by\n   * descending priority\n   */\n  get(item, objectPath) {\n    if (objectPath === undefined) {\n      throw 'objectPath must be provided to get applicable views for an object';\n    }\n\n    function byPriority(providerA, providerB) {\n      let priorityA = providerA.priority ? providerA.priority(item) : PRIORITIES.DEFAULT;\n      let priorityB = providerB.priority ? providerB.priority(item) : PRIORITIES.DEFAULT;\n\n      return priorityB - priorityA;\n    }\n\n    return this.getAllProviders()\n      .filter(function (provider) {\n        return provider.canView(item, objectPath);\n      })\n      .sort(byPriority);\n  }\n\n  /**\n   * @private\n   */\n  getAllProviders() {\n    return Object.values(this.providers);\n  }\n\n  /**\n   * Register a new type of view.\n   *\n   * @param {ViewProvider} provider the provider for this view\n   */\n  addProvider(provider) {\n    const key = provider.key;\n    if (key === undefined) {\n      throw \"View providers must have a unique 'key' property defined\";\n    }\n\n    if (this.providers[key] !== undefined) {\n      console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`);\n    }\n\n    const wrappedView = provider.view.bind(provider);\n    provider.view = (domainObject, objectPath) => {\n      const viewObject = wrappedView(domainObject, objectPath);\n      const wrappedShow = viewObject.show.bind(viewObject);\n      viewObject.key = key; // provide access to provider key on view object\n      viewObject.show = (element, isEditing, viewOptions) => {\n        viewObject.parentElement = element.parentElement;\n        wrappedShow(element, isEditing, viewOptions);\n      };\n\n      return viewObject;\n    };\n\n    this.providers[key] = provider;\n  }\n\n  /**\n   * Returns the view provider by key\n   * @param {string} key\n   * @returns {ViewProvider}\n   */\n  getByProviderKey(key) {\n    return this.providers[key];\n  }\n\n  /**\n   * Used internally to support seamless usage of new views with old\n   * views.\n   * @private\n   */\n  getByVPID(vpid) {\n    return this.providers.filter(function (p) {\n      return p.vpid === vpid;\n    })[0];\n  }\n}\n\n/**\n * @typedef {import('openmct').DomainObject} DomainObject\n * @typedef {import('openmct').ObjectPath} ObjectPath\n */\n\n/**\n * @typedef {Object} ViewOptions\n * @property {() => void} [renderWhenVisible]\n * This function can be used for all rendering logic that would otherwise be executed within a\n * `requestAnimationFrame` call. When called, `renderWhenVisible` will either execute the provided\n * function immediately (via `requestAnimationFrame`) if the view is currently visible, or defer its\n * execution until the view becomes visible.\n *\n * Additionally, `renderWhenVisible` returns a boolean value indicating whether the provided\n * function was executed immediately (`true`) or deferred (`false`).\n * Monitoring of visibility begins after the first call to `renderWhenVisible` is made.\n */\n\n/**\n * @typedef {Object} View\n * A View is used to provide displayable content, and to react to\n * associated life cycle events.\n * @property {(container: HTMLElement, isEditing: boolean | undefined, viewOptions: ViewOptions | undefined) => void} show\n * Populate the supplied DOM element with the contents of this view.\n * View implementations should use this method to attach any\n * listeners or acquire other resources that are necessary to keep\n * the contents of this view up-to-date.\n *\n * - `container`: The DOM element where the view should be rendered.\n * - `isEditing`: Indicates whether the view is in editing mode.\n * - `viewOptions`: An object with configuration options for the view.\n * @property {() => void} destroy - Release any resources associated with this view.\n * View implementations should use this method to detach any listeners or release other resources\n * that are no longer necessary once a view is no longer used.\n * @property {() => { item: DomainObject, isMultiSelectEvent: boolean }} [getSelectionContext]\n * A function that returns the selection context of the view.\n\n * View implementations may use this method to customize the selection context.\n */\n\n/**\n * Exposes types of views in Open MCT.\n *\n * @typedef {Object} ViewProvider\n * @property {string} key - The unique key that identifies this view\n * @property {string} name - The name of the view\n * @property {string} [cssClass] - The CSS class to apply to labels for this view (to add icons,\n * for instance)\n * @property {(domainObject: DomainObject, objectPath: ObjectPath) => boolean} canView\n * Returns true if this provider is able to supply views for the given {@link DomainObject}.\n *\n * When called by Open MCT, this may include additional arguments\n * which are on the path to the object to be viewed; for instance,\n * when viewing \"A Folder\" within \"My Items\", this method will be\n * invoked with \"A Folder\" (as a {@link DomainObject}) as the first argument.\n * @property {(domainObject: DomainObject, objectPath: ObjectPath) => boolean} [canEdit]\n * An optional function that defines whether or not this view can be used to edit a given object.\n * If not provided, will default to `false` and the view will not support editing.\n * @property {(domainObject: DomainObject, objectPath: ObjectPath) => View} view A function that\n * provides a view for the provided domain object.\n * @property {(domainObject: DomainObject) => number} [priority]\n * A function that returns the priority of the view. The more positive the value, the higher the\n * priority. Similarly, the more negative the value, the lower the priority.\n *\n * If not provided, the default priority of 100 will be used. This value is used to sort the views\n * by descending priority if there are multiple views that can be shown for a given object.\n */\n"
  },
  {
    "path": "src/ui/router/ApplicationRouter.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { EventEmitter } from 'eventemitter3';\nimport LocationBar from 'location-bar';\nimport _ from 'lodash';\n\nclass ApplicationRouter extends EventEmitter {\n  /**\n   * events\n   * change:params -> notify listeners w/ new, old, and changed.\n   * change:path -> notify listeners w/ new, old paths.\n   *\n   * methods:\n   * update(path, params) -> updates path and params at the same time.  Only\n   *   updates specified params, other params are not modified.\n   * updateParams(newParams) -> update only specified params, leaving rest\n   *      intact.  Does not modify path.\n   * updatePath(path);\n   *\n   * route(path, handler);\n   * start(); Start routing.\n   * @param {import('../../../openmct').OpenMCT} openmct\n   */\n\n  constructor(openmct) {\n    super();\n\n    this.locationBar = new LocationBar();\n    this.openmct = openmct;\n    this.routes = [];\n    this.started = false;\n    this.path = null;\n\n    this.setHash = _.debounce(this.setHash.bind(this), 300);\n\n    openmct.once('destroy', () => {\n      this.destroy();\n    });\n  }\n\n  // Public Methods\n\n  destroy() {\n    this.locationBar.stop();\n  }\n\n  /**\n   * Delete a given query parameter from current url\n   *\n   * @param {string} paramName name of searchParam to delete from current url searchParams\n   */\n  deleteSearchParam(paramName) {\n    let url = this.getHashRelativeURL();\n\n    url.searchParams.delete(paramName);\n    this.setLocationFromUrl();\n  }\n\n  /**\n   * object for accessing all current search parameters\n   *\n   * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams}\n   */\n  getAllSearchParams() {\n    return this.getHashRelativeURL()?.searchParams;\n  }\n\n  /**\n   * Uniquely identifies a domain object.\n   *\n   * @typedef CurrentLocation\n   * @property {URL} url current url location\n   * @property {string} path current url location pathname\n   * @property {string} getQueryString a function which returns url search query\n   * @property {Object} params object representing url searchParams\n   */\n\n  /**\n   * object for accessing current url location and search params\n   *\n   * @returns {CurrentLocation} A {@link CurrentLocation}\n   */\n  getCurrentLocation() {\n    return this.currentLocation;\n  }\n\n  /**\n   * Get current location URL Object\n   *\n   * @returns {URL} current url location\n   */\n  getHashRelativeURL() {\n    return this.getCurrentLocation()?.url;\n  }\n\n  /**\n   * Get current location URL Object searchParams\n   *\n   * @returns {Object} object representing current url searchParams\n   */\n  getParams() {\n    return this.currentLocation.params;\n  }\n\n  /**\n   * Get a value of given param from current url searchParams\n   *\n   * @returns {string} value of paramName from current url searchParams\n   */\n  getSearchParam(paramName) {\n    return this.getAllSearchParams()?.get(paramName);\n  }\n\n  /**\n   * Navigate to given hash, update current location object, and notify listeners about location change\n   *\n   * @param {string} hash The URL hash to navigate to in the form of \"#/browse/mine/{keyString}/{keyString}\".\n   * Should not include any params.\n   */\n  navigate(hash) {\n    this.handleLocationChange(hash.substring(1));\n  }\n\n  /**\n   * Check if a given object and current location object are same\n   *\n   * @param {Array<Object>} objectPath Object path of a given Domain Object\n   *\n   * @returns {boolean}\n   */\n  isNavigatedObject(objectPath) {\n    let targetObject = objectPath[0];\n    let navigatedObject = this.path[0];\n\n    if (!targetObject.identifier) {\n      return false;\n    }\n\n    return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);\n  }\n\n  /**\n   * Add routes listeners\n   *\n   * @param {RegExp} matcher Regex to match value in url\n   * @param {Function} callback function called when found match in url\n   */\n  route(matcher, callback) {\n    this.routes.push({\n      matcher,\n      callback\n    });\n  }\n\n  /**\n   * Set url hash using path and queryString\n   *\n   * @param {string} path path for url\n   * @param {string} queryString queryString for url\n   */\n  set(path, queryString) {\n    this.setHash(`${path}?${queryString}`);\n  }\n\n  /**\n   * Will replace all current search parameters with the ones defined in urlSearchParams\n   */\n  setAllSearchParams() {\n    this.setLocationFromUrl();\n  }\n\n  /**\n   * To force update url based on value in currentLocation object\n   */\n  setLocationFromUrl() {\n    this.updateTimeSettings();\n  }\n\n  /**\n   * Set url hash using path\n   *\n   * @param {string} path path for url\n   */\n  setPath(path) {\n    this.handleLocationChange(path.substring(1));\n  }\n\n  /**\n   * Update param value from current url searchParams\n   *\n   * @param {string} paramName param name from current url searchParams\n   * @param {string} paramValue param value from current url searchParams\n   */\n  setSearchParam(paramName, paramValue) {\n    let url = this.getHashRelativeURL();\n\n    url.searchParams.set(paramName, paramValue);\n    this.setLocationFromUrl();\n  }\n\n  /**\n   * start application routing, should be done after handlers are registered.\n   */\n  start() {\n    if (this.started) {\n      throw new Error('Router already started!');\n    }\n\n    this.started = true;\n\n    this.locationBar.onChange((p) => this.hashChanged(p));\n    this.locationBar.start({\n      root: location.pathname\n    });\n  }\n\n  /**\n   * Set url hash using path and searchParams object\n   *\n   * @param {string} path path for url\n   * @param {string} params oject representing searchParams key/value\n   */\n  update(path, params) {\n    let searchParams = this.currentLocation.url.searchParams;\n    for (let [key, value] of Object.entries(params)) {\n      if (typeof value === 'undefined') {\n        searchParams.delete(key);\n      } else {\n        searchParams.set(key, value);\n      }\n    }\n\n    this.set(path, searchParams.toString());\n  }\n\n  /**\n   * Update route params. Takes an object of updates.  New parameters\n   */\n  updateParams(updateParams) {\n    let searchParams = this.currentLocation.url.searchParams;\n    Object.entries(updateParams).forEach(([key, value]) => {\n      if (typeof value === 'undefined') {\n        searchParams.delete(key);\n      } else {\n        searchParams.set(key, value);\n      }\n    });\n\n    this.setQueryString(searchParams.toString());\n  }\n\n  /**\n   * To force update url based on value in currentLocation object\n   */\n  updateTimeSettings() {\n    const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`;\n\n    this.setHash(hash);\n  }\n\n  // Private Methods\n\n  /**\n   * @private\n   * Create currentLocation object\n   *\n   * @param {string} pathString USVString representing relative URL.\n   *\n   * @returns {CurrentLocation} A {@link CurrentLocation}\n   */\n  createLocation(pathString) {\n    if (pathString[0] !== '/') {\n      pathString = '/' + pathString;\n    }\n\n    let url = new URL(pathString, `${location.protocol}//${location.host}${location.pathname}`);\n\n    return {\n      url: url,\n      path: url.pathname,\n      getQueryString: () => url.search.replace(/^\\?/, ''),\n      params: paramsToObject(url.searchParams)\n    };\n  }\n\n  /**\n   * @private\n   * Compare new and old path and on change emit event 'change:path'\n   *\n   * @param {string} newPath new path of url\n   * @param {string} oldPath old path of url\n   * @returns {boolean} true if path changed, false otherwise\n   */\n  doPathChange(newPath, oldPath) {\n    if (newPath === oldPath) {\n      return false;\n    }\n\n    let route = this.routes.filter((r) => r.matcher.test(newPath))[0];\n    if (route) {\n      route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params);\n    }\n\n    this.openmct.telemetry.abortAllRequests();\n\n    this.emit('change:path', newPath, oldPath);\n\n    return true;\n  }\n\n  /**\n   * @private\n   * Compare new and old params and on change emit event 'change:params'\n   *\n   * @param {Object} newParams new params of url\n   * @param {Object} oldParams old params of url\n   * @returns {boolean} true if params changed, false otherwise\n   */\n  doParamsChange(newParams, oldParams) {\n    if (_.isEqual(newParams, oldParams)) {\n      return false;\n    }\n\n    let changedParams = {};\n    Object.entries(newParams).forEach(([key, value]) => {\n      if (value !== oldParams[key]) {\n        changedParams[key] = value;\n      }\n    });\n    Object.keys(oldParams).forEach((key) => {\n      if (!Object.prototype.hasOwnProperty.call(newParams, key)) {\n        changedParams[key] = undefined;\n      }\n    });\n\n    this.emit('change:params', newParams, oldParams, changedParams);\n    return true;\n  }\n\n  /**\n   * @private\n   * On location change, update currentLocation object and emit appropriate events\n   *\n   * @param {string} pathString USVString representing relative URL.\n   */\n  handleLocationChange(pathString) {\n    let oldLocation = this.currentLocation;\n    let newLocation = this.createLocation(pathString);\n\n    this.currentLocation = newLocation;\n\n    if (!oldLocation) {\n      this.doPathChange(newLocation.path, null);\n      this.doParamsChange(newLocation.params, {});\n\n      return;\n    }\n\n    const pathChanged = this.doPathChange(newLocation.path, oldLocation.path);\n\n    const paramsChanged = this.doParamsChange(newLocation.params, oldLocation.params, pathChanged);\n    if (pathChanged || paramsChanged) {\n      // If either path or parameters have changed, we update the URL in the address bar.\n      this.set(newLocation.path, newLocation.getQueryString());\n    }\n  }\n\n  /**\n   * @private\n   * On hash changed, update currentLocation object and emit appropriate events\n   *\n   * @param {string} hash new hash for url\n   */\n  hashChanged(hash) {\n    this.emit('change:hash', hash);\n    this.handleLocationChange(hash);\n  }\n\n  /**\n   * @private\n   * Set new hash for url\n   *\n   * @param {string} hash new hash for url\n   */\n  setHash(hash) {\n    location.hash = '#' + hash.replace(/#/g, '');\n  }\n\n  /**\n   * @private\n   * Set queryString part of current url\n   *\n   * @param {string} queryString queryString part of url\n   */\n  setQueryString(queryString) {\n    this.handleLocationChange(`${this.currentLocation.path}?${queryString}`);\n  }\n}\n\n/**\n * Convert searchParams into Object\n *\n * @param {URLSearchParams} searchParams queryString part of url\n *\n * @returns {Object}\n */\nfunction paramsToObject(searchParams) {\n  let params = {};\n  for (let [key, value] of searchParams.entries()) {\n    if (params[key]) {\n      if (!Array.isArray(params[key])) {\n        params[key] = [params[key]];\n      }\n\n      params[key].push(value);\n    } else {\n      params[key] = value;\n    }\n  }\n\n  return params;\n}\n\nexport default ApplicationRouter;\n"
  },
  {
    "path": "src/ui/router/ApplicationRouterSpec.js",
    "content": "import { createOpenMct, resetApplicationState } from 'utils/testing';\n\nlet openmct;\nlet element;\nlet child;\nlet appHolder;\nlet resolveFunction;\n\nxdescribe('Application router utility functions', () => {\n  beforeEach((done) => {\n    appHolder = document.createElement('div');\n    appHolder.style.width = '640px';\n    appHolder.style.height = '480px';\n\n    openmct = createOpenMct();\n    openmct.install(openmct.plugins.MyItems());\n\n    element = document.createElement('div');\n    child = document.createElement('div');\n    element.appendChild(child);\n\n    openmct.on('start', () => {\n      resolveFunction = () => {\n        const success = window.location.hash !== null && window.location.hash !== '';\n        if (success) {\n          done();\n        }\n      };\n\n      openmct.router.on('change:hash', resolveFunction);\n      // We have a debounce set to 300ms on setHash, so if we don't flush,\n      // the above resolve function sometimes doesn't fire due to a race condition.\n      openmct.router.setHash.flush();\n      openmct.router.setLocationFromUrl();\n    });\n\n    openmct.start(appHolder);\n\n    document.body.append(appHolder);\n  });\n\n  afterEach(() => {\n    openmct.router.removeListener('change:hash', resolveFunction);\n    appHolder.remove();\n\n    return resetApplicationState(openmct);\n  });\n\n  it('has initial hash when loaded', () => {\n    const success = window.location.hash !== null;\n    expect(success).toBe(true);\n  });\n\n  it('The setSearchParam function sets an individual search parameter in the window location hash', () => {\n    openmct.router.setSearchParam('testParam1', 'testValue1');\n\n    const searchParams = openmct.router.getAllSearchParams();\n    expect(searchParams.get('testParam1')).toBe('testValue1');\n  });\n\n  it('The deleteSearchParam function deletes an individual search parameter in the window location hash', () => {\n    openmct.router.deleteSearchParam('testParam');\n    const searchParams = openmct.router.getAllSearchParams();\n    expect(searchParams.get('testParam')).toBe(null);\n  });\n\n  it('The setSearchParam function sets a multiple individual search parameters in the window location hash', () => {\n    openmct.router.setSearchParam('testParam1', 'testValue1');\n    openmct.router.setSearchParam('testParam2', 'testValue2');\n\n    const searchParams = openmct.router.getAllSearchParams();\n    expect(searchParams.get('testParam1')).toBe('testValue1');\n    expect(searchParams.get('testParam2')).toBe('testValue2');\n  });\n\n  it('The setAllSearchParams function replaces all search parameters in the window location hash', () => {\n    openmct.router.setSearchParam('testParam2', 'updatedtestValue2');\n    openmct.router.setSearchParam('newTestParam3', 'newTestValue3');\n\n    const searchParams = openmct.router.getAllSearchParams();\n    expect(searchParams.get('testParam2')).toBe('updatedtestValue2');\n    expect(searchParams.get('newTestParam3')).toBe('newTestValue3');\n  });\n\n  it('The doPathChange function triggers aborting all requests when doing a path change', () => {\n    const abortSpy = spyOn(openmct.telemetry, 'abortAllRequests');\n    openmct.router.doPathChange('newPath', 'oldPath');\n    expect(abortSpy).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "src/ui/router/Browse.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nclass Browse {\n  /**\n   * @type {number}\n   */\n  #navigateCall = 0;\n  /**\n   * @type {Object?}\n   */\n  #browseObject = null;\n  /**\n   * @type {Function | undefined}\n   */\n  #unobserve = undefined;\n  /**\n   * @type {string | undefined}\n   */\n  #currentObjectPath = undefined;\n  /**\n   * @type {boolean}\n   */\n  #isRoutingInProgress = false;\n  /**\n   * @type {import('../../../openmct').OpenMCT}\n   */\n  #openmct;\n\n  /**\n   *\n   * @param {import('../../../openmct').OpenMCT} openmct\n   */\n  constructor(openmct) {\n    this.#openmct = openmct;\n    this.#openmct.router.route(/^\\/browse\\/?$/, this.#navigateToFirstChildOfRoot.bind(this));\n    this.#openmct.router.route(/^\\/browse\\/(.*)$/, this.#handleBrowseRoute.bind(this));\n    this.#openmct.router.on('change:params', this.#onParamsChanged.bind(this));\n  }\n\n  #onParamsChanged(newParams, oldParams, changed) {\n    if (this.#isRoutingInProgress) {\n      return;\n    }\n\n    if (changed.view && this.#browseObject) {\n      const provider = this.#openmct.objectViews.getByProviderKey(changed.view);\n      this.#viewObject(this.#browseObject, provider);\n    }\n  }\n\n  #viewObject(object, viewProvider) {\n    this.#currentObjectPath = this.#openmct.router.path;\n\n    this.#openmct.layout.$refs.browseObject.show(\n      object,\n      viewProvider.key,\n      true,\n      this.#currentObjectPath\n    );\n    this.#openmct.layout.$refs.browseBar.domainObject = object;\n    this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;\n  }\n\n  #handleBrowseObjectUpdate(newObject) {\n    this.#openmct.layout.$refs.browseBar.domainObject = newObject;\n\n    if (typeof newObject.name === 'string' && newObject.name !== document.title) {\n      document.title = newObject.name;\n    }\n  }\n\n  async #navigateToPath(path, currentViewKey) {\n    this.#navigateCall++;\n    const currentNavigation = this.#navigateCall;\n\n    if (this.#unobserve) {\n      this.#unobserve();\n      this.#unobserve = undefined;\n    }\n    path = decodeURIComponent(path);\n    if (!Array.isArray(path)) {\n      path = path.split('/');\n    }\n\n    let objects = await this.#pathToObjects(path);\n    if (currentNavigation !== this.#navigateCall) {\n      return; // Prevent race.\n    }\n    this.#isRoutingInProgress = false;\n    objects = objects.reverse();\n    this.#openmct.router.path = objects;\n    this.#browseObject = objects[0];\n    this.#openmct.router.emit('afterNavigation');\n    this.#openmct.layout.$refs.browseBar.domainObject = this.#browseObject;\n    if (!this.#browseObject) {\n      this.#openmct.layout.$refs.browseObject.clear();\n      return;\n    }\n    document.title = this.#browseObject.name; //change document title to current object in main view\n    this.#unobserve = this.#openmct.objects.observe(\n      this.#browseObject,\n      '*',\n      this.#handleBrowseObjectUpdate.bind(this)\n    );\n\n    if (!currentViewKey) {\n      currentViewKey = this.#getPreferredViewForObjectType(this.#browseObject);\n    }\n\n    const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);\n    if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {\n      this.#viewObject(this.#browseObject, currentProvider);\n      return;\n    }\n    const routerPath = this.#openmct.router.path;\n    const retrievedObjectViews = this.#openmct.objectViews.get(this.#browseObject, routerPath);\n    const defaultProvider = retrievedObjectViews?.[0];\n    if (defaultProvider) {\n      this.#openmct.router.updateParams({ view: defaultProvider.key });\n    } else {\n      this.#openmct.router.updateParams({ view: undefined });\n      this.#openmct.layout.$refs.browseObject.clear();\n    }\n  }\n\n  #pathToObjects(path) {\n    return Promise.all(\n      path.map((keyString) => {\n        const identifier = this.#openmct.objects.parseKeyString(keyString);\n        return this.#openmct.objects.supportsMutation(identifier)\n          ? this.#openmct.objects.getMutable(identifier)\n          : this.#openmct.objects.get(identifier);\n      })\n    );\n  }\n\n  #getPreferredViewForObjectType(obj) {\n    const storedViewPrefs =\n      JSON.parse(window.localStorage.getItem('openmct-stored-view-prefs')) || {};\n    return storedViewPrefs[obj.type] ? storedViewPrefs[obj.type] : undefined;\n  }\n\n  async #navigateToFirstChildOfRoot() {\n    try {\n      const rootObject = await this.#openmct.objects.get('ROOT');\n      const composition = this.#openmct.composition.get(rootObject);\n      if (!composition) {\n        return;\n      }\n\n      const children = await composition.load();\n      const lastChild = children[children.length - 1];\n      if (lastChild) {\n        const lastChildId = this.#openmct.objects.makeKeyString(lastChild.identifier);\n        this.#openmct.router.setPath(`#/browse/${lastChildId}`);\n      }\n    } catch (e) {\n      console.error(e);\n    }\n  }\n\n  #clearMutationListeners() {\n    if (this.#openmct.router.path) {\n      this.#openmct.router.path.forEach((pathObject) => {\n        if (pathObject.isMutable) {\n          this.#openmct.objects.destroyMutable(pathObject);\n        }\n      });\n    }\n  }\n\n  #handleBrowseRoute(path, results, params) {\n    this.#isRoutingInProgress = true;\n    const navigatePath = results[1];\n    this.#clearMutationListeners();\n    this.#navigateToPath(navigatePath, params.view);\n  }\n}\n\nexport default Browse;\n"
  },
  {
    "path": "src/ui/toolbar/ToolbarContainer.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div role=\"menubar\" class=\"c-toolbar\">\n    <div class=\"c-toolbar__element-controls\">\n      <component\n        :is=\"item.control\"\n        v-for=\"(item, index) in primaryStructure\"\n        :key=\"index\"\n        :options=\"item\"\n        @change=\"updateObjectValue\"\n        @click=\"triggerMethod(item, $event)\"\n      />\n    </div>\n    <div class=\"c-toolbar__dimensions-and-controls\">\n      <component\n        :is=\"item.control\"\n        v-for=\"(item, index) in secondaryStructure\"\n        :key=\"index\"\n        :options=\"item\"\n        @change=\"updateObjectValue\"\n        @click=\"triggerMethod(item, $event)\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport _ from 'lodash';\n\nimport ToolbarButton from './components/ToolbarButton.vue';\nimport ToolbarCheckbox from './components/ToolbarCheckbox.vue';\nimport ToolbarColorPicker from './components/ToolbarColorPicker.vue';\nimport ToolbarInput from './components/ToolbarInput.vue';\nimport ToolbarMenu from './components/ToolbarMenu.vue';\nimport ToolbarSelectMenu from './components/ToolbarSelectMenu.vue';\nimport ToolbarSeparator from './components/ToolbarSeparator.vue';\nimport ToolbarToggleButton from './components/ToolbarToggleButton.vue';\n\nexport default {\n  components: {\n    ToolbarButton,\n    ToolbarColorPicker,\n    ToolbarCheckbox,\n    ToolbarInput,\n    ToolbarMenu,\n    ToolbarSelectMenu,\n    ToolbarSeparator,\n    ToolbarToggleButton\n  },\n  inject: ['openmct'],\n  data: function () {\n    return {\n      structure: []\n    };\n  },\n  computed: {\n    primaryStructure() {\n      return this.structure.filter((item) => !item.secondary);\n    },\n    secondaryStructure() {\n      return this.structure.filter((item) => item.secondary);\n    }\n  },\n  mounted() {\n    this.openmct.selection.on('change', this.handleSelection);\n    this.handleSelection(this.openmct.selection.get());\n\n    // Toolbars may change when edit mode is enabled/disabled, so listen\n    // for edit mode changes and update toolbars if necessary.\n    this.openmct.editor.on('isEditing', this.handleEditing);\n  },\n  beforeUnmount() {\n    this.openmct.selection.off('change', this.handleSelection);\n    this.openmct.editor.off('isEditing', this.handleEditing);\n    this.removeListeners();\n  },\n  methods: {\n    handleSelection(selection) {\n      this.removeListeners();\n      this.domainObjectsById = {};\n\n      if (selection.length === 0 || !selection[0][0]) {\n        this.structure = [];\n\n        return;\n      }\n\n      let structure = this.openmct.toolbars.get(selection) || [];\n      this.structure = structure.map((toolbarItem) => {\n        let domainObject = toolbarItem.domainObject;\n        let formKeys = [];\n        toolbarItem.control = 'toolbar-' + toolbarItem.control;\n\n        if (toolbarItem.dialog) {\n          toolbarItem.dialog.sections.forEach((section) => {\n            section.rows.forEach((row) => {\n              formKeys.push(row.key);\n            });\n          });\n          toolbarItem.formKeys = formKeys;\n        }\n\n        if (domainObject) {\n          toolbarItem.value = this.getValue(domainObject, toolbarItem);\n          this.registerListener(domainObject);\n        }\n\n        return toolbarItem;\n      });\n    },\n    registerListener(domainObject) {\n      let id = this.openmct.objects.makeKeyString(domainObject.identifier);\n\n      if (!this.domainObjectsById[id]) {\n        this.domainObjectsById[id] = {\n          domainObject: domainObject\n        };\n        this.observeObject(domainObject, id);\n      }\n    },\n    observeObject(domainObject, id) {\n      let unobserveObject = this.openmct.objects.observe(\n        domainObject,\n        '*',\n        function (newObject) {\n          this.domainObjectsById[id].newObject = newObject;\n          this.updateToolbarAfterMutation();\n        }.bind(this)\n      );\n      this.unObserveObjects.push(unobserveObject);\n    },\n    updateToolbarAfterMutation() {\n      this.structure = this.structure.map((toolbarItem) => {\n        let domainObject = toolbarItem.domainObject;\n\n        if (domainObject) {\n          let id = this.openmct.objects.makeKeyString(domainObject.identifier);\n          let newObject = this.domainObjectsById[id].newObject;\n\n          if (newObject) {\n            toolbarItem.domainObject = newObject;\n            toolbarItem.value = this.getValue(newObject, toolbarItem);\n          }\n        }\n\n        return toolbarItem;\n      });\n\n      Object.values(this.domainObjectsById).forEach(function (tracker) {\n        if (tracker.newObject) {\n          tracker.domainObject = tracker.newObject;\n          delete tracker.newObject;\n        }\n      });\n    },\n    getValue(domainObject, toolbarItem) {\n      let value = undefined;\n      let applicableSelectedItems = toolbarItem.applicableSelectedItems;\n\n      if (!applicableSelectedItems && !toolbarItem.property) {\n        return value;\n      }\n\n      if (toolbarItem.formKeys) {\n        value = this.getFormValue(domainObject, toolbarItem);\n      } else {\n        let values = [];\n        if (applicableSelectedItems) {\n          applicableSelectedItems.forEach((selectionPath) => {\n            values.push(this.getPropertyValue(domainObject, toolbarItem, selectionPath));\n          });\n        } else {\n          values.push(this.getPropertyValue(domainObject, toolbarItem));\n        }\n\n        // If all values are the same, use it, otherwise mark the item as non-specific.\n        if (values.every((val) => val === values[0])) {\n          value = values[0];\n          toolbarItem.nonSpecific = false;\n        } else {\n          toolbarItem.nonSpecific = true;\n        }\n      }\n\n      return value;\n    },\n    getPropertyValue(domainObject, toolbarItem, selectionPath, formKey) {\n      let property = this.getItemProperty(toolbarItem, selectionPath);\n\n      if (formKey) {\n        property = property + '.' + formKey;\n      }\n\n      return _.get(domainObject, property);\n    },\n    getFormValue(domainObject, toolbarItem) {\n      let value = {};\n      let values = {};\n\n      toolbarItem.formKeys.forEach((key) => {\n        values[key] = [];\n\n        if (toolbarItem.applicableSelectedItems) {\n          toolbarItem.applicableSelectedItems.forEach((selectionPath) => {\n            values[key].push(this.getPropertyValue(domainObject, toolbarItem, selectionPath, key));\n          });\n        } else {\n          values[key].push(this.getPropertyValue(domainObject, toolbarItem, undefined, key));\n        }\n      });\n\n      for (let key in values) {\n        if (values[key].every((val) => val === values[key][0])) {\n          value[key] = values[key][0];\n          toolbarItem.nonSpecific = false;\n        } else {\n          toolbarItem.nonSpecific = true;\n\n          return {};\n        }\n      }\n\n      return value;\n    },\n    getItemProperty(item, selectionPath) {\n      return typeof item.property === 'function' ? item.property(selectionPath) : item.property;\n    },\n    removeListeners() {\n      if (this.unObserveObjects) {\n        this.unObserveObjects.forEach((unObserveObject) => {\n          unObserveObject();\n        });\n      }\n\n      this.unObserveObjects = [];\n    },\n    updateObjectValue(value, item) {\n      let changedItemId = this.openmct.objects.makeKeyString(item.domainObject.identifier);\n      this.structure = this.structure.map((toolbarItem) => {\n        if (toolbarItem.domainObject) {\n          let id = this.openmct.objects.makeKeyString(toolbarItem.domainObject.identifier);\n\n          if (changedItemId === id && _.isEqual(toolbarItem, item)) {\n            toolbarItem.value = value;\n          }\n        }\n\n        return toolbarItem;\n      });\n\n      // If value is an object, iterate the toolbar structure and mutate all keys in form.\n      // Otherwise, mutate the property.\n      if (value === Object(value)) {\n        this.structure.forEach((s) => {\n          if (s.formKeys) {\n            s.formKeys.forEach((key) => {\n              if (item.applicableSelectedItems) {\n                item.applicableSelectedItems.forEach((selectionPath) => {\n                  this.mutateObject(item, value[key], selectionPath, key);\n                });\n              } else {\n                this.mutateObject(item, value[key], undefined, key);\n              }\n            });\n          }\n        });\n      } else {\n        if (item.applicableSelectedItems) {\n          item.applicableSelectedItems.forEach((selectionPath) => {\n            this.mutateObject(item, value, selectionPath);\n          });\n        } else {\n          this.mutateObject(item, value);\n        }\n      }\n    },\n    mutateObject(item, value, selectionPath, formKey) {\n      let property = this.getItemProperty(item, selectionPath);\n\n      if (formKey) {\n        property = property + '.' + formKey;\n      }\n\n      this.openmct.objects.mutate(item.domainObject, property, value);\n    },\n    triggerMethod(item, event) {\n      if (item.method) {\n        item.method({ ...event });\n      }\n    },\n    handleEditing(isEditing) {\n      this.handleSelection(this.openmct.selection.get());\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarButton.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-ctrl-wrapper\">\n    <div\n      ref=\"button\"\n      class=\"c-icon-button\"\n      role=\"menuitem\"\n      :aria-label=\"options.title\"\n      :title=\"options.title\"\n      :class=\"{\n        [options.icon]: true,\n        'c-icon-button--caution': options.modifier === 'caution',\n        'c-icon-button--mixed': nonSpecific\n      }\"\n      @click=\"onClick\"\n    >\n      <span v-if=\"options.label\" class=\"c-icon-button__label\">\n        {{ options.label }}\n      </span>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  inject: ['openmct'],\n  props: {\n    options: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['change', 'click'],\n  computed: {\n    nonSpecific() {\n      return this.options.nonSpecific === true;\n    }\n  },\n  methods: {\n    onClick(event) {\n      const self = this;\n\n      if ((self.options.isEditing === undefined || self.options.isEditing) && self.options.dialog) {\n        this.updateFormStructure();\n\n        self.openmct.forms\n          .showForm(self.options.dialog)\n          .then((changes) => {\n            self.$emit('change', { ...changes }, self.options);\n          })\n          .catch((e) => {\n            // canceled, do nothing\n          });\n      }\n\n      self.$emit('click', self.options);\n    },\n    updateFormStructure() {\n      if (!this.options.value) {\n        return;\n      }\n\n      Object.entries(this.options.value).forEach(([key, value]) => {\n        this.options.dialog.sections.forEach((section) => {\n          section.rows.forEach((row) => {\n            if (row.key === key) {\n              row.value = value;\n            }\n          });\n        });\n      });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarCheckbox.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-custom-checkbox\">\n    <input\n      :id=\"uid\"\n      type=\"checkbox\"\n      :name=\"options.name\"\n      :checked=\"options.value\"\n      :disabled=\"options.disabled\"\n      @change=\"onChange\"\n    />\n\n    <label :for=\"uid\">\n      <div class=\"c-custom-checkbox__box\"></div>\n      <div class=\"c-custom-checkbox__label-text\">\n        {{ options.name }}\n      </div>\n    </label>\n  </div>\n</template>\n\n<script>\nlet uniqueId = 100;\n\nexport default {\n  props: {\n    options: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['change'],\n  data() {\n    uniqueId++;\n\n    return {\n      uid: `mct-checkbox-id-${uniqueId}`\n    };\n  },\n  methods: {\n    onChange(event) {\n      this.$emit('change', event.target.checked, { ...this.options });\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarColorPicker.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-ctrl-wrapper\">\n    <button\n      class=\"c-icon-button c-icon-button--swatched\"\n      :class=\"[options.icon, { 'c-icon-button--mixed': nonSpecific }]\"\n      :title=\"options.title\"\n      :aria-label=\"options.title\"\n      @click=\"handleClick\"\n    >\n      <div\n        class=\"c-swatch\"\n        :style=\"{ background: options.value }\"\n        role=\"img\"\n        aria-label=\"None\"\n      ></div>\n    </button>\n    <div v-if=\"open\" class=\"c-menu c-palette c-palette--color\">\n      <div\n        v-if=\"!options.preventNone\"\n        class=\"c-palette__item-none\"\n        role=\"grid\"\n        aria-label=\"No Style\"\n        @click=\"select({ value: 'transparent' })\"\n      >\n        <div class=\"c-palette__item\"></div>\n        None\n      </div>\n      <div class=\"c-palette__items\">\n        <div\n          v-for=\"(color, index) in colorPalette\"\n          :key=\"index\"\n          role=\"gridcell\"\n          class=\"c-palette__item\"\n          :style=\"{ background: color.value }\"\n          :title=\"color.value\"\n          :aria-label=\"color.value\"\n          @click=\"select(color)\"\n        ></div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport toggleMixin from '../../mixins/toggle-mixin.js';\n\nexport default {\n  mixins: [toggleMixin],\n  props: {\n    options: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['change'],\n  data() {\n    return {\n      colorPalette: [\n        { value: '#000000' },\n        { value: '#434343' },\n        { value: '#666666' },\n        { value: '#999999' },\n        { value: '#b7b7b7' },\n        { value: '#cccccc' },\n        { value: '#d9d9d9' },\n        { value: '#efefef' },\n        { value: '#f3f3f3' },\n        { value: '#ffffff' },\n        { value: '#980000' },\n        { value: '#ff0000' },\n        { value: '#ff9900' },\n        { value: '#ffff00' },\n        { value: '#00ff00' },\n        { value: '#00ffff' },\n        { value: '#4a86e8' },\n        { value: '#0000ff' },\n        { value: '#9900ff' },\n        { value: '#ff00ff' },\n        { value: '#e6b8af' },\n        { value: '#f4cccc' },\n        { value: '#fce5cd' },\n        { value: '#fff2cc' },\n        { value: '#d9ead3' },\n        { value: '#d0e0e3' },\n        { value: '#c9daf8' },\n        { value: '#cfe2f3' },\n        { value: '#d9d2e9' },\n        { value: '#ead1dc' },\n        { value: '#dd7e6b' },\n        { value: '#dd7e6b' },\n        { value: '#f9cb9c' },\n        { value: '#ffe599' },\n        { value: '#b6d7a8' },\n        { value: '#a2c4c9' },\n        { value: '#a4c2f4' },\n        { value: '#9fc5e8' },\n        { value: '#b4a7d6' },\n        { value: '#d5a6bd' },\n        { value: '#cc4125' },\n        { value: '#e06666' },\n        { value: '#f6b26b' },\n        { value: '#ffd966' },\n        { value: '#93c47d' },\n        { value: '#76a5af' },\n        { value: '#6d9eeb' },\n        { value: '#6fa8dc' },\n        { value: '#8e7cc3' },\n        { value: '#c27ba0' },\n        { value: '#a61c00' },\n        { value: '#cc0000' },\n        { value: '#e69138' },\n        { value: '#f1c232' },\n        { value: '#6aa84f' },\n        { value: '#45818e' },\n        { value: '#3c78d8' },\n        { value: '#3d85c6' },\n        { value: '#674ea7' },\n        { value: '#a64d79' },\n        { value: '#85200c' },\n        { value: '#990000' },\n        { value: '#b45f06' },\n        { value: '#bf9000' },\n        { value: '#38761d' },\n        { value: '#134f5c' },\n        { value: '#1155cc' },\n        { value: '#0b5394' },\n        { value: '#351c75' },\n        { value: '#741b47' },\n        { value: '#5b0f00' },\n        { value: '#660000' },\n        { value: '#783f04' },\n        { value: '#7f6000' },\n        { value: '#274e13' },\n        { value: '#0c343d' },\n        { value: '#1c4587' },\n        { value: '#073763' },\n        { value: '#20124d' },\n        { value: '#4c1130' }\n      ]\n    };\n  },\n  computed: {\n    nonSpecific() {\n      return this.options.nonSpecific === true;\n    }\n  },\n  methods: {\n    select(color) {\n      if (color.value !== this.options.value) {\n        this.$emit('change', color.value, this.options);\n      }\n    },\n    handleClick(event) {\n      if (this.options.isEditing === undefined || this.options.isEditing) {\n        this.toggle(event);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarInput.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-labeled-input\" :title=\"options.title\">\n    <label :for=\"uid\">\n      <div class=\"c-labeled-input__label\">{{ options.label }}</div>\n    </label>\n    <input v-bind=\"options.attrs\" :id=\"uid\" :type=\"options.type\" :value=\"options.value\" />\n  </div>\n</template>\n\n<script>\nlet inputUniqueId = 100;\n\nexport default {\n  props: {\n    options: {\n      type: Object,\n      required: true,\n      validator(value) {\n        return ['number', 'text'].indexOf(value.type) !== -1;\n      }\n    }\n  },\n  emits: ['change'],\n  data() {\n    inputUniqueId++;\n\n    return {\n      uid: `mct-input-id-${inputUniqueId}`\n    };\n  },\n  mounted() {\n    if (this.options.type === 'number') {\n      this.$el.addEventListener('input', this.onInput);\n    } else {\n      this.$el.addEventListener('change', this.onChange);\n    }\n  },\n  beforeUnmount() {\n    if (this.options.type === 'number') {\n      this.$el.removeEventListener('input', this.onInput);\n    } else {\n      this.$el.removeEventListener('change', this.onChange);\n    }\n  },\n  methods: {\n    onChange(event) {\n      this.$emit('change', event.target.value, this.options);\n    },\n    onInput(event) {\n      this.$emit('change', event.target.valueAsNumber, this.options);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarMenu.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-ctrl-wrapper\">\n    <div\n      class=\"c-icon-button c-icon-button--menu\"\n      :class=\"options.icon\"\n      :title=\"options.title\"\n      :aria-label=\"options.label\"\n      role=\"button\"\n      @click=\"toggle\"\n    >\n      <span v-if=\"options.label\" class=\"c-icon-button__label\">\n        {{ options.label }}\n      </span>\n    </div>\n    <div v-if=\"open\" class=\"c-menu\" role=\"menu\">\n      <ul>\n        <li\n          v-for=\"(option, index) in options.options\"\n          :key=\"index\"\n          :class=\"option.class\"\n          role=\"menuitem\"\n          :aria-labelledby=\"`${option.name}-menuitem-label`\"\n          @click=\"onClick(option)\"\n        >\n          <span :id=\"`${option.name}-menuitem-label`\">\n            {{ option.name }}\n          </span>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport toggle from '../../mixins/toggle-mixin.js';\nexport default {\n  mixins: [toggle],\n  props: {\n    options: {\n      type: Object,\n      required: true,\n      validator(value) {\n        // must pass valid options array.\n        return Array.isArray(value.options) && value.options.every((o) => o.name);\n      }\n    }\n  },\n  emits: ['click'],\n  methods: {\n    onClick(option) {\n      this.$emit('click', option);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarSelectMenu.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-ctrl-wrapper\">\n    <div\n      class=\"c-icon-button c-icon-button--menu\"\n      :class=\"[options.icon, { 'c-click-icon--mixed': nonSpecific }]\"\n      :title=\"options.title\"\n      @click=\"toggle\"\n    >\n      <div class=\"c-button__label\">\n        {{ selectedName }}\n      </div>\n    </div>\n    <div v-if=\"open\" class=\"c-menu\">\n      <ul>\n        <li v-for=\"option in options.options\" :key=\"option.value\" @click=\"select(option)\">\n          {{ option.name || option.value }}\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport toggleMixin from '../../mixins/toggle-mixin.js';\n\nexport default {\n  mixins: [toggleMixin],\n  props: {\n    options: {\n      type: Object,\n      required: true,\n      validator(value) {\n        // must pass valid options array.\n        return Array.isArray(value.options) && value.options.every((o) => o.value);\n      }\n    }\n  },\n  emits: ['change'],\n  computed: {\n    selectedName() {\n      let selectedOption = this.options.options.filter((o) => o.value === this.options.value)[0];\n      if (selectedOption) {\n        return selectedOption.name || selectedOption.value;\n      }\n\n      // If no selected option, then options are non-specific\n      return '??';\n    },\n    nonSpecific() {\n      return this.options.nonSpecific === true;\n    }\n  },\n  methods: {\n    select(option) {\n      if (this.options.value === option.value) {\n        return;\n      }\n\n      this.$emit('change', option.value, this.options);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarSeparator.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div role=\"separator\" class=\"c-toolbar__separator\"></div>\n</template>\n\n<script>\nexport default {\n  props: {\n    options: {\n      type: Object,\n      required: true\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/ToolbarToggleButton.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <div class=\"c-ctrl-wrapper\">\n    <div\n      class=\"c-icon-button\"\n      :title=\"nextValue.title\"\n      :class=\"[nextValue.icon, { 'c-icon-button--mixed': nonSpecific }]\"\n      @click=\"cycle\"\n    >\n      <div v-if=\"nextValue.label\" class=\"c-icon-button__label\">\n        {{ nextValue.label }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    options: {\n      type: Object,\n      required: true\n    }\n  },\n  emits: ['change'],\n  computed: {\n    nextValue() {\n      let currentValue = this.options.options.filter((v) => this.options.value === v.value)[0];\n      let nextIndex = this.options.options.indexOf(currentValue) + 1;\n      if (nextIndex >= this.options.options.length) {\n        nextIndex = 0;\n      }\n\n      return this.options.options[nextIndex];\n    },\n    nonSpecific() {\n      return this.options.nonSpecific === true;\n    }\n  },\n  methods: {\n    cycle() {\n      if (this.options.isEditing === undefined || this.options.isEditing) {\n        this.$emit('change', this.nextValue.value, this.options);\n      }\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/ui/toolbar/components/toolbar-checkbox.scss",
    "content": ".c-custom-checkbox {\n  $d: 14px;\n  display: flex;\n  align-items: center;\n\n  label {\n    @include userSelectNone();\n    display: flex;\n    align-items: center;\n  }\n\n  &__box {\n    @include nice-input();\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    line-height: $d;\n    width: $d;\n    height: $d;\n    margin-right: $interiorMarginSm;\n  }\n\n  input {\n    opacity: 0;\n    position: absolute;\n\n    &:checked + label > .c-custom-checkbox__box {\n      background: $colorKeyBg;\n      &:before {\n        color: $colorKeyFg;\n        content: $glyph-icon-check;\n        font-family: symbolsfont;\n        font-size: 0.6em;\n      }\n    }\n\n    &:not(:disabled) + label {\n      cursor: pointer;\n    }\n\n    &:disabled + label {\n      opacity: 0.5;\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/agent/Agent.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n/**\n * The query service handles calls for browser and userAgent\n * info using a comparison between the userAgent and key\n * device names\n * @constructor\n * @param window the browser object model\n */\nexport default class Agent {\n  constructor(window) {\n    const userAgent = window.navigator.userAgent;\n    const matches = userAgent.match(/iPad|iPhone|Android/i) || [];\n\n    this.userAgent = userAgent;\n    this.mobileName = matches[0];\n    this.window = window;\n    this.touchEnabled = window.ontouchstart !== undefined;\n  }\n  /**\n   * Check if the user is on a mobile device.\n   * @returns {boolean} true on mobile\n   */\n  isMobile() {\n    return Boolean(this.mobileName);\n  }\n  /**\n   * Check if the user is on a phone-sized mobile device.\n   * @returns {boolean} true on a phone\n   */\n  isPhone() {\n    if (this.isMobile()) {\n      if (this.isAndroidTablet()) {\n        return false;\n      } else if (this.mobileName === 'iPad') {\n        return false;\n      } else {\n        return true;\n      }\n    } else {\n      return false;\n    }\n  }\n  /**\n   * Check if the user is on a tablet sized android device\n   * @returns {boolean | undefined} true on an android tablet\n   */\n  isAndroidTablet() {\n    if (this.mobileName === 'Android') {\n      if (this.isPortrait() && this.window.innerWidth >= 768) {\n        return true;\n      } else if (this.isLandscape() && this.window.innerHeight >= 768) {\n        return true;\n      }\n    } else {\n      return false;\n    }\n  }\n  /**\n   * Check if the user is on a tablet-sized mobile device.\n   * @returns {boolean | undefined} true on a tablet\n   */\n  isTablet() {\n    return (\n      (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') ||\n      (this.isMobile() && this.isAndroidTablet())\n    );\n  }\n  /**\n   * Check if the user's device is in a portrait-style\n   * orientation (display width is narrower than display height.)\n   * @returns {boolean} true in portrait mode\n   */\n  isPortrait() {\n    const { screen } = this.window;\n    const hasScreenOrientation =\n      screen && Object.prototype.hasOwnProperty.call(screen, 'orientation');\n    const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation');\n\n    if (hasScreenOrientation) {\n      return screen.orientation.type.includes('portrait');\n    } else if (hasWindowOrientation) {\n      // Use window.orientation API if available (e.g. Safari mobile)\n      // which returns [-90, 0, 90, 180] based on device orientation.\n      const { orientation } = this.window;\n\n      return Math.abs(orientation / 90) % 2 === 0;\n    } else {\n      return this.window.innerWidth < this.window.innerHeight;\n    }\n  }\n  /**\n   * Check if the user's device is in a landscape-style\n   * orientation (display width is greater than display height.)\n   * @returns {boolean} true in landscape mode\n   */\n  isLandscape() {\n    return !this.isPortrait();\n  }\n  /**\n   * Check if the user's device supports a touch interface.\n   * @returns {boolean} true if touch is supported\n   */\n  isTouch() {\n    return this.touchEnabled;\n  }\n  /**\n   * Check if the user agent matches a certain named device,\n   * as indicated by checking for a case-insensitive substring\n   * match.\n   * @param {string} name the name to check for\n   * @returns {boolean} true if the user agent includes that name\n   */\n  isBrowser(name) {\n    name = name.toLowerCase();\n\n    return this.userAgent.toLowerCase().indexOf(name) !== -1;\n  }\n}\n"
  },
  {
    "path": "src/utils/agent/AgentSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport Agent from './Agent.js';\n\nconst TEST_USER_AGENTS = {\n  DESKTOP:\n    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36',\n  IPAD: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53',\n  IPHONE:\n    'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53'\n};\n\ndescribe('The Agent', function () {\n  let testWindow;\n  let agent;\n\n  beforeEach(function () {\n    testWindow = {\n      innerWidth: 640,\n      innerHeight: 480,\n      navigator: {\n        userAgent: TEST_USER_AGENTS.DESKTOP\n      }\n    };\n  });\n\n  it('recognizes desktop devices as non-mobile', function () {\n    testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;\n    agent = new Agent(testWindow);\n    expect(agent.isMobile()).toBeFalsy();\n    expect(agent.isPhone()).toBeFalsy();\n    expect(agent.isTablet()).toBeFalsy();\n  });\n\n  it('detects iPhones', function () {\n    testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;\n    agent = new Agent(testWindow);\n    expect(agent.isMobile()).toBeTruthy();\n    expect(agent.isPhone()).toBeTruthy();\n    expect(agent.isTablet()).toBeFalsy();\n  });\n\n  it('detects iPads', function () {\n    testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;\n    agent = new Agent(testWindow);\n    expect(agent.isMobile()).toBeTruthy();\n    expect(agent.isPhone()).toBeFalsy();\n    expect(agent.isTablet()).toBeTruthy();\n  });\n\n  it('detects display orientation by innerHeight and innerWidth', function () {\n    agent = new Agent(testWindow);\n    testWindow.innerWidth = 1024;\n    testWindow.innerHeight = 400;\n    expect(agent.isPortrait()).toBeFalsy();\n    expect(agent.isLandscape()).toBeTruthy();\n    testWindow.innerWidth = 400;\n    testWindow.innerHeight = 1024;\n    expect(agent.isPortrait()).toBeTruthy();\n    expect(agent.isLandscape()).toBeFalsy();\n  });\n\n  it('detects display orientation by screen.orientation', function () {\n    agent = new Agent(testWindow);\n    testWindow.screen = {\n      orientation: {\n        type: 'landscape-primary'\n      }\n    };\n    expect(agent.isPortrait()).toBeFalsy();\n    expect(agent.isLandscape()).toBeTruthy();\n    testWindow.screen = {\n      orientation: {\n        type: 'portrait-primary'\n      }\n    };\n    expect(agent.isPortrait()).toBeTruthy();\n    expect(agent.isLandscape()).toBeFalsy();\n  });\n\n  it('detects display orientation by window.orientation', function () {\n    agent = new Agent(testWindow);\n    testWindow.orientation = 90;\n    expect(agent.isPortrait()).toBeFalsy();\n    expect(agent.isLandscape()).toBeTruthy();\n    testWindow.orientation = 0;\n    expect(agent.isPortrait()).toBeTruthy();\n    expect(agent.isLandscape()).toBeFalsy();\n  });\n\n  it('detects touch support', function () {\n    testWindow.ontouchstart = null;\n    expect(new Agent(testWindow).isTouch()).toBe(true);\n    delete testWindow.ontouchstart;\n    expect(new Agent(testWindow).isTouch()).toBe(false);\n  });\n\n  it('allows for checking browser type', function () {\n    testWindow.navigator.userAgent = 'Chromezilla Safarifox';\n    agent = new Agent(testWindow);\n    expect(agent.isBrowser('Chrome')).toBe(true);\n    expect(agent.isBrowser('Firefox')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/utils/clipboard.js",
    "content": "class Clipboard {\n  updateClipboard(newClip) {\n    // return promise\n    return navigator.clipboard.writeText(newClip);\n  }\n\n  readClipboard() {\n    // return promise\n    return navigator.clipboard.readText();\n  }\n}\n\nexport default new Clipboard();\n"
  },
  {
    "path": "src/utils/clock/DefaultClock.js",
    "content": "/*****************************************************************************\n * Open MCT Web, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT Web is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT Web includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { EventEmitter } from 'eventemitter3';\n\n/**\n * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the\n * application based on values provided by a ticking clock.\n * @constructor\n */\nexport default class DefaultClock extends EventEmitter {\n  constructor() {\n    super();\n\n    this.key = 'clock';\n    this.cssClass = 'icon-clock';\n    this.name = 'Clock';\n    this.description = 'A default clock for openmct.';\n  }\n\n  tick(tickValue) {\n    this.emit('tick', tickValue);\n    this.lastTick = tickValue;\n  }\n\n  /**\n   * Register a listener for the clock. When it ticks, the\n   * clock will provide the time from the configured endpoint\n   *\n   * @override\n   * @param {string | symbol} event the event to listen for\n   * @param {Function} fn the function to call when the event is emitted\n   * @param {*} [context] the context to use for the function call\n   * @returns {this} a function for deregistering the provided listener\n   */\n  on(event, fn, context) {\n    super.on(event, fn, context);\n\n    if (this.listeners(event).length === 1) {\n      this.start();\n    }\n\n    return this;\n  }\n\n  /**\n   * Register a listener for the clock. When it ticks, the\n   * clock will provide the current local system time\n   *\n   * @override\n   * @param {string | symbol} event the event to listen for\n   * @param {Function} [fn] the function to call when the event is emitted\n   * @param {*} [context] the context to use for the function call\n   * @param {boolean} [once]\n   * @returns {this}\n   */\n  off(event, fn, context, once) {\n    super.off(event, fn, context, once);\n\n    if (this.listeners(event).length === 0) {\n      this.stop();\n    }\n\n    return this;\n  }\n\n  stop() {\n    throw new Error(\"Method 'stop()' must be implemented.\");\n  }\n\n  start() {\n    throw new Error(\"Method 'start()' must be implemented.\");\n  }\n\n  /**\n   * @returns {number} The most recent value provided for a clock tick\n   */\n  currentValue() {\n    return this.lastTick;\n  }\n}\n"
  },
  {
    "path": "src/utils/constants.js",
    "content": "export const SupportedViewTypes = [\n  'plot-stacked',\n  'plot-overlay',\n  'bar-graph.view',\n  'time-strip.view',\n  'example.imagery',\n  'timelist.view'\n];\n"
  },
  {
    "path": "src/utils/debounce.js",
    "content": "export default function debounce(func, delay) {\n  let debounceTimer;\n\n  return function (...args) {\n    clearTimeout(debounceTimer);\n    debounceTimer = setTimeout(() => func(...args), delay);\n  };\n}\n"
  },
  {
    "path": "src/utils/duration.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nconst ONE_SECOND = 1000;\nconst ONE_MINUTE = 60 * ONE_SECOND;\nconst ONE_HOUR = ONE_MINUTE * 60;\nconst ONE_DAY = ONE_HOUR * 24;\n\nfunction normalizeAge(num) {\n  const hundredtized = num * 100;\n  const isWhole = hundredtized % 100 === 0;\n\n  return isWhole ? hundredtized / 100 : num;\n}\n\nfunction padLeadingZeros(num, numOfLeadingZeros) {\n  return num.toString().padStart(numOfLeadingZeros, '0');\n}\n\nfunction toDoubleDigits(num) {\n  return padLeadingZeros(num, 2);\n}\n\nfunction toTripleDigits(num) {\n  return padLeadingZeros(num, 3);\n}\n\nfunction addTimeSuffix(value, suffix) {\n  return typeof value === 'number' && value > 0 ? `${value + suffix}` : '';\n}\n\nexport function millisecondsToDHMS(numericDuration) {\n  const ms = numericDuration || 0;\n  const dhms = [\n    addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),\n    addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),\n    addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),\n    addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'),\n    addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), 'ms')\n  ]\n    .filter(Boolean)\n    .join(' ');\n\n  return `${dhms ? '+' : ''} ${dhms}`;\n}\n\n/**\n *\n * @param {number} value\n * @param {Object} options\n * @param {boolean | undefined} options.excludeMilliSeconds\n * @param {boolean | undefined} options.useDayFormat\n * @returns {string}\n */\nexport function getPreciseDuration(\n  value,\n  { excludeMilliSeconds, useDayFormat } = {\n    excludeMilliSeconds: null,\n    useDayFormat: null\n  }\n) {\n  let preciseDuration;\n  const ms = value || 0;\n\n  const duration = [\n    Math.floor(normalizeAge(ms / ONE_DAY)),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),\n    toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))\n  ];\n  if (!excludeMilliSeconds) {\n    duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));\n  }\n\n  if (useDayFormat) {\n    // Format days as XD\n    const days = duration.shift();\n    if (days > 0) {\n      preciseDuration = `${days}D ${duration.join(':')}`;\n    } else {\n      preciseDuration = duration.join(':');\n    }\n  } else {\n    const days = toDoubleDigits(duration.shift());\n    duration.unshift(days);\n    preciseDuration = duration.join(':');\n  }\n\n  return preciseDuration;\n}\n"
  },
  {
    "path": "src/utils/encoding.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nexport function encode_url(url) {\n  return url ? encodeURI(url) : url;\n}\n"
  },
  {
    "path": "src/utils/mount.js",
    "content": "import { createApp, defineComponent } from 'vue';\n\n/**\n * @typedef {import('vue').Component} Component\n */\n\n/**\n * Mounts a Vue component to a DOM element.\n * @param {Component | any} component\n * @param {Object} [options={}] the options object\n * @param {Object} [options.props] the props for the component\n * @param {Object} [options.children] the children for the component\n * @param {HTMLElement} [options.element] the element to mount the component to\n * @returns {Object}\n */\nexport default function mount(component, { props, children, element } = {}) {\n  let el = element;\n  if (!el) {\n    el = document.createElement('div');\n  }\n  /** @type {Component | any} */\n  let vueComponent = defineComponent(component);\n  let app = createApp(vueComponent);\n  let mountedComponentInstance = app.mount(el);\n\n  // eslint-disable-next-line func-style\n  const destroy = () => {\n    app.unmount();\n  };\n\n  return {\n    vNode: {\n      componentInstance: mountedComponentInstance,\n      el: mountedComponentInstance.$el\n    },\n    destroy,\n    el\n  };\n}\n"
  },
  {
    "path": "src/utils/raf.js",
    "content": "export default function raf(callback) {\n  let rendering = false;\n\n  return (...args) => {\n    if (!rendering) {\n      rendering = true;\n\n      requestAnimationFrame(() => {\n        callback(...args);\n        rendering = false;\n      });\n    }\n  };\n}\n"
  },
  {
    "path": "src/utils/rafSpec.js",
    "content": "import raf from './raf.js';\n\ndescribe('The raf utility function', () => {\n  it('Throttles function calls that arrive in quick succession using Request Animation Frame', () => {\n    const unthrottledFunction = jasmine.createSpy('unthrottledFunction');\n    const throttledCallback = jasmine.createSpy('throttledCallback');\n    const throttledFunction = raf(throttledCallback);\n\n    for (let i = 0; i < 10; i++) {\n      unthrottledFunction();\n      throttledFunction();\n    }\n\n    return new Promise((resolve) => {\n      requestAnimationFrame(resolve);\n    }).then(() => {\n      expect(unthrottledFunction).toHaveBeenCalledTimes(10);\n      expect(throttledCallback).toHaveBeenCalledTimes(1);\n    });\n  });\n  it('Only invokes callback once per animation frame', () => {\n    const throttledCallback = jasmine.createSpy('throttledCallback');\n    const throttledFunction = raf(throttledCallback);\n\n    for (let i = 0; i < 10; i++) {\n      throttledFunction();\n    }\n\n    return new Promise((resolve) => {\n      requestAnimationFrame(resolve);\n    })\n      .then(() => {\n        return new Promise((resolve) => {\n          requestAnimationFrame(resolve);\n        });\n      })\n      .then(() => {\n        expect(throttledCallback).toHaveBeenCalledTimes(1);\n      });\n  });\n  it('Invokes callback again if called in subsequent animation frame', () => {\n    const throttledCallback = jasmine.createSpy('throttledCallback');\n    const throttledFunction = raf(throttledCallback);\n\n    for (let i = 0; i < 10; i++) {\n      throttledFunction();\n    }\n\n    return new Promise((resolve) => {\n      requestAnimationFrame(resolve);\n    })\n      .then(() => {\n        for (let i = 0; i < 10; i++) {\n          throttledFunction();\n        }\n\n        return new Promise((resolve) => {\n          requestAnimationFrame(resolve);\n        });\n      })\n      .then(() => {\n        expect(throttledCallback).toHaveBeenCalledTimes(2);\n      });\n  });\n});\n"
  },
  {
    "path": "src/utils/random.js",
    "content": "/**\n * Generates a pseudo-random number based on a seed.\n *\n * @param {number} seed - The seed value to generate the random number.\n * @returns {number} A pseudo-random number between 0 (inclusive) and 1 (exclusive).\n */\nfunction seededRandom(seed = Date.now()) {\n  const x = Math.sin(seed) * 10000;\n  return x - Math.floor(x);\n}\n\nexport { seededRandom };\n"
  },
  {
    "path": "src/utils/sanitization.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nfunction filter__proto__(key, value) {\n  if (key !== '__proto__') {\n    return value;\n  }\n}\n\nexport { filter__proto__ };\n"
  },
  {
    "path": "src/utils/staleness.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nclass StalenessUtils {\n  constructor(openmct, domainObject) {\n    this.openmct = openmct;\n    this.domainObject = domainObject;\n    this.metadata = this.openmct.telemetry.getMetadata(domainObject);\n    this.lastStalenessResponseTime = 0;\n\n    this.setTimeSystem(this.openmct.time.getTimeSystem());\n    this.watchTimeSystem();\n  }\n\n  shouldUpdateStaleness(stalenessResponse, id) {\n    const stalenessResponseTime = this.parseTime(stalenessResponse);\n\n    // Accept latest staleness updates from staleness provider,\n    // regardless of whether the update occurred within time conductor bounds.\n    if (stalenessResponseTime > this.lastStalenessResponseTime) {\n      this.lastStalenessResponseTime = stalenessResponseTime;\n\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  watchTimeSystem() {\n    this.openmct.time.on('timeSystem', this.setTimeSystem, this);\n  }\n\n  unwatchTimeSystem() {\n    this.openmct.time.off('timeSystem', this.setTimeSystem, this);\n  }\n\n  setTimeSystem(timeSystem) {\n    this.timeSystem = timeSystem;\n  }\n\n  parseTime(stalenessResponse) {\n    const metadataValue = this.metadata.value(this.timeSystem.key) ?? {\n      format: this.timeSystem.key\n    };\n    const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);\n\n    const stalenessDatum = {\n      ...stalenessResponse,\n      source: stalenessResponse[this.timeSystem.key]\n    };\n\n    return valueFormatter.parse(stalenessDatum);\n  }\n\n  destroy() {\n    this.unwatchTimeSystem();\n  }\n}\n\nexport default StalenessUtils;\n"
  },
  {
    "path": "src/utils/template/templateHelpers.js",
    "content": "export function convertTemplateToHTML(templateString) {\n  const parser = new DOMParser();\n  const doc = parser.parseFromString(templateString, 'text/html');\n\n  // Create a document fragment to hold the parsed content\n  const fragment = document.createDocumentFragment();\n\n  // Append nodes from the parsed content to the fragment\n  while (doc.body.firstChild) {\n    fragment.appendChild(doc.body.firstChild);\n  }\n\n  // Convert children of the fragment to an array and return\n  return Array.from(fragment.children);\n}\n\nexport function toggleClass(element, className) {\n  if (element.classList.contains(className)) {\n    element.classList.remove(className);\n  } else {\n    element.classList.add(className);\n  }\n}\n"
  },
  {
    "path": "src/utils/template/templateHelpersSpec.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\nimport { toggleClass } from '@/utils/template/templateHelpers';\n\nconst CLASS_AS_NON_EMPTY_STRING = 'class-to-toggle';\nconst CLASS_AS_EMPTY_STRING = '';\nconst CLASS_DEFAULT = CLASS_AS_NON_EMPTY_STRING;\nconst CLASS_SECONDARY = 'another-class-to-toggle';\nconst CLASS_TERTIARY = 'yet-another-class-to-toggle';\n\nconst CLASS_TO_TOGGLE = CLASS_DEFAULT;\n\ndescribe('toggleClass', () => {\n  describe('type checking', () => {\n    const A_DOM_NODE = document.createElement('div');\n    const NOT_A_DOM_NODE = 'not-a-dom-node';\n    describe('errors', () => {\n      it('throws when \"className\" is an empty string', () => {\n        expect(() => toggleClass(A_DOM_NODE, CLASS_AS_EMPTY_STRING)).toThrow();\n      });\n      it('throws when \"element\" is not a DOM node', () => {\n        expect(() => toggleClass(NOT_A_DOM_NODE, CLASS_DEFAULT)).toThrow();\n      });\n    });\n    describe('success', () => {\n      it('does not throw when \"className\" is not an empty string', () => {\n        expect(() => toggleClass(A_DOM_NODE, CLASS_AS_NON_EMPTY_STRING)).not.toThrow();\n      });\n      it('does not throw when \"element\" is a DOM node', () => {\n        expect(() => toggleClass(A_DOM_NODE, CLASS_DEFAULT)).not.toThrow();\n      });\n    });\n  });\n  describe('adding a class', () => {\n    it('adds specified class to an element without any classes', () => {\n      // test case\n      const ELEMENT_WITHOUT_CLASS = document.createElement('div');\n      toggleClass(ELEMENT_WITHOUT_CLASS, CLASS_TO_TOGGLE);\n      // expected\n      const ELEMENT_WITHOUT_CLASS_EXPECTED = document.createElement('div');\n      ELEMENT_WITHOUT_CLASS_EXPECTED.classList.add(CLASS_TO_TOGGLE);\n      expect(ELEMENT_WITHOUT_CLASS).toEqual(ELEMENT_WITHOUT_CLASS_EXPECTED);\n    });\n    it('adds specified class to an element that already has another class', () => {\n      // test case\n      const ELEMENT_WITH_SINGLE_CLASS = document.createElement('div');\n      ELEMENT_WITH_SINGLE_CLASS.classList.add(CLASS_SECONDARY);\n      toggleClass(ELEMENT_WITH_SINGLE_CLASS, CLASS_TO_TOGGLE);\n      // expected\n      const ELEMENT_WITH_SINGLE_CLASS_EXPECTED = document.createElement('div');\n      ELEMENT_WITH_SINGLE_CLASS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TO_TOGGLE);\n      expect(ELEMENT_WITH_SINGLE_CLASS).toEqual(ELEMENT_WITH_SINGLE_CLASS_EXPECTED);\n    });\n    it('adds specified class to an element that already has more than one other classes', () => {\n      // test case\n      const ELEMENT_WITH_MULTIPLE_CLASSES = document.createElement('div');\n      ELEMENT_WITH_MULTIPLE_CLASSES.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY);\n      toggleClass(ELEMENT_WITH_MULTIPLE_CLASSES, CLASS_TO_TOGGLE);\n      // expected\n      const ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED = document.createElement('div');\n      ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED.classList.add(CLASS_SECONDARY);\n      expect(ELEMENT_WITH_MULTIPLE_CLASSES).toEqual(ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED);\n    });\n  });\n  describe('removing a class', () => {\n    it('removes specified class from an element that only has the specified class', () => {\n      // test case\n      const ELEMENT_WITH_ONLY_SPECIFIED_CLASS = document.createElement('div');\n      ELEMENT_WITH_ONLY_SPECIFIED_CLASS.classList.add(CLASS_TO_TOGGLE);\n      toggleClass(ELEMENT_WITH_ONLY_SPECIFIED_CLASS, CLASS_TO_TOGGLE);\n      // expected\n      const ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED = document.createElement('div');\n      ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED.className = '';\n      expect(ELEMENT_WITH_ONLY_SPECIFIED_CLASS).toEqual(ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED);\n    });\n    it('removes specified class from an element that has specified class, and others', () => {\n      // test case\n      const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS = document.createElement('div');\n      ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS.classList.add(\n        CLASS_TO_TOGGLE,\n        CLASS_SECONDARY,\n        CLASS_TERTIARY\n      );\n      toggleClass(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS, CLASS_TO_TOGGLE);\n      // expected\n      const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED = document.createElement('div');\n      ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED.classList.add(\n        CLASS_SECONDARY,\n        CLASS_TERTIARY\n      );\n      expect(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS).toEqual(\n        ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/utils/testing/mockLocalStorage.js",
    "content": "export function mockLocalStorage() {\n  let store;\n\n  beforeEach(() => {\n    spyOn(Storage.prototype, 'getItem').and.callFake(getItem);\n    spyOn(Storage.prototype, 'setItem').and.callFake(setItem);\n    spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem);\n    spyOn(Storage.prototype, 'clear').and.callFake(clear);\n\n    store = {};\n\n    function getItem(key) {\n      return store[key] || null;\n    }\n\n    function setItem(key, value) {\n      store[key] = typeof value === 'string' ? value : JSON.stringify(value);\n    }\n\n    function removeItem(key) {\n      store[key] = undefined;\n      delete store[key];\n    }\n\n    function clear() {\n      store = {};\n    }\n  });\n\n  afterEach(() => {\n    store = undefined;\n  });\n}\n"
  },
  {
    "path": "src/utils/testing.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { MCT } from 'MCT';\nimport { markRaw } from 'vue';\n\nlet nativeFunctions = [];\nlet mockObjects = setMockObjects();\n\nconst EXAMPLE_ROLE = 'flight';\nconst DEFAULT_TIME_OPTIONS = {\n  timeSystemKey: 'utc',\n  bounds: {\n    start: 0,\n    end: 1\n  }\n};\n\nexport function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) {\n  const openmct = markRaw(new MCT());\n  openmct.install(openmct.plugins.LocalStorage());\n  openmct.install(openmct.plugins.UTCTimeSystem());\n  openmct.setAssetPath('/base');\n  openmct.user.setActiveRole(EXAMPLE_ROLE);\n\n  const timeSystemKey = timeSystemOptions.timeSystemKey;\n  const start = timeSystemOptions.bounds.start;\n  const end = timeSystemOptions.bounds.end;\n\n  openmct.time.setTimeSystem(timeSystemKey, {\n    start,\n    end\n  });\n\n  return openmct;\n}\n\nexport function createMouseEvent(eventName) {\n  return new MouseEvent(eventName, {\n    bubbles: true,\n    cancelable: true,\n    view: window\n  });\n}\n\nexport function spyOnBuiltins(functionNames, object = window) {\n  functionNames.forEach((functionName) => {\n    if (nativeFunctions[functionName]) {\n      throw `Builtin spy function already defined for ${functionName}`;\n    }\n\n    nativeFunctions.push({\n      functionName,\n      object,\n      nativeFunction: object[functionName]\n    });\n    spyOn(object, functionName);\n  });\n}\n\nexport function clearBuiltinSpies() {\n  nativeFunctions.forEach(clearBuiltinSpy);\n  nativeFunctions = [];\n}\n\nexport function resetApplicationState(openmct) {\n  clearBuiltinSpies();\n\n  if (openmct) {\n    openmct.destroy();\n  }\n\n  if (window.location.hash !== '#' && window.location.hash !== '') {\n    window.location.hash = '#';\n    // Optionally wait for hashchange if necessary\n    return new Promise((resolve) => {\n      // eslint-disable-next-line func-style\n      const onHashChange = () => {\n        window.removeEventListener('hashchange', onHashChange);\n        resolve();\n      };\n      window.addEventListener('hashchange', onHashChange);\n    });\n  } else {\n    return Promise.resolve();\n  }\n}\n\n// required: key\n// optional: element, keyCode, type\nexport function simulateKeyEvent(opts) {\n  if (!opts.key) {\n    console.warn('simulateKeyEvent needs a key');\n\n    return;\n  }\n\n  const el = opts.element || document;\n  const key = opts.key;\n  const keyCode = opts.keyCode || key;\n  const type = opts.type || 'keydown';\n  const event = new Event(type);\n\n  event.keyCode = keyCode;\n  event.key = key;\n\n  el.dispatchEvent(event);\n}\n\nfunction clearBuiltinSpy(funcDefinition) {\n  funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction;\n}\n\nexport function getLatestTelemetry(telemetry = [], opts = {}) {\n  let latest = [];\n  let timeFormat = opts.timeFormat || 'utc';\n\n  if (telemetry.length) {\n    latest = telemetry.reduce((prev, cur) => {\n      return prev[timeFormat] > cur[timeFormat] ? prev : cur;\n    });\n  }\n\n  return latest;\n}\n\n// EXAMPLE:\n// getMockObjects({\n//     name: 'Jamie Telemetry',\n//     keys: ['test','other','yeah','sup'],\n//     format: 'local',\n//     telemetryConfig: {\n//          hints: {\n//              test: {\n//                  domain: 1\n//              },\n//              other: {\n//                  range: 2\n//              }\n//          }\n//      }\n// })\nexport function getMockObjects(opts = {}) {\n  opts.type = opts.type || 'default';\n  if (opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) {\n    throw `\"getMockObjects\" optional parameter \"objectKeyStrings\" must be an array of string object keys`;\n  }\n\n  let requestedMocks = {};\n\n  if (!opts.objectKeyStrings) {\n    requestedMocks = copyObj(mockObjects[opts.type]);\n  } else {\n    opts.objectKeyStrings.forEach((objKey) => {\n      if (mockObjects[opts.type] && mockObjects[opts.type][objKey]) {\n        requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]);\n      } else {\n        throw `No mock object for object key \"${objKey}\" of type \"${opts.type}\"`;\n      }\n    });\n  }\n\n  // build out custom telemetry mappings if necessary\n  if (requestedMocks.telemetry && opts.telemetryConfig) {\n    let keys = opts.telemetryConfig.keys;\n    let format = opts.telemetryConfig.format || 'utc';\n    let hints = opts.telemetryConfig.hints;\n    let values;\n\n    // if utc, keep default\n    if (format === 'utc') {\n      // save for later if new keys\n      if (keys) {\n        format = requestedMocks.telemetry.telemetry.values.find((vals) => vals.key === 'utc');\n      }\n    } else {\n      format = {\n        key: format,\n        name: 'Time',\n        format: format === 'local' ? 'local-format' : format,\n        hints: {\n          domain: 1\n        }\n      };\n    }\n\n    if (keys) {\n      values = keys.map((key) => ({\n        key,\n        name: key + ' attribute'\n      }));\n      values.push(format); // add time format back in\n    } else {\n      values = requestedMocks.telemetry.telemetry.values;\n    }\n\n    if (hints) {\n      for (let val of values) {\n        if (hints[val.key]) {\n          val.hints = hints[val.key];\n        }\n      }\n    }\n\n    requestedMocks.telemetry.telemetry.values = values;\n  }\n\n  // overwrite any field keys\n  if (opts.overwrite) {\n    for (let mock in requestedMocks) {\n      if (opts.overwrite[mock]) {\n        requestedMocks[mock] = Object.assign(requestedMocks[mock], opts.overwrite[mock]);\n      }\n    }\n  }\n\n  return requestedMocks;\n}\n\n// EXAMPLE:\n// getMockTelemetry({\n//     name: 'My Telemetry',\n//     keys: ['test','other','yeah','sup'],\n//     count: 8,\n//     format: 'local'\n// })\nexport function getMockTelemetry(opts = {}) {\n  let count = opts.count || 2;\n  let format = opts.format || 'utc';\n  let name = opts.name || 'Mock Telemetry Datum';\n  let keyCount = 2;\n  let keys = false;\n  let telemetry = [];\n\n  if (opts.keys && Array.isArray(opts.keys)) {\n    keyCount = opts.keys.length;\n    keys = opts.keys;\n  } else if (opts.keyCount) {\n    keyCount = opts.keyCount;\n  }\n\n  for (let i = 1; i < count + 1; i++) {\n    let datum = {\n      [format]: i,\n      name\n    };\n\n    for (let k = 1; k < keyCount + 1; k++) {\n      let key = keys ? keys[k - 1] : 'some-key-' + k;\n      let value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k;\n      datum[key] = value;\n    }\n\n    telemetry.push(datum);\n  }\n\n  return telemetry;\n}\n\n// used to inject into tests that require a render\nexport function renderWhenVisible(func) {\n  func();\n  return true;\n}\n\n// copy objects a bit more easily\nfunction copyObj(obj) {\n  return JSON.parse(JSON.stringify(obj));\n}\n\n// add any other necessary types to this mockObjects object\nfunction setMockObjects() {\n  return {\n    default: {\n      folder: {\n        identifier: {\n          namespace: '',\n          key: 'folder-object'\n        },\n        name: 'Test Folder Object',\n        type: 'folder',\n        composition: [],\n        location: 'mine'\n      },\n      ladTable: {\n        identifier: {\n          namespace: '',\n          key: 'lad-object'\n        },\n        type: 'LadTable',\n        composition: []\n      },\n      ladTableSet: {\n        identifier: {\n          namespace: '',\n          key: 'lad-set-object'\n        },\n        type: 'LadTableSet',\n        composition: []\n      },\n      telemetry: {\n        identifier: {\n          namespace: '',\n          key: 'telemetry-object'\n        },\n        type: 'test-telemetry-object',\n        name: 'Test Telemetry Object',\n        telemetry: {\n          values: [\n            {\n              key: 'name',\n              name: 'Name',\n              format: 'string'\n            },\n            {\n              key: 'utc',\n              name: 'Time',\n              format: 'utc',\n              hints: {\n                domain: 1\n              }\n            },\n            {\n              name: 'Some attribute 1',\n              key: 'some-key-1',\n              hints: {\n                range: 1\n              }\n            },\n            {\n              name: 'Some attribute 2',\n              key: 'some-key-2'\n            }\n          ]\n        }\n      }\n    },\n    otherType: {\n      example: {}\n    }\n  };\n}\n"
  },
  {
    "path": "src/utils/textHighlight/TextHighlight.vue",
    "content": "<!--\n Open MCT, Copyright (c) 2014-2024, United States Government\n as represented by the Administrator of the National Aeronautics and Space\n Administration. All rights reserved.\n\n Open MCT is licensed under the Apache License, Version 2.0 (the\n \"License\"); you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0.\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n License for the specific language governing permissions and limitations\n under the License.\n\n Open MCT includes source code licensed under additional open source\n licenses. See the Open Source Licenses file (LICENSES.md) included with\n this source code distribution or the Licensing information page available\n at runtime from the About dialog for additional information.\n-->\n<template>\n  <!-- eslint-disable-next-line vue/no-v-html -->\n  <span v-html=\"highlightedText\"></span>\n</template>\n\n<script>\nexport default {\n  props: {\n    text: {\n      type: String,\n      required: true\n    },\n    highlight: {\n      type: String,\n      default() {\n        return '';\n      }\n    },\n    highlightClass: {\n      type: String,\n      default() {\n        return 'highlight';\n      }\n    }\n  },\n  computed: {\n    highlightedText() {\n      const highlight = this.highlight;\n\n      const normalCharsRegex = /^[^A-Za-z0-9]+$/g;\n\n      const newHighLight = normalCharsRegex.test(highlight) ? `\\\\${highlight}` : highlight;\n\n      const highlightRegex = new RegExp(`(?<!<[^>]*)(${newHighLight})`, 'gi');\n\n      const replacement = `<span class=\"${this.highlightClass}\">${highlight}</span>`;\n\n      return this.text.replace(highlightRegex, replacement);\n    }\n  }\n};\n</script>\n"
  },
  {
    "path": "src/utils/throttle.js",
    "content": "/**\n * Creates a throttled function that only invokes the provided function at most once every\n * specified number of milliseconds. Subsequent calls within the waiting period will be ignored.\n * @param {Function} func The function to throttle.\n * @param {number} wait The number of milliseconds to wait between successive calls to the function.\n * @return {Function} Returns the new throttled function.\n */\nexport default function throttle(func, wait) {\n  let timeout;\n  let result;\n  let previous = 0;\n\n  return function (...args) {\n    const now = new Date().getTime();\n    const remaining = wait - (now - previous);\n\n    if (remaining <= 0 || remaining > wait) {\n      if (timeout) {\n        clearTimeout(timeout);\n        timeout = null;\n      }\n\n      previous = now;\n      result = func(...args);\n    } else if (!timeout) {\n      timeout = setTimeout(() => {\n        previous = new Date().getTime();\n        timeout = null;\n        result = func(...args);\n      }, remaining);\n    }\n    return result;\n  };\n}\n"
  },
  {
    "path": "src/utils/useEventBus.js",
    "content": "import emitter from 'tiny-emitter/instance.js';\nimport { ref } from 'vue';\n\nexport function useEventBus() {\n  // Create a reactive reference to the emitter\n  const reactiveEmitter = ref(emitter);\n\n  // Expose the emitter's methods\n  const EventBus = {\n    $on: (...args) => reactiveEmitter.value.on(...args),\n    $once: (...args) => reactiveEmitter.value.once(...args),\n    $off: (...args) => reactiveEmitter.value.off(...args),\n    $emit: (...args) => reactiveEmitter.value.emit(...args)\n  };\n\n  return {\n    EventBus\n  };\n}\n"
  },
  {
    "path": "src/utils/visibility/VisibilityObserver.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\n/**\n * Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport.\n */\nclass VisibilityObserver {\n  /**\n   * @type {HTMLElement | null}\n   */\n  #element;\n  /**\n   * @type {IntersectionObserver | null}\n   */\n  #observer;\n  /**\n   * @type {(() => void) | null}\n   */\n  lastUnfiredFunc;\n  /**\n   * @type {boolean | null}\n   */\n  isIntersecting;\n  /**\n   * @type {boolean}\n   */\n  calledOnce;\n\n  /**\n   * Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.\n   *\n   * @param {HTMLElement} element - The DOM element to observe for visibility changes.\n   * @param {HTMLElement} rootContainer - The DOM element that is the root of the viewport.\n   * @throws {Error} If element is not provided.\n   */\n  constructor(element, rootContainer) {\n    if (!element || !rootContainer) {\n      throw new Error(`VisibilityObserver must be created with an element and a rootContainer.`);\n    }\n    this.#element = element;\n    this.isIntersecting = true;\n    this.calledOnce = false;\n    const options = {\n      root: rootContainer\n    };\n    this.#observer = new IntersectionObserver(this.#observerCallback, options);\n    this.lastUnfiredFunc = null;\n    this.renderWhenVisible = this.renderWhenVisible.bind(this);\n  }\n\n  /**\n   * @returns {boolean}\n   */\n  #inOverlay() {\n    return this.#element?.closest('.js-overlay');\n  }\n\n  #observerCallback = (entries) => {\n    const entry = entries[0];\n    if (entry && entry.target === this.#element) {\n      if (this.#inOverlay() && !entry.isIntersecting) {\n        this.isIntersecting = true;\n      } else {\n        this.isIntersecting = entry.isIntersecting;\n      }\n      if (this.isIntersecting && this.lastUnfiredFunc) {\n        window.requestAnimationFrame(this.lastUnfiredFunc);\n        this.lastUnfiredFunc = null;\n      }\n    }\n  };\n\n  /**\n   * Executes a function within requestAnimationFrame if the observed element is visible.\n   * If the element is not visible, the function is stored and called when the element becomes visible.\n   * Note that if called multiple times while not visible, only the last execution is stored and executed.\n   *\n   * @param {() => void} func - The function to execute.\n   * @returns {boolean} True if the function was executed immediately, false otherwise.\n   */\n  renderWhenVisible(func) {\n    if (!this.calledOnce) {\n      this.calledOnce = true;\n      if (!this.#observer || !this.#element) {\n        this.lastUnfiredFunc = func;\n        return false;\n      }\n      this.#observer.observe(this.#element);\n    } else if (!this.isIntersecting) {\n      this.lastUnfiredFunc = func;\n      return false;\n    }\n    window.requestAnimationFrame(func);\n    return true;\n  }\n\n  /**\n   * Stops observing the element for visibility changes and cleans up resources to prevent memory leaks.\n   */\n  destroy() {\n    if (this.#observer && this.#element) {\n      this.#observer.unobserve(this.#element);\n    }\n\n    this.#element = null;\n    this.isIntersecting = null;\n    this.#observer = null;\n    this.lastUnfiredFunc = null;\n  }\n}\n\nexport default VisibilityObserver;\n"
  },
  {
    "path": "src/utils/vue/useDragResizer.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { ref } from 'vue';\n\n/**\n * @typedef {Object} DragResizerOptions the options object\n * @property {number} [initialX=0] the initial x of the object to track size for\n * @property {number} [initialY=0] the initial y of the object to track size for\n * @property {Function} [callback] the function to call when drag is complete\n */\n\n/**\n * @typedef {Object} ReturnObject the return object\n * @property {number} x the reactive horizontal size during/post drag\n * @property {number} y the reactive vertical size during/post drag\n * @property {function} mousedown\n */\n\n/**\n * Defines a drag resizer hook that tracks the size of an object\n * in vertical and horizontal direction on drag\n * @param {DragResizerOptions} [param={}] the options object\n * @returns {ReturnObject}\n */\nexport function useDragResizer({ initialX = 0, initialY = 0, callback } = {}) {\n  const x = ref(initialX);\n  const y = ref(initialY);\n  const isDragging = ref(false);\n\n  let dragStartX;\n  let dragStartY;\n  let dragStartClientX;\n  let dragStartClientY;\n\n  /**\n   * Begins the tracking process for the drag resizer\n   * and attaches mousemove and mousedown listeners to track size after drag completion\n   * Attach to a mousedown event for a draggable element\n   * @param {*} event the mousedown event\n   */\n  function mousedown(event) {\n    dragStartX = x.value;\n    dragStartY = y.value;\n    dragStartClientX = event.clientX;\n    dragStartClientY = event.clientY;\n    isDragging.value = true;\n\n    document.addEventListener('mouseup', mouseup, {\n      once: true,\n      capture: true\n    });\n    document.addEventListener('mousemove', mousemove);\n    event.preventDefault();\n  }\n\n  function mousemove(event) {\n    const deltaX = event.clientX - dragStartClientX;\n    const deltaY = event.clientY - dragStartClientY;\n\n    x.value = dragStartX + deltaX;\n    y.value = dragStartY + deltaY;\n  }\n\n  function mouseup(event) {\n    dragStartX = undefined;\n    dragStartY = undefined;\n    dragStartClientX = undefined;\n    dragStartClientY = undefined;\n    isDragging.value = false;\n\n    document.removeEventListener('mousemove', mousemove);\n    event.preventDefault();\n    event.stopPropagation();\n\n    callback?.();\n  }\n\n  return {\n    mousedown,\n    x,\n    y,\n    isDragging\n  };\n}\n"
  },
  {
    "path": "src/utils/vue/useFlexContainers.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { computed, ref } from 'vue';\n\n/**\n * @typedef {Object} configuration\n * @property {boolean} rowsLayout true if containers arranged as rows, false if columns\n * @property {number} minContainerSize minimum size in pixels of a container\n * @property {Function} callback function to call when container resize completes\n */\n\n/**\n * Provides a means to size a collection of containers to a total size of 100%.\n * The containers will resize proportionally to fit the total size on add/remove.\n * The containers will initially be sized based on their scale property.\n * @param {import('vue').Ref<HTMLElement} element ref to the html total sizing element\n * @param {configuration} configuration object with configuration options\n * @returns\n */\nexport function useFlexContainers(\n  element,\n  { containers: existingContainers = [], rowsLayout, minContainerSize = 5, callback }\n) {\n  const containers = ref(existingContainers);\n  const maxMoveSize = ref(null);\n\n  const fixedContainersSize = computed(() => {\n    return containers.value\n      .filter((container) => container.fixed === true)\n      .reduce((size, currentContainer) => size + currentContainer.size, 0);\n  });\n\n  function addContainer(container) {\n    containers.value.push(container);\n\n    sizeItems(containers.value);\n    roundExcess(containers.value);\n    callback?.();\n  }\n\n  function removeContainer(index) {\n    const isFlexContainer = !containers.value[index].fixed;\n\n    containers.value.splice(index, 1);\n\n    if (isFlexContainer) {\n      sizeItems(containers.value);\n      roundExcess(containers.value);\n    }\n\n    callback?.();\n  }\n\n  function reorderContainers(reorderPlan) {\n    const oldContainers = containers.value.slice();\n\n    reorderPlan.forEach((reorderEvent) => {\n      containers.value[reorderEvent.newIndex] = oldContainers[reorderEvent.oldIndex];\n    });\n\n    callback?.();\n  }\n\n  function setContainers(_containers) {\n    containers.value = _containers;\n    sizeItems(containers.value);\n    roundExcess(containers.value);\n  }\n\n  function startContainerResizing(index) {\n    const beforeContainer = getBeforeContainer(index);\n    const afterContainer = getAfterContainer(index);\n\n    if (beforeContainer && afterContainer && !beforeContainer.fixed && !afterContainer.fixed) {\n      maxMoveSize.value = beforeContainer.size + afterContainer.size;\n    }\n  }\n\n  function getBeforeContainer(index) {\n    return containers.value\n      .slice(0, index + 1)\n      .filter((container) => !container.fixed === true)\n      .at(-1);\n  }\n\n  function getAfterContainer(index) {\n    return containers.value.slice(index + 1).filter((container) => !container.fixed === true)[0];\n  }\n\n  function containerResizing(index, delta, event) {\n    const beforeContainer = getBeforeContainer(index);\n    const afterContainer = getAfterContainer(index);\n    const percentageMoved = Math.round((delta / getElSize()) * 100);\n\n    if (beforeContainer && afterContainer && !beforeContainer.fixed && !afterContainer.fixed) {\n      beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved);\n      afterContainer.size = getContainerSize(afterContainer.size - percentageMoved);\n    } else {\n      console.warn(\n        'Drag requires two flexible containers. Use Elements Tab in Inspector to resize.'\n      );\n    }\n  }\n\n  function endContainerResizing() {\n    callback?.();\n  }\n\n  function getElSize() {\n    const elSize = rowsLayout === true ? element.value.offsetHeight : element.value.offsetWidth;\n    // TODO FIXME temporary patch for timeline\n    const timelineHeight = 32;\n\n    return elSize - fixedContainersSize.value - timelineHeight;\n  }\n\n  function getContainerSize(size) {\n    if (size < minContainerSize) {\n      return minContainerSize;\n    } else if (size > maxMoveSize.value - minContainerSize) {\n      return maxMoveSize.value - minContainerSize;\n    } else {\n      return size;\n    }\n  }\n\n  /**\n   * Resize flexible sized items so they fit proportionally within a viewport\n   * 1. add size to 0 sized items based on scale proportional to total scale\n   * 2. resize item sizes to equal 100\n   * if total size < 100, resize all items\n   * if total size > 100, resize only items not resized in step 1 (newly added)\n   *\n   * Items may have a scale (ie. items with composition)\n   *\n   * Handles single add or removal, as well as atypical use cases,\n   * such as composition out of sync with containers config\n   * due to composition edits outside of view\n   *\n   * Typically roundExcess is called afterwards to limit pixels and percents to integers\n   *\n   * @param {*} items\n   */\n  function sizeItems(items) {\n    let totalSize;\n    const flexItems = items.filter((item) => !item.fixed);\n\n    if (flexItems.length === 0) {\n      return;\n    }\n\n    if (flexItems.length === 1) {\n      flexItems[0].size = 100;\n      return;\n    }\n\n    const flexItemsWithSize = flexItems.filter((item) => item.size);\n    const flexItemsWithoutSize = flexItems.filter((item) => !item.size);\n    // total number of flexible items, adjusted by each item scale\n    const totalScale = flexItems.reduce((total, item) => {\n      const scale = item.scale ?? 1;\n      return total + scale;\n    }, 0);\n\n    flexItemsWithoutSize.forEach((item) => {\n      const scale = item.scale ?? 1;\n      item.size = Math.round((100 * scale) / totalScale);\n    });\n\n    totalSize = flexItems.reduce((total, item) => total + item.size, 0);\n\n    if (totalSize > 100) {\n      const addedSize = flexItemsWithoutSize.reduce((total, item) => total + item.size, 0);\n      const remainingSize = 100 - addedSize;\n\n      flexItemsWithSize.forEach((item) => {\n        item.size = Math.round((item.size * remainingSize) / 100);\n      });\n    } else if (totalSize < 100) {\n      flexItems.forEach((item) => {\n        item.size = Math.round((item.size * 100) / totalSize);\n      });\n    }\n  }\n\n  /**\n   *\n   * Rounds excess and applies to one of the items\n   * if an optional index is not specified, excess applied to last item\n   *\n   * @param {*} items\n   * @param {Number} (optional) index of the item to apply excess to in the event of rounding errors\n   */\n  function roundExcess(items, specifiedIndex) {\n    const flexItems = items.filter((item) => !item.fixed);\n\n    if (!flexItems.length) {\n      return;\n    }\n\n    const totalSize = flexItems.reduce((total, item) => total + item.size, 0);\n    const excess = Math.round(100 - totalSize);\n    let index;\n\n    if (specifiedIndex !== undefined && items[specifiedIndex] && !items[specifiedIndex].fixed) {\n      index = specifiedIndex;\n    }\n\n    if (index === undefined) {\n      index = items.findLastIndex((item) => !item.fixed);\n    }\n\n    if (index > -1) {\n      items[index].size += excess;\n    }\n  }\n\n  function toggleFixed(index, fixed) {\n    let addExcessToContainer;\n    const remainingItems = containers.value.slice();\n    const container = remainingItems.splice(index, 1)[0];\n\n    if (container.fixed !== fixed) {\n      if (fixed) {\n        // toggle flex to fixed\n        container.size = Math.round((container.size / 100) * getElSize());\n        container.fixed = fixed;\n        sizeItems(remainingItems);\n      } else {\n        // toggle fixed to flex\n        addExcessToContainer = index;\n        container.size = Math.round((container.size * 100) / (getElSize() + container.size));\n        const remainingSize = 100 - container.size;\n\n        remainingItems\n          .filter((item) => !item.fixed)\n          .forEach((item) => {\n            item.size = Math.round((item.size * remainingSize) / 100);\n          });\n\n        container.fixed = fixed;\n      }\n\n      roundExcess(containers.value, addExcessToContainer);\n      callback?.();\n    }\n  }\n\n  function sizeFixedContainer(index, size) {\n    const container = containers.value[index];\n\n    if (container.fixed) {\n      container.size = size;\n\n      callback?.();\n    } else {\n      console.warn('Use view drag resizing to resize flexible containers.');\n    }\n  }\n\n  return {\n    addContainer,\n    removeContainer,\n    reorderContainers,\n    setContainers,\n    containers,\n    startContainerResizing,\n    containerResizing,\n    endContainerResizing,\n    toggleFixed,\n    sizeFixedContainer\n  };\n}\n"
  },
  {
    "path": "src/utils/vue/useIsEditing.js",
    "content": "/*****************************************************************************\n * Open MCT, Copyright (c) 2014-2024, United States Government\n * as represented by the Administrator of the National Aeronautics and Space\n * Administration. All rights reserved.\n *\n * Open MCT is licensed under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n *\n * Open MCT includes source code licensed under additional open source\n * licenses. See the Open Source Licenses file (LICENSES.md) included with\n * this source code distribution or the Licensing information page available\n * at runtime from the About dialog for additional information.\n *****************************************************************************/\n\nimport { onBeforeUnmount, onMounted, ref } from 'vue';\n\nexport default function useIsEditing(openmct) {\n  const isEditing = ref(openmct.editor.isEditing());\n\n  onMounted(() => {\n    openmct.editor.on('isEditing', setIsEditing);\n  });\n\n  onBeforeUnmount(() => {\n    openmct.editor.off('isEditing', setIsEditing);\n  });\n\n  function setIsEditing(_isEditing) {\n    isEditing.value = _isEditing;\n  }\n\n  return {\n    isEditing\n  };\n}\n"
  },
  {
    "path": "src/utils/vueWrapHtmlElement.js",
    "content": "import { defineComponent, h, onMounted, ref } from 'vue';\n\n/**\n * Compatibility wrapper for wrapping an HTMLElement in a Vue component.\n *\n * @param {HTMLElement} element\n * @returns {import('vue').Component}\n */\nexport default function vueWrapHtmlElement(element) {\n  return defineComponent({\n    setup() {\n      /** @type {import('vue').Ref<HTMLElement | null>} */\n      const wrapper = ref(null);\n\n      onMounted(() => {\n        if (wrapper.value) {\n          wrapper.value.appendChild(element);\n        }\n      });\n\n      // Render function returning the wrapper div\n      // Use class 'u-contents' to set 'display: contents' of the parent div\n      return () => h('div', { ref: wrapper, class: 'u-contents' });\n    }\n  });\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "/* Note: Open MCT does not intend to support the entire Typescript ecosystem at this time.\n * This file is intended to add Intellisense for IDEs like VSCode. For more information\n * about Typescript, please discuss in https://github.com/nasa/openmct/discussions/4693\n */\n{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"baseUrl\": \"./\",\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationMap\": true,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitAny\": false,\n    \"outFile\": \"dist/types/index.d.ts\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\"\n  },\n  \"include\": [\n    \"src/api/**/*.js\"\n  ],\n  \"exclude\": [\n    \"**/*Spec.js\"\n  ]\n}"
  }
]